Skip to content

Latest commit

 

History

History
144 lines (113 loc) · 5.46 KB

closing_account.md

File metadata and controls

144 lines (113 loc) · 5.46 KB

Closing an account in Anchor Solana typically refers to the process of deallocating and cleaning up resources associated with a user's account on the Solana blockchain. This might involve releasing any outstanding loans, transferring or withdrawing funds, and closing the account altogether. Account on solana needs to be closed properly otherwise it cam lead to re-initialization of that account.

How account closing works: Whenever you're to close account you need to transfer the lamports from the account to another account. It triggers the solana run-time to garbage collect the account. The ownership of account changes from owner account to system program.

In below program you can see the function close() which is supposed to close the account. Here it takes to things:

  • account which is to be closed
  • destination to where the lamports are to be sent.

The program simply increases the balances of the destination address by number of lamports simultanously decreasing the balance of the account which is to be closed.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod closing_accounts_insecure {
    use super::*;

    pub fn close(ctx: Context<Close>) -> ProgramResult {
        let dest_starting_lamports = ctx.accounts.destination.lamports();

        **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports
            .checked_add(ctx.accounts.account.to_account_info().lamports())
            .unwrap();
        **ctx.accounts.account.to_account_info().lamports.borrow_mut() = 0;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Close<'info> {
    account: Account<'info, Data>,
    destination: AccountInfo<'info>,
}

#[account]
pub struct Data {
    data: u64,
}

The garbage collection occurs at the end of the transaction so it is possible for the attacker to hack and re-initialize the account. There can be many instructions in a single transaction so it is quite possible that ataacker can put instruction to close the account and to revoke it in same transaction and as the garbage collection doesn't trigger till end of the instruction it'll be revived.

In the following program we are using anchor's closed account disciminator to close the account. To close the account it must zeroed out all the data and use close account disciminator representing that the account has been closed. As for the PDA accounts which are created using program ids still can be vulnerable if the previous data is acsessed the attacker so you need both measures to be implemented to ensure the safe account closing

use anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR;
use anchor_lang::prelude::*;
use std::io::{Cursor, Write};
use std::ops::DerefMut;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod closing_accounts_secure {
    use super::*;

    pub fn close(ctx: Context<Close>) -> ProgramResult {
        let dest_starting_lamports = ctx.accounts.destination.lamports();

        let account = ctx.accounts.account.to_account_info();
        **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports
            .checked_add(account.lamports())
            .unwrap();
        **account.lamports.borrow_mut() = 0;

        let mut data = account.try_borrow_mut_data()?;
        for byte in data.deref_mut().iter_mut() {
            *byte = 0;
        }

        let dst: &mut [u8] = &mut data;
        let mut cursor = Cursor::new(dst);
        cursor.write_all(&CLOSED_ACCOUNT_DISCRIMINATOR).unwrap();

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Close<'info> {
    account: Account<'info, Data>,
    destination: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct ForceDefund<'info> {
    account: AccountInfo<'info>,
    destination: AccountInfo<'info>,
}

#[account]
pub struct Data {
    data: u64,
}

As specified above about the garbage collection of the closed account it is quite possible that even after the account is closed the attacker can invoke the account if the instruction is in the same transaction. To resolve and mitigate such issue another function such as force_defund() can be added to potentially check that the account is closed and lamports need to be transferred to another destination account.

    pub fn force_defund(ctx: Context<ForceDefund>) -> ProgramResult {
        let account = &ctx.accounts.account;

        let data = account.try_borrow_data()?;
        assert!(data.len() > 8);

        let mut discriminator = [0u8; 8];
        discriminator.copy_from_slice(&data[0..8]);
        if discriminator != CLOSED_ACCOUNT_DISCRIMINATOR {
            return Err(ProgramError::InvalidAccountData);
        }

        let dest_starting_lamports = ctx.accounts.destination.lamports();

        **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports
            .checked_add(account.lamports())
            .unwrap();
        **account.lamports.borrow_mut() = 0;

        Ok(())
    }

Anchor has #[account(close = <target_account>)] constraint which handles the following

  • Transfers the account’s lamports to the given <target_account>
  • Zeroes out the account data
  • Sets the account discriminator to the CLOSED_ACCOUNT_DISCRIMINATOR variant All you have to do is add it in the account validation struct to the account you want closed:
#[derive(Accounts)]
pub struct Close<'info> {
    #[account(mut, close = destination)]
    account: Account<'info, Data>,
    #[account(mut)]
    destination: AccountInfo<'info>,
}