Hacktricks-skills defi-amm-cache-exploit-audit

Audit DeFi AMMs for virtual balance cache exploitation vulnerabilities. Use this skill whenever you're reviewing AMM code, analyzing DeFi protocol security, investigating accounting bugs, or looking for cache-related exploits in weighted pools, stableswap implementations, or any protocol that caches derived state (virtual balances, TWAP snapshots, invariant helpers). Trigger this when users mention AMM audits, DeFi security reviews, cache invalidation issues, or when examining code with packed storage arrays and proportional updates.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation/SKILL.MD
source content

DeFi AMM Cache Exploit Audit

This skill helps you identify and analyze virtual balance cache exploitation vulnerabilities in DeFi AMMs, where gas-saving caches are weaponized during boundary-state transitions.

Vulnerability Pattern

The core issue: Cached derived state is not reset when pool liquidity hits zero, allowing attackers to poison the cache and mint inflated LP tokens on the "first deposit" after a full drain.

Key Indicators

Look for these patterns in AMM code:

  1. Cached virtual balances stored in packed arrays for gas efficiency
  2. Proportional updates using floor division that leave rounding residues
  3. Missing cache reset when
    totalSupply == 0
    or
    totalLiquidity == 0
  4. Initialization branches that read cached values instead of recomputing from actual balances
  5. No sanity bounds on mint amounts relative to actual deposits

Detection Workflow

Step 1: Identify Cached State

Search for storage patterns that cache derived values:

// Red flags:
uint256[] packed_vbs;           // Virtual balances cache
uint256[] cached_rates;         // Oracle rate cache
uint256 invariant_helper;       // Cached invariant
uint256[] twap_snapshots;       // TWAP cache

Step 2: Trace Update Logic

Find where caches are updated. Look for:

// Vulnerable pattern: proportional decrement with truncation
packed_vbs[i] -= packed_vbs[i] * burnAmount / supplyBefore;

// Safe pattern: explicit reset on zero supply
if (totalSupply == 0) {
    for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}

Step 3: Check Boundary Conditions

Examine initialization paths:

// Vulnerable: trusts cache on first deposit
if (prevSupply == 0) {
    sumVb = _calc_vb_prod_sum();  // Reads stale cache!
}

// Safe: recomputes from ground truth
if (prevSupply == 0) {
    sumVb = _recompute_from_balances();  // Ignores cache
}

Step 4: Verify Mint Bounds

Check if mint logic has sanity checks:

// Missing: no bounds on mint amount
uint256 lpToMint = pricingInvariant(sumVb, prevSupply, amountsIn);

// Should have: revert if mint exceeds reasonable ratio
require(lpToMint <= depositValue * MAX_INIT_RATIO, "Mint ratio exceeded");

Exploit Simulation

Use the included scripts to test for this vulnerability:

# Analyze Solidity code for cache patterns
python scripts/detect_cache_vulnerabilities.py <contract.sol>

# Simulate the exploit conditions
python scripts/simulate_cache_poison.py --config <config.json>

Remediation Checklist

Immediate Fixes

  1. Reset caches on zero supply:

    if (totalSupply == 0) {
        for (uint256 i; i < tokens.length; ++i) {
            packed_vbs[i] = 0;
        }
    }
    
  2. Recompute on initialization:

    if (prevSupply == 0) {
        // Ignore cache, compute from actual balances
        sumVb = _compute_from_actual_balances();
    }
    
  3. Add mint sanity bounds:

    uint256 maxMint = depositValue * 100; // 100x max ratio
    require(lpToMint <= maxMint, "Mint ratio exceeded");
    

Long-term Improvements

  • Differential testing: Recompute invariants off-chain with high-precision math after every state transition
  • Rounding residue drains: Aggregate dust into treasury/burn to prevent cache drift
  • Runtime simulations: Before critical operations, recompute from scratch and compare with cached values
  • Multi-transaction monitoring: Track sequences of deposit/withdraw cycles that could poison state

Real-World Case Study: yETH (Nov 2025)

Impact: ~$9M drained with only 16 wei of attacker capital

Attack flow:

  1. Flash-loan multiple LSD assets (wstETH, rETH, cbETH, etc.)
  2. Loop deposits/withdrawals to accumulate rounding residues in
    packed_vbs[]
  3. Burn all LP tokens, setting
    totalSupply == 0
    while cache remains poisoned
  4. Deposit 16 wei total across LSD slots
  5. Pool reads stale cache, mints ~2.35×10²⁶ yETH
  6. Redeem LP position, drain vault, repay flash loans

Root cause:

remove_liquidity()
used proportional decrements with floor division, leaving residues.
add_liquidity()
trusted the cache on first deposit instead of recomputing.

Testing Strategy

Unit Tests

function testCacheResetOnZeroSupply() public {
    // 1. Add liquidity
    // 2. Remove liquidity in cycles to accumulate residues
    // 3. Burn final LP tokens (supply == 0)
    // 4. Assert: all cached values are zero
    // 5. Add new liquidity
    // 6. Assert: mint amount matches actual deposit value
}

Fuzz Tests

  • Fuzz deposit/withdraw cycles with varying amounts
  • Check that
    cached_value ≈ actual_value
    within epsilon after each operation
  • Test boundary:
    totalSupply == 0
    transitions
  • Test mint ratio:
    lpMinted / depositValue
    should be bounded

References