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.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation/SKILL.MDDeFi 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:
- Cached virtual balances stored in packed arrays for gas efficiency
- Proportional updates using floor division that leave rounding residues
- Missing cache reset when
ortotalSupply == 0totalLiquidity == 0 - Initialization branches that read cached values instead of recomputing from actual balances
- 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
-
Reset caches on zero supply:
if (totalSupply == 0) { for (uint256 i; i < tokens.length; ++i) { packed_vbs[i] = 0; } } -
Recompute on initialization:
if (prevSupply == 0) { // Ignore cache, compute from actual balances sumVb = _compute_from_actual_balances(); } -
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:
- Flash-loan multiple LSD assets (wstETH, rETH, cbETH, etc.)
- Loop deposits/withdrawals to accumulate rounding residues in
packed_vbs[] - Burn all LP tokens, setting
while cache remains poisonedtotalSupply == 0 - Deposit 16 wei total across LSD slots
- Pool reads stale cache, mints ~2.35×10²⁶ yETH
- 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
within epsilon after each operationcached_value ≈ actual_value - Test boundary:
transitionstotalSupply == 0 - Test mint ratio:
should be boundedlpMinted / depositValue