Implicit Instruction Ordering Remediation
How to fix implicit instruction ordering dependencies not enforced on all control flow paths.
Implicit Instruction Ordering Remediation
Overview
Related Detector: Implicit Instruction Ordering
Implicit instruction ordering vulnerabilities occur when required validation (signer checks, owner checks, writable checks) exists on some control flow paths but not all. An attacker can reach a sensitive operation via the unvalidated path. The fix is to move all validation before any branching, so every downstream path inherits the validated state.
Recommended Fix
Before (Vulnerable)
pub fn process(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
if data[0] == 0 {
// Path A: has signer check
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**vault.try_borrow_mut_lamports()? -= 100;
} else {
// Path B: missing signer check
**vault.try_borrow_mut_lamports()? -= 50;
}
Ok(())
}
After (Fixed)
pub fn process(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
// All validation before branching
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if !vault.is_writable {
return Err(ProgramError::InvalidArgument);
}
if data[0] == 0 {
**vault.try_borrow_mut_lamports()? -= 100;
} else {
**vault.try_borrow_mut_lamports()? -= 50;
}
Ok(())
}
Alternative Mitigations
Use Anchor constraints to enforce validation before the handler executes:
#[derive(Accounts)]
pub struct Withdraw<'info> {
pub user: Signer<'info>, // Enforces signer check
#[account(mut, has_one = user)]
pub vault: Account<'info, Vault>, // Enforces owner + writable
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// All paths are validated before this point
ctx.accounts.vault.balance -= amount;
Ok(())
}
Extract a validated context struct that is constructed only after all checks pass. The handler receives the validated struct and cannot bypass checks:
struct ValidatedContext<'a> {
authority: &'a AccountInfo<'a>,
vault: &'a AccountInfo<'a>,
}
fn validate(accounts: &[AccountInfo]) -> Result<ValidatedContext, ProgramError> {
let authority = &accounts[0];
let vault = &accounts[1];
if !authority.is_signer { return Err(MissingRequiredSignature); }
if vault.owner != &crate::ID { return Err(IncorrectProgramId); }
Ok(ValidatedContext { authority, vault })
}
Common Mistakes
Duplicating validation in each branch. While technically correct, this is error-prone and difficult to maintain. Move shared validation before the branch instead.
Validating the signer but not the owner (or vice versa). Each sensitive operation type has specific validation requirements. Transfers need signer checks, stores need writable checks, and data reads need owner checks.
Assuming CPI targets validate on behalf of the caller. Each program must validate its own preconditions. A CPI callee validates its own constraints, not the constraints of the calling program.