Skip to content

Commit ec10724

Browse files
authored
Merge pull request #1629 from HackTricks-wiki/update_The__9M_yETH_Exploit__How_16_Wei_Became_Infinite_T_20251202_183152
The $9M yETH Exploit How 16 Wei Became Infinite Tokens
2 parents 3ce72ad + dc053b2 commit ec10724

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393

9494
# 🧙‍♂️ Generic Hacking
9595

96+
- [Defi Amm Virtual Balance Cache Exploitation](blockchain/blockchain-and-crypto-currencies/defi-amm-virtual-balance-cache-exploitation.md)
9697
- [Archive Extraction Path Traversal](generic-hacking/archive-extraction-path-traversal.md)
9798
- [Brute Force - CheatSheet](generic-hacking/brute-force.md)
9899
- [Esim Javacard Exploitation](generic-hacking/esim-javacard-exploitation.md)

src/blockchain/blockchain-and-crypto-currencies/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,12 @@ If you are researching practical exploitation of DEXes and AMMs (Uniswap v4 hook
201201
defi-amm-hook-precision.md
202202
{{#endref}}
203203

204+
For multi-asset weighted pools that cache virtual balances and can be poisoned when `supply == 0`, study:
205+
206+
{{#ref}}
207+
defi-amm-virtual-balance-cache-exploitation.md
208+
{{#endref}}
209+
204210
{{#include ../../banners/hacktricks-training.md}}
205211

206212

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# DeFi AMM Accounting Bugs & Virtual Balance Cache Exploitation
2+
3+
{{#include ../../banners/hacktricks-training.md}}
4+
5+
## Overview
6+
7+
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.
8+
9+
Key ingredients:
10+
11+
- **Derived-state caching**: expensive oracle lookups are avoided by persisting virtual balances and incrementally updating them.
12+
- **Missing reset when `supply == 0`**: `remove_liquidity()` proportional decrements left non-zero residues in `packed_vbs[]` after each withdrawal cycle.
13+
- **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.
14+
- **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.
15+
16+
## Cache design & missing boundary handling
17+
18+
The vulnerable flow is simplified below:
19+
20+
```solidity
21+
function remove_liquidity(uint256 burnAmount) external {
22+
uint256 supplyBefore = totalSupply();
23+
_burn(msg.sender, burnAmount);
24+
25+
for (uint256 i; i < tokens.length; ++i) {
26+
packed_vbs[i] -= packed_vbs[i] * burnAmount / supplyBefore; // truncates to floor
27+
}
28+
29+
// BUG: packed_vbs not cleared when supply hits zero
30+
}
31+
32+
function add_liquidity(Amounts calldata amountsIn) external {
33+
uint256 prevSupply = totalSupply();
34+
uint256 sumVb = prevSupply == 0 ? _calc_vb_prod_sum() : _calc_adjusted_vb(amountsIn);
35+
uint256 lpToMint = pricingInvariant(sumVb, prevSupply, amountsIn);
36+
_mint(msg.sender, lpToMint);
37+
}
38+
39+
function _calc_vb_prod_sum() internal view returns (uint256 sum) {
40+
for (uint256 i; i < tokens.length; ++i) {
41+
sum += packed_vbs[i]; // assumes cache == 0 for a pristine pool
42+
}
43+
}
44+
```
45+
46+
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.
47+
48+
## Exploit playbook (yETH case study)
49+
50+
1. **Flash-loan working capital** – Borrow wstETH, rETH, cbETH, ETHx, WETH, etc. from Balancer/Aave to avoid tying up capital while manipulating the pool.
51+
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.
52+
3. **Force `supply == 0`** – Burn every remaining LP token so the pool believes it is empty. Implementation oversight leaves the poisoned `packed_vbs[]` untouched.
53+
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**.
54+
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.
55+
56+
## Generalized exploitation conditions
57+
58+
You can abuse similar AMMs when all of the following hold:
59+
60+
- **Cached derivatives of balances** (virtual balances, TWAP snapshots, invariant helpers) persist between transactions for gas savings.
61+
- **Partial updates truncate** results (floor division, fixed-point rounding), letting an attacker accumulate stateful residues via symmetric deposit/withdraw cycles.
62+
- **Boundary conditions reuse caches** instead of ground-truth recomputation, especially when `totalSupply == 0`, `totalLiquidity == 0`, or pool composition resets.
63+
- **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.
64+
- **Cheap capital is available** (flash loans or internal credit) to run dozens of state-adjusting operations inside one transaction or tightly choreographed bundle.
65+
66+
## Defensive engineering checklist
67+
68+
- **Explicit resets when supply/lpShares hit zero**:
69+
```solidity
70+
if (totalSupply == 0) {
71+
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
72+
}
73+
```
74+
Apply the same treatment to every cached accumulator derived from balances or oracle data.
75+
- **Recompute on initialization branches** – When `prev_supply == 0`, ignore caches entirely and rebuild virtual balances from actual token balances + live oracle rates.
76+
- **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.
77+
- **Rounding-residue drains** – Aggregate per-token dust into a sink (treasury/burn) so repeated proportional adjustments do not drift caches away from real balances.
78+
- **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.
79+
80+
## Monitoring & response
81+
82+
- **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.
83+
- **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.
84+
- **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.
85+
86+
## References
87+
88+
- [Check Point Research – The $9M yETH Exploit: How 16 Wei Became Infinite Tokens](https://research.checkpoint.com/2025/16-wei/)
89+
90+
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)