DeFi AMM Buchhaltungsfehler & Ausnutzung des Virtual Balance Cache
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Übersicht
Yearn Finance’s yETH pool (Nov 2025) zeigte, wie gas-sparende Caches in komplexen AMMs ausgenutzt werden können, wenn sie bei Übergängen zwischen Randzuständen nicht abgeglichen werden. Der gewichtete stableswap-Pool verfolgt bis zu 32 liquid staking derivatives (LSDs), konvertiert sie in ETH-äquivalente virtuelle Salden (vb_i = balance_i × rate_i / PRECISION) und speichert diese Werte in einem gepackten Storage-Array packed_vbs[]. Wenn alle LP-Token verbrannt sind, sinkt totalSupply korrekt auf Null, aber die zwischengespeicherten packed_vbs[i]-Slots behielten riesige historische Werte. Der nachfolgende Depositor wurde als “first” Liquidity Provider behandelt, obwohl der Cache noch phantomartige Liquidität enthielt, was einem Angreifer erlaubte, ~235 Septillionen yETH für nur 16 wei zu minten, bevor er ≈USD 9M an LSD-Kollateral abzweigte.
Schlüsselkomponenten:
- Derived-state caching: aufwändige Oracle-Abfragen werden vermieden, indem virtuelle Salden persistent gehalten und inkrementell aktualisiert werden.
- Missing reset when
supply == 0:remove_liquidity()proportionale Abzüge hinterließen nach jedem Withdraw-Zyklus nicht-null Rückstände inpacked_vbs[]. - Initialization branch trusts the cache:
add_liquidity()ruft_calc_vb_prod_sum()auf und liest einfachpacked_vbs[], wennprev_supply == 0, in der Annahme, dass der Cache ebenfalls auf Null gesetzt ist. - Flash-loan financed state poisoning: Einzahlungs-/Abhebungs-Schleifen verstärkten Rundungsreste ohne Kapitalbindung und ermöglichten ein katastrophales Over-Mint im “first deposit”-Pfad.
Cache-Design & fehlende Grenzfallbehandlung
Der verwundbare Ablauf ist unten vereinfacht dargestellt:
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
}
}
Weil remove_liquidity() nur proportionale Verminderungen anwandte, hinterließ jede Schleife fixed-point rounding dust. Nach ≳10 Einzahlungs-/Abhebungszyklen akkumulierten diese Reste zu extrem großen phantomartigen virtuellen Salden, während die On-Chain-Token-Salden nahezu leer waren. Das Verbrennen der letzten LP-Shares setzte totalSupply auf null, doch die Caches blieben gefüllt und bereiteten das Protokoll auf eine fehlerhafte Initialisierung vor.
Exploit playbook (yETH case study)
- Flash-loan working capital – Borrow wstETH, rETH, cbETH, ETHx, WETH, etc. from Balancer/Aave to avoid tying up capital while manipulating the pool.
- Poison
packed_vbs[]– Loop deposits and withdrawals across eight LSD assets. Each partial withdrawal truncatespacked_vbs[i] − vb_share, leaving >0 residues per token. Repeating the loop inflates phantom ETH-equivalent balances without raising suspicion because real balances roughly net out. - Force
supply == 0– Burn every remaining LP token so the pool believes it is empty. Implementation oversight leaves the poisonedpacked_vbs[]untouched. - Dust-size “first deposit” – Send a total of 16 wei divided across the supported LSD slots.
add_liquidity()seesprev_supply == 0, runs_calc_vb_prod_sum(), and reads the stale cache instead of recomputing from actual balances. The mint calculation therefore acts as if trillions of USD entered, emitting ~2.35×10^26 yETH. - Drain & repay – Redeem the inflated LP position for all vaulted LSDs, swap yETH→WETH on Balancer, convert to ETH via Uniswap v3, repay flash loans/fees, and launder the profit (e.g., through Tornado Cash). Net profit ≈USD 9M while only 16 wei of own funds ever touched the pool.
Generalized exploitation conditions
You can abuse similar AMMs when all of the following hold:
- Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) persist between transactions for gas savings.
- Partial updates truncate results (floor division, fixed-point rounding), letting an attacker accumulate stateful residues via symmetric deposit/withdraw cycles.
- Boundary conditions reuse caches instead of ground-truth recomputation, especially when
totalSupply == 0,totalLiquidity == 0, or pool composition resets. - Minting logic lacks ratio sanity checks (e.g., absence of
expected_value/actual_valuebounds) so a dust deposit can mint essentially the entire historic supply. - Cheap capital is available (flash loans or internal credit) to run dozens of state-adjusting operations inside one transaction or tightly choreographed bundle.
Defensive engineering checklist
- Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}
Apply the same treatment to every cached accumulator derived from balances or oracle data.
- Recompute on initialization branches – When
prev_supply == 0, ignore caches entirely and rebuild virtual balances from actual token balances + live oracle rates. - Minting sanity bounds – Revert if
lpToMint > depositValue × MAX_INIT_RATIOor if a single transaction mints >X% of historic supply while total deposits are below a minimal threshold. - Rounding-residue drains – Aggregate per-token dust into a sink (treasury/burn) so repeated proportional adjustments do not drift caches away from real balances.
- Differential tests – For every state transition (add/remove/swap), recompute the same invariant off-chain with high-precision math and assert equality within a tight epsilon even after full liquidity drains.
Monitoring & response
- Multi-transaction detection – Track sequences of near-symmetric deposit/withdraw events that leave the pool with low balances but high cached state, followed by
supply == 0. Single-transaction anomaly detectors miss these poisoning campaigns. - Runtime simulations – Before executing
add_liquidity(), recompute virtual balances from scratch and compare with cached sums; revert or pause if deltas exceed a basis-point threshold. - Flash-loan aware alerts – Flag transactions that combine large flash loans, exhaustive pool withdrawals, and a dust-sized final deposit; block or require manual approval.
References
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
HackTricks

