Skip to main content
Sigvex

Vault Manipulation

Detects unauthorized vault state modifications and missing invariant checks in DeFi vault operations.

Vault Manipulation

Overview

Remediation Guide: How to Fix Vault Manipulation

The vault manipulation detector identifies DeFi vault operations where state is modified without proper authorization or invariant validation. Vaults hold pooled user funds and expose parameters (collateral ratios, interest rates, reserve balances, fee structures) that, if manipulated, can drain the entire pool. This detector flags vault state writes that lack authority checks and PDA derivation, as well as missing ratio and balance invariant validations.

The detector targets functions with vault-related semantics (deposit, withdraw, update_vault, set_ratio, and similar naming patterns) and checks for the presence of authorization and invariant validation before state modifications.

Why This Is an Issue

DeFi vaults are high-value targets because they aggregate funds from many users. An attacker who can modify vault parameters without authorization can:

  • Set collateral ratios to zero, enabling under-collateralized borrowing
  • Manipulate interest rates to extract unfair returns
  • Modify reserve balances to bypass withdrawal limits
  • Change fee parameters to redirect protocol revenue
  • Alter liquidation thresholds to trigger or prevent liquidations

These attacks result in direct fund loss for depositors.

CWE mapping: CWE-284 (Improper Access Control), CWE-682 (Incorrect Calculation).

How to Resolve

// Before: Vulnerable -- vault state modified without authorization
pub fn update_vault_ratio(accounts: &[AccountInfo], new_ratio: u64) -> ProgramResult {
    let vault = &accounts[0];

    // No authority check, no ratio validation
    let mut vault_data = vault.try_borrow_mut_data()?;
    let mut vault_state = VaultState::try_from_slice(&vault_data[8..])?;
    vault_state.collateral_ratio = new_ratio;  // Attacker sets to 0
    vault_state.serialize(&mut &mut vault_data[8..])?;
    Ok(())
}

// After: Validate authority and enforce invariants
pub fn update_vault_ratio(accounts: &[AccountInfo], new_ratio: u64) -> ProgramResult {
    let vault = &accounts[0];
    let admin = &accounts[1];

    // FIXED: verify admin authority
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // FIXED: verify admin matches vault admin
    let vault_data = vault.try_borrow_data()?;
    let vault_state = VaultState::try_from_slice(&vault_data[8..])?;
    if vault_state.admin != *admin.key {
        return Err(ProgramError::InvalidAccountData);
    }

    // FIXED: enforce ratio bounds
    if new_ratio < MIN_COLLATERAL_RATIO || new_ratio > MAX_COLLATERAL_RATIO {
        return Err(ProgramError::InvalidArgument);
    }

    drop(vault_data);
    let mut vault_data = vault.try_borrow_mut_data()?;
    let mut vault_state = VaultState::try_from_slice(&vault_data[8..])?;
    vault_state.collateral_ratio = new_ratio;
    vault_state.serialize(&mut &mut vault_data[8..])?;
    Ok(())
}

Examples

Vulnerable Code

pub fn set_withdrawal_fee(accounts: &[AccountInfo], fee_bps: u64) -> ProgramResult {
    let vault = &accounts[0];

    // Anyone can call -- no access control
    let mut data = vault.try_borrow_mut_data()?;
    let mut state = VaultState::try_from_slice(&data[8..])?;
    state.withdrawal_fee_bps = fee_bps;  // Attacker sets to 10000 (100%)
    state.serialize(&mut &mut data[8..])?;
    Ok(())
}

Fixed Code

pub fn set_withdrawal_fee(accounts: &[AccountInfo], fee_bps: u64) -> ProgramResult {
    let vault = &accounts[0];
    let admin = &accounts[1];

    // FIXED: verify admin
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    let data = vault.try_borrow_data()?;
    let state = VaultState::try_from_slice(&data[8..])?;
    if state.admin != *admin.key {
        return Err(ProgramError::InvalidAccountData);
    }

    // FIXED: enforce fee bounds
    if fee_bps > MAX_FEE_BPS {
        return Err(ProgramError::InvalidArgument);
    }

    drop(data);
    let mut data = vault.try_borrow_mut_data()?;
    let mut state = VaultState::try_from_slice(&data[8..])?;
    state.withdrawal_fee_bps = fee_bps;
    state.serialize(&mut &mut data[8..])?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "vault-manipulation",
  "severity": "critical",
  "confidence": 0.75,
  "description": "Vault state is being modified without verifying the caller's authority. An attacker could manipulate vault parameters (collateral ratios, reserves, interest rates) to drain funds or disrupt the protocol.",
  "location": { "function": "set_withdrawal_fee", "offset": 3 }
}

Detection Methodology

The detector performs a two-pass analysis:

  1. Function filtering: Only analyzes functions with vault-related names (deposit, withdraw, update_vault, set_ratio, set_fee, liquidate, and similar patterns). This reduces false positives from non-DeFi programs.
  2. First pass – Validation scan: Scans the entire function for authority validation patterns (signer checks, owner checks, key checks) and invariant validation patterns (ratio comparisons, balance assertions, PDA derivations).
  3. Second pass – State modification detection: Identifies StoreAccountData statements targeting vault-related accounts. If no authority check or PDA derivation was found in the first pass, a finding is emitted.
  4. Invariant checking: Separately flags missing ratio/bounds checks when vault parameters are being modified.

Limitations

False positives:

  • Programs where authority validation is performed in a separate instruction within the same transaction.
  • Programs using governance-based authority that is validated through account state rather than signer checks.

False negatives:

  • Vault operations that use non-standard function names may not be analyzed.
  • Programs that modify vault state through intermediate helper accounts rather than direct StoreAccountData.

References