DeFi AMM Accounting Bugs & Virtual Balance Cache Exploitation

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें

अवलोकन

Yearn Finance के yETH пул (Nov 2025) ने दिखाया कि कैसे complex AMMs के अंदर gas-saving caches को तब हथियार बनाया जा सकता है जब boundary-state transitions के दौरान उन्हें reconcile नहीं किया जाता। यह weighted stableswap pool 32 तक के liquid staking derivatives (LSDs) को ट्रैक करता है, उन्हें ETH-समकक्ष Virtual Balances में कनवर्ट करता है (vb_i = balance_i × rate_i / PRECISION), और उन मानों को packed storage array packed_vbs[] में स्टोर करता है। जब सभी LP tokens burned हो जाते हैं, तो totalSupply सही ढंग से शून्य पर गिरता है लेकिन cached packed_vbs[i] slots बड़े historic मान बनाए रखते रहे। इसके बाद जो depositor आया उसे “first” liquidity provider माना गया भले ही cache में phantom liquidity बनी हुई थी, जिससे एक attacker केवल 16 wei में लगभग 235 septillion yETH mint कर सका और ≈USD 9M के LSD collateral को निकाल डाला।

मुख्य बिंदु:

  • Derived-state caching: mahalga-oracle lookups बचाने के लिए virtual balances को persist किया जाता है और incrementally update किया जाता है।
  • Missing reset when supply == 0: remove_liquidity() के proportional decrements withdrawal cycle के बाद packed_vbs[] में non-zero residues छोड़ गए।
  • Initialization branch trusts the cache: जब prev_supply == 0 होता है, add_liquidity() _calc_vb_prod_sum() को कॉल करता है और बस packed_vbs[] को पढ़ लेता है, यह मानते हुए कि cache भीゼro किया गया है।
  • Flash-loan financed state poisoning: deposit/withdraw loops ने rounding residues को बिना किसी capital lockup के amplify किया, जिससे “first deposit” path में catastrophic over-mint संभव हुआ।

Cache design & missing boundary handling

The vulnerable flow is simplified below:

function remove_liquidity(uint256 burnAmount) external {
uint256 supplyBefore = totalSupply();
_burn(msg.sender, burnAmount);

for (uint256 i; i < tokens.length; ++i) {
packed_vbs[i] -= packed_vbs[i] * burnAmount / supplyBefore; // truncates to floor
}

// BUG: packed_vbs not cleared when supply hits zero
}

function add_liquidity(Amounts calldata amountsIn) external {
uint256 prevSupply = totalSupply();
uint256 sumVb = prevSupply == 0 ? _calc_vb_prod_sum() : _calc_adjusted_vb(amountsIn);
uint256 lpToMint = pricingInvariant(sumVb, prevSupply, amountsIn);
_mint(msg.sender, lpToMint);
}

function _calc_vb_prod_sum() internal view returns (uint256 sum) {
for (uint256 i; i < tokens.length; ++i) {
sum += packed_vbs[i]; // assumes cache == 0 for a pristine pool
}
}

Because remove_liquidity() only applied proportional decrements, every loop left फिक्स्ड‑पॉइंट राउंडिंग अवशेष। After ≳10 deposit/withdraw cycles those residues accumulated into extremely large phantom virtual balances while the on-chain token balances were almost empty. Burning the final LP shares set totalSupply to zero yet caches stayed populated, priming the protocol for a त्रुटिपूर्ण आरंभिकरण।

Exploit playbook (yETH case study)

  1. Flash-loan working capital – Balancer/Aave से wstETH, rETH, cbETH, ETHx, WETH आदि उधार लें ताकि पूल को मैनिपुलेट करते समय अपनी पूँजी फँसी न रहे।
  2. Poison packed_vbs[] – आठ LSD assets में deposit और withdrawal को लूप करें। हर आंशिक निकासी packed_vbs[i] − vb_share को truncate कर देती है, जिससे प्रति टोकन >0 अवशेष बचते हैं। लूप दोहराने से वास्तविक बैलेंस लगभग net out रहते हुए भी phantom ETH‑समान बैलेंस फुल जा जाते हैं।
  3. Force supply == 0 – सभी शेष LP टोकन जला दें ताकि पूल खुद को खाली माने। कार्यान्वयन की चूक प्रदूषित packed_vbs[] को अपरिवर्तित छोड़ देती है।
  4. Dust-size “first deposit” – समर्थित LSD स्लॉट्स में विभाजित कुल 16 wei भेजें। add_liquidity() को prev_supply == 0 दिखता है, यह _calc_vb_prod_sum() चलाता है, और actual balances से पुनःगणना करने के बजाय stale cache को पढ़ता है। इसलिए mint गणना ऐसे व्यवहार करती है जैसे खरबों USD प्रविष्ट हुए हों, और ~2.35×10^26 yETH जारी हो जाते हैं।
  5. Drain & repay – सभी vaulted LSDs के लिए फूले हुए LP पोजिशन को redeem करें, Balancer पर yETH→WETH swap करें, Uniswap v3 के जरिए ETH में कनवर्ट करें, flash loans/fees चुकाएं, और मुनाफ़ा लाँडर करें (उदा., Tornado Cash के माध्यम से)। कुल शुद्ध लाभ ≈ USD 9M जबकि पूल में केवल 16 wei ही अपने फंड्स से टच हुआ था।

Generalized exploitation conditions

आप समान AMMs का दुरुपयोग तब कर सकते हैं जब निम्नलिखित सभी सच हों:

  • Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) ट्रांज़ैक्शन्स के बीच टिके रहते हैं ताकि गैस बच सके।
  • Partial updates truncate परिणामों को (floor division, फिक्स्ड‑पॉइंट राउंडिंग), जिससे एक हमलावर symmetric deposit/withdraw चक्रों के जरिए stateful अवशेष जमा कर सकता है।
  • Boundary conditions reuse caches बजाय कि ground‑truth से पुनर्गणना के, विशेषकर जब totalSupply == 0, totalLiquidity == 0, या पूल की composition रिसेट हो।
  • Minting logic lacks ratio sanity checks (उदा., expected_value/actual_value बाउंड्स की अनुपस्थिति) इसलिए एक dust deposit लगभग पूरा historic supply मिंट कर सकता है।
  • Cheap capital is available (flash loans या internal credit) ताकि एक ट्रांज़ैक्शन के अंदर या tightly choreographed bundle में दर्जनों state‑adjusting ऑपरेशन चलाए जा सकें।

Defensive engineering checklist

  • Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}

उसी उपचार को हर cached accumulator पर लागू करें जो बैलेंस या oracle डाटा से निकला हो।

  • Recompute on initialization branches – जब prev_supply == 0 हो, तो caches को पूरी तरह नज़रअंदाज़ करें और वर्चुअल बैलेंस actual token balances + live oracle rates से पुनर्निर्माण करें।
  • Minting sanity bounds – यदि lpToMint > depositValue × MAX_INIT_RATIO हो तो revert करें या यदि एकल ट्रांज़ैक्शन historic supply का >X% मिंट कर रहा हो जबकि कुल जमा न्यूनतम थ्रेशोल्ड से नीचे हों तो रोकें।
  • Rounding-residue drains – प्रत्येक टोकन के dust को एक sink (treasury/burn) में समेकित करें ताकि अनुपाती समायोजन बार‑बार कैश को वास्तविक बैलेंस से दूर न ले जाएँ।
  • Differential tests – हर state transition (add/remove/swap) के लिए ऑफ‑चेन उच्च‑प्रेसिजन गणित से वही invariant पुनःगणना करें और tight epsilon के भीतर समानता को सत्यापित करें, भले ही पूरी liquidity ड्रेन हो चुकी हो।

Monitoring & response

  • Multi-transaction detection – उन अनुक्रमों को ट्रैक करें जहाँ nær‑symmetric deposit/withdraw इवेंट्स पूल को कम बैलेंस के साथ पर उच्च cached state छोड़ते हैं, और उसके बाद supply == 0 होता है। single‑transaction anomaly detectors इन प्रदूषित करने के अभियानों को मिस कर देते हैं।
  • Runtime simulationsadd_liquidity() चलाने से पहले वर्चुअल बैलेंस को scratch से पुनर्गणना करें और cached sums से तुलना करें; यदि डेल्टा basis‑point थ्रेशोल्ड से अधिक हो तो revert या pause करें।
  • Flash-loan aware alerts – ऐसे ट्रांज़ैक्शन्स को flag करें जो बड़े flash loans, exhaustive pool withdrawals, और एक dust‑sized final deposit को मिलाते हों; इन्हें ब्लॉक या मैनुअल अप्रूवल की आवश्यकता रखें।

References

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें