Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/blockchain/blockchain-and-crypto-currencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}


Expand Down
Original file line number Diff line number Diff line change
@@ -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}}