Readonly Account Mutation
Detects attempts to modify accounts that have been verified as readonly.
Readonly Account Mutation
Overview
Remediation Guide: How to Fix Readonly Account Mutation
The readonly account mutation detector identifies Solana programs that explicitly verify an account is readonly (not writable) and then proceed to modify it through data stores or lamport transfers. This pattern indicates a logic error: the program has confirmed the account should not be modified, yet modifies it anyway. While the Solana runtime prevents the transaction from succeeding, the logic error signals confused access control.
Sigvex tracks CheckWritable statements and negated writable checks in branch conditions, then flags subsequent StoreAccountData or TransferLamports operations targeting the readonly-verified account.
Why This Is an Issue
- Runtime rejection: the Solana runtime will reject the transaction, wasting compute budget and confusing users with opaque errors.
- Logic error signal: the contradictory check-then-modify pattern indicates a deeper design issue in how the program handles account mutability.
- Access control confusion: the readonly check may have been intended to protect a different code path, but a subsequent modification bypasses that protection.
CWE mapping: CWE-754 (Improper Check for Unusual or Exceptional Conditions).
How to Resolve
Native Solana
// Option 1: Remove the readonly check if the account needs modification
if !account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Safe to modify
let mut data = account.data.borrow_mut();
data[8..16].copy_from_slice(&new_value.to_le_bytes());
// Option 2: Remove the modification if the account should be readonly
let data = account.data.borrow();
let value = u64::from_le_bytes(data[8..16].try_into()?);
// Read-only access only
Anchor
// Anchor enforces this at compile time:
// #[account(mut)] for writable, no annotation for readonly
#[derive(Accounts)]
pub struct ReadOnly<'info> {
pub state: Account<'info, MyState>, // readonly, cannot be modified
}
Examples
Vulnerable
// Verifies readonly, then modifies — logic error
if account.is_writable {
return Err(ProgramError::InvalidAccountData); // Reject writable
}
// But then modifies the readonly account!
**account.try_borrow_mut_lamports()? -= fee;
Fixed
// Consistent: check writable BEFORE modification
if !account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
**account.try_borrow_mut_lamports()? -= fee;
JSON Finding
{
"detector": "readonly-account-mutation",
"severity": "Low",
"confidence": 0.70,
"title": "Mutation of Readonly Account",
"description": "Account is modified after being verified as readonly.",
"cwe": [754]
}
Detection Methodology
The detector performs two passes. The first pass identifies accounts verified as readonly (via CheckWritable or negated IsWritable in branch conditions) and accounts verified as writable. The second pass flags StoreAccountData and TransferLamports operations targeting readonly-verified accounts that are not also writable-verified.
Limitations
- The detector requires explicit readonly verification patterns and cannot infer mutability from Anchor attribute macros in decompiled bytecode.
- Indirect writable checks through helper functions or CPI are not tracked.
- The severity is low because the Solana runtime prevents actual exploitation.
Related Detectors
- Readonly Misuse - detects writes without any writable check.