From dc053b22e84c39a3c92f51160146c78092379a64 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Tue, 2 Dec 2025 18:38:15 +0000 Subject: [PATCH] Add content from: The $9M yETH Exploit: How 16 Wei Became Infinite Tokens --- src/SUMMARY.md | 1 + .../README.md | 6 ++ ...-amm-virtual-balance-cache-exploitation.md | 90 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9ebd31195ad..8dbee3d44c5 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -93,6 +93,7 @@ # πŸ§™β€β™‚οΈ Generic Hacking + - [Defi Amm Virtual Balance Cache Exploitation](blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md) - [Archive Extraction Path Traversal](generic-hacking/archive-extraction-path-traversal.md) - [Brute Force - CheatSheet](generic-hacking/brute-force.md) - [Esim Javacard Exploitation](generic-hacking/esim-javacard-exploitation.md) diff --git a/src/blockchain/blockchain-and-crypto-currencies/README.md b/src/blockchain/blockchain-and-crypto-currencies/README.md index 7ddbd2abde8..3e6dd46c87f 100644 --- a/src/blockchain/blockchain-and-crypto-currencies/README.md +++ b/src/blockchain/blockchain-and-crypto-currencies/README.md @@ -201,6 +201,12 @@ If you are researching practical exploitation of DEXes and AMMs (Uniswap v4 hook defi-amm-hook-precision.md {{#endref}} +For multi-asset weighted pools that cache virtual balances and can be poisoned when `supply == 0`, study: + +{{#ref}} +defi-amm-virtual-balance-cache-exploitation.md +{{#endref}} + {{#include ../../banners/hacktricks-training.md}} diff --git a/src/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md b/src/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md new file mode 100644 index 00000000000..f05aa31c09a --- /dev/null +++ b/src/blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md @@ -0,0 +1,90 @@ +# DeFi AMM Accounting Bugs & Virtual Balance Cache Exploitation + +{{#include ../../banners/hacktricks-training.md}} + +## Overview + +Yearn Finance's yETH pool (Nov 2025) exposed how gas-saving caches inside complex AMMs can be weaponized when they are not reconciled during boundary-state transitions. The weighted stableswap pool tracks up to 32 liquid staking derivatives (LSDs), converts them to ETH-equivalent **virtual balances** (`vb_i = balance_i Γ— rate_i / PRECISION`), and stores those values in a packed storage array `packed_vbs[]`. When **all LP tokens are burned**, `totalSupply` correctly drops to zero but the cached `packed_vbs[i]` slots retained huge historic values. The subsequent depositor was treated as the "first" liquidity provider even though the cache still held phantom liquidity, letting an attacker mint ~235 septillion yETH for only **16 wei** before draining β‰ˆUSD 9M in LSD collateral. + +Key ingredients: + +- **Derived-state caching**: expensive oracle lookups are avoided by persisting virtual balances and incrementally updating them. +- **Missing reset when `supply == 0`**: `remove_liquidity()` proportional decrements left non-zero residues in `packed_vbs[]` after each withdrawal cycle. +- **Initialization branch trusts the cache**: `add_liquidity()` calls `_calc_vb_prod_sum()` and simply **reads** `packed_vbs[]` when `prev_supply == 0`, assuming the cache is also zeroed. +- **Flash-loan financed state poisoning**: deposit/withdraw loops amplified rounding residues with no capital lockup, enabling a catastrophic over-mint in the "first deposit" path. + +## Cache design & missing boundary handling + +The vulnerable flow is simplified below: + +```solidity +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 **fixed-point rounding dust**. 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 malformed initialization. + +## Exploit playbook (yETH case study) + +1. **Flash-loan working capital** – Borrow wstETH, rETH, cbETH, ETHx, WETH, etc. from Balancer/Aave to avoid tying up capital while manipulating the pool. +2. **Poison `packed_vbs[]`** – Loop deposits and withdrawals across eight LSD assets. Each partial withdrawal truncates `packed_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. +3. **Force `supply == 0`** – Burn every remaining LP token so the pool believes it is empty. Implementation oversight leaves the poisoned `packed_vbs[]` untouched. +4. **Dust-size "first deposit"** – Send a total of 16 wei divided across the supported LSD slots. `add_liquidity()` sees `prev_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**. +5. **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_value` bounds) 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**: + ```solidity + 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_RATIO` or 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 + +- [Check Point Research – The $9M yETH Exploit: How 16 Wei Became Infinite Tokens](https://research.checkpoint.com/2025/16-wei/) + +{{#include ../../banners/hacktricks-training.md}}