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:
- 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.
- 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).
- Second pass — State modification detection: Identifies
StoreAccountDatastatements targeting vault-related accounts. If no authority check or PDA derivation was found in the first pass, a finding is emitted. - 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.
Related Detectors
- Missing Signer Check — missing signer validation for admin operations
- Missing Owner Check — missing ownership validation on vault accounts
- DeFi Reentrancy — reentrancy in vault withdrawal flows