Rust
Pinocchio Quantum Vault

Pinocchio Quantum Vault

7 Graduates

Close Vault

The close instruction performs a complete withdrawal from quantum-resistant vaults, transferring all lamports to a single recipient account.

Unlike the split instruction, this provides a simpler full-withdrawal mechanism when fund distribution isn't needed.

Required Accounts

The instruction requires three accounts:

  • vault: Source vault containing stored lamports (must be mutable)
  • refund: Recipient account for the vault lamports balance (must be mutable)

Here's how it looks in code:

rust
pub struct CloseVaultAccounts<'a> {
    pub vault: &'a AccountInfo,
    pub refund: &'a AccountInfo,
}
 
impl<'a> TryFrom<&'a [AccountInfo]> for CloseVaultAccounts<'a> {
    type Error = ProgramError;
 
    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [vault, refund] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };
 
        Ok(Self { vault, refund })
    }
}
 

Account validation is handled by the runtime. If accounts don't meet requirements (mutability), the instruction will fail automatically.

Instruction Data

Two pieces of data are required:

  • signature: Winternitz signature proving ownership of the vault's keypair
  • bump: PDA derivation bump for optimization (1 byte)

Here's how it looks in code:

rust
pub struct CloseVaultInstructionData {
    pub signature: WinternitzSignature,
    pub bump: [u8; 1],
}
 
impl<'a> TryFrom<&'a [u8]> for CloseVaultInstructionData {
    type Error = ProgramError;
 
    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() != core::mem::size_of::<CloseVaultInstructionData>() {
            return Err(ProgramError::InvalidInstructionData);
        }
 
        let mut signature_array = MaybeUninit::<[u8; 896]>::uninit();
        unsafe {
            core::ptr::copy_nonoverlapping(data[0..896].as_ptr(), signature_array.as_mut_ptr() as *mut u8, 896);
        }
        
        Ok(Self { 
            signature: WinternitzSignature::from(unsafe { signature_array.assume_init() }),
            bump: data[896..897].try_into().map_err(|_| ProgramError::InvalidInstructionData)?,
        })
    }
}

Instruction Logic

The verification process follows these steps:

  1. Message Assembly: A 32-byte message is constructed containing the refund account publickey

  2. Signature Verification: The Winternitz signature is used to recover the original public key hash, which is then compared against the vault's PDA derivation seeds.

  3. PDA Validation: A fast equivalence check ensures the recovered hash matches the vault's PDA, proving the signer owns the vault.

  4. Fund Distribution If validation succeeds: the whole balance is transferred to the refund account and the vault acount is closed.

Since the program owns the vault account, you can transfer lamports directly without having to call a Cross-Program Invocation (CPI).

Here's how it looks in code:

rust
pub struct CloseVault<'a> {
    pub accounts: CloseVaultAccounts<'a>,
    pub instruction_data: CloseVaultInstructionData,
}
 
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for CloseVault<'a> {
    type Error = ProgramError;
 
    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let instruction_data = CloseVaultInstructionData::try_from(data)?;
        let accounts = CloseVaultAccounts::try_from(accounts)?;
 
        Ok(Self { accounts, instruction_data })
    }
}
 
impl<'a> CloseVault<'a> {
    pub const DISCRIMINATOR: &'a u8 = &2;
 
    pub fn process(&self) -> ProgramResult {
        // Recover our pubkey hash from the signature
        let hash = self.instruction_data.signature.recover_pubkey(self.accounts.refund.key()).merklize();
 
        // Fast PDA equivalence check
        if solana_nostd_sha256::hashv(&[
            hash.as_ref(),
            self.instruction_data.bump.as_ref(),
            crate::ID.as_ref(),
            b"ProgramDerivedAddress",
        ])
        .ne(self.accounts.vault.key())
        {
            return Err(ProgramError::MissingRequiredSignature);
        }
 
        // Close Vault and refund balance to Refund account
        *self.accounts.refund.try_borrow_mut_lamports()? += self.accounts.vault.lamports();
        self.accounts.vault.close()
    }
}

Reconstructing the message prevents signature replay attacks where malicious actors substitute different recipient accounts while MEV-ing valid signatures captured from the mempool.

Ready to take the challenge?
Contents
View Source
Blueshift © 2025Commit: de6a6b9