Bugs de Contabilidade em DeFi AMM & Exploração do Virtual Balance Cache
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Visão geral
Yearn Finance’s yETH pool (Nov 2025) expôs como caches que economizam gás dentro de AMMs complexos podem ser instrumentalizados quando não são reconciliados durante transições de estado-limite. O weighted stableswap pool monitora até 32 liquid staking derivatives (LSDs), converte-os para ETH-equivalente virtual balances (vb_i = balance_i × rate_i / PRECISION) e armazena esses valores em um array de armazenamento empacotado packed_vbs[]. Quando all LP tokens are burned, totalSupply corretamente cai para zero, mas os slots em cache packed_vbs[i] mantiveram enormes valores históricos. O depositante subsequente foi tratado como o “primeiro” provedor de liquidez mesmo que o cache ainda contivesse liquidez fantasma, permitindo que um atacante cunhasse ~235 septillion yETH por apenas 16 wei antes de drenar ≈USD 9M em colateral LSD.
Elementos-chave:
- Derived-state caching: consultas caras a oráculos são evitadas ao persistir virtual balances e atualizá-los incrementalmente.
- Missing reset when
supply == 0:remove_liquidity()reduções proporcionais deixaram resíduos não nulos empacked_vbs[]após cada ciclo de retirada. - Initialization branch trusts the cache:
add_liquidity()chama_calc_vb_prod_sum()e simplesmente lêpacked_vbs[]quandoprev_supply == 0, assumindo que o cache também foi zerado. - Flash-loan financed state poisoning: loops de depósito/retirada amplificaram resíduos de arredondamento sem travamento de capital, permitindo uma cunhagem catastrófica no caminho do “primeiro depósito”.
Design do cache e falta de tratamento de limites
O fluxo vulnerável é simplificado abaixo:
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)
- 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
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
HackTricks

