Duplicate Mutable Accounts Remediation
Overview
Duplicate mutable account vulnerabilities occur when a caller passes the same public key at two positions in an instruction’s accounts array where both are declared mutable. The remediation is to add an account-validation framework constraint asserting the two account keys are different, or to add an explicit key comparison at the start of native program handlers.
Related Detector: Duplicate Mutable Accounts
Recommended Fix
Before (Vulnerable)
use anchor_lang::prelude::*;
use anchor_spl::token::{TokenAccount};
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub source: Account<'info, TokenAccount>,
#[account(mut)] // Caller can pass the same account as source
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}
After (Fixed)
use anchor_lang::prelude::*;
use anchor_spl::token::{TokenAccount};
#[error_code]
pub enum MyError {
#[msg("Source and destination accounts must be different")]
SameAccount,
}
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub source: Account<'info, TokenAccount>,
#[account(
mut,
constraint = destination.key() != source.key() @ MyError::SameAccount
)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}
Alternative Mitigations
Native program: explicit key check at handler entry:
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError};
pub fn process_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let source = &accounts[0];
let destination = &accounts[1];
// Reject before any mutable borrows
if source.key == destination.key {
return Err(ProgramError::InvalidAccountData);
}
**source.lamports.borrow_mut() -= amount;
**destination.lamports.borrow_mut() += amount;
Ok(())
}
Three-account patterns — for swap instructions with A→B and B→A flows, also check that all three accounts (user, pool_a, pool_b) are mutually distinct:
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(mut)]
pub user_token_a: Account<'info, TokenAccount>,
#[account(mut, constraint = pool_token_a.key() != user_token_a.key())]
pub pool_token_a: Account<'info, TokenAccount>,
#[account(
mut,
constraint = pool_token_b.key() != user_token_a.key(),
constraint = pool_token_b.key() != pool_token_a.key()
)]
pub pool_token_b: Account<'info, TokenAccount>,
}
Common Mistakes
Relying on the SPL Token program to check — the SPL Token program validates source ≠ destination for its own instructions, but any business logic that processes accounts before the CPI can still be vulnerable.
Checking only the first pair — for instructions with three or more mutable accounts, all pairs must be validated, not just the most obvious ones.