DeFi AMM računovodstvene greške i eksploatacija keša virtuelnih balansa

Tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks

Pregled

Yearn Finance-ov yETH pool (Nov 2025) otkrio je kako keševi koji štede gas unutar složenih AMM-ova mogu biti iskorišćeni kada se ne usklade tokom prelaza granica stanja. Weighted stableswap pool prati do 32 liquid staking derivata (LSDs), konvertuje ih u ETH-ekvivalentne virtuelne bilanse (vb_i = balance_i × rate_i / PRECISION), i čuva te vrednosti u pakovanom storage nizu packed_vbs[]. Kada su svi LP tokeni spaljeni, totalSupply ispravno pada na nulu, ali keširani slotovi packed_vbs[i] su zadržali ogromne istorijske vrednosti. Sledeći deponent je tretiran kao “prvi” liquidity provider iako je keš i dalje držao fantomsku likvidnost, što je napadaču omogućilo da mintuje ~235 septiliona yETH za samo 16 wei pre nego što isprazni ≈USD 9M u LSD kolateralu.

Ključni elementi:

  • Keširanje izvedenog stanja: skupi zahtevi ka oracle-ima se izbegavaju tako što se virtuelni bilansi upisuju i inkrementalno ažuriraju.
  • Nedostaje reset kada supply == 0: remove_liquidity() proporcionalna smanjenja su ostavljala nenulte ostatke u packed_vbs[] nakon svakog ciklusa povlačenja.
  • Inicijalizaciona grana veruje kešu: add_liquidity() poziva _calc_vb_prod_sum() i jednostavno čita packed_vbs[] kada je prev_supply == 0, pretpostavljajući da je keš takođe nula.
  • Trovanje stanja finansirano flash-lonom: petlje depozit/povlačenje su pojačavale ostatke zaokruživanja bez zaključavanja kapitala, omogućavajući katastrofalno prekomerno mintovanje u putanji “first deposit”.

Dizajn keša i nedostatak rukovanja granicama

Ranjiv tok je pojednostavljen u nastavku:

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
}
}

Pošto remove_liquidity() primenjivao samo proporcionalna umanjenja, svaka iteracija je ostavljala ostatke zaokruživanja u fiksnoj tački. Posle ≳10 ciklusa depozita/isplate ti ostaci su se akumulirali u ekstremno velike fantomske virtualne bilanse dok su on-chain token bilansi bili skoro prazni. Spaljivanjem poslednjih LP tokena totalSupply je postao nula, ali su keševi ostali popunjeni, pripremajući protokol za neispravnu inicijalizaciju.

Plan eksploatacije (studija slučaja yETH)

  1. Flash-loan working capital – Pozajmite wstETH, rETH, cbETH, ETHx, WETH, itd. sa Balancer/Aave da ne vezujete kapital dok manipulišete pool-om.
  2. Poison packed_vbs[] – Ponavljajte depozite i isplate preko osam LSD asset-a. Svako parcijalno povlačenje flooruje/trimuje packed_vbs[i] − vb_share, ostavljajući >0 ostataka po tokenu. Ponavljanje petlje napuhuje fantomske ETH-ekvivalent bilanse bez podizanja sumnje jer se realni bilansi otprilike poništavaju.
  3. Force supply == 0 – Spalite sve preostale LP tokene tako da pool misli da je prazan. Greška u implementaciji ostavlja zatrovani packed_vbs[] netaknutim.
  4. Dust-size “first deposit” – Pošaljite ukupno 16 wei podeljenih preko podržanih LSD slotova. add_liquidity() vidi prev_supply == 0, pokreće _calc_vb_prod_sum(), i čita zastareli keš umesto da prerčuna iz stvarnih bilansa. Računica za minting zbog toga deluje kao da su ušli bilioni USD, emitujući ~2.35×10^26 yETH.
  5. Drain & repay – Iskoristite napuhanu LP poziciju za sve vaulted LSD, swapujte yETH→WETH na Balanceru, konvertujte u ETH preko Uniswap v3, vratite flash loanove/takse, i operite profit (npr. kroz Tornado Cash). Neto profit ≈USD 9M dok su samo 16 wei sopstvenih sredstava ikada dotakli pool.

Generalizovani uslovi za eksploataciju

Možete zloupotrebiti slične AMM-ove kada su ispunjeni svi sledeći uslovi:

  • Keširani izvodi balansa (virtual balances, TWAP snapshots, invariant helpers) opstaju između transakcija radi uštede gasa.
  • Delimična ažuriranja skraćuju rezultate (floor division, fixed-point rounding), omogućavajući napadaču da akumulira stanje-vezane ostatke putem simetričnih ciklusa depozit/isplata.
  • Granični uslovi ponovo koriste keševe umesto ponovnog izračunavanja iz stvarnih podataka, posebno kada totalSupply == 0, totalLiquidity == 0, ili se kompozicija pool-a resetuje.
  • Minting logika nema provere razumnosti odnosa (npr. odsustvo granica expected_value/actual_value), pa dust depozit može da mintuje praktično celu istorijsku ponudu.
  • Jeftin kapital je dostupan (flash loans ili interni kredit) za izvođenje desetina operacija menjanja stanja u jednoj transakciji ili u tesno orkestriranom bundlu.

Kontrolna lista za odbrambeni inženjering

  • Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}

Primijenite isto postupanje za svaki keširani akumulator izveden iz bilansa ili oracle podataka.

  • Recompute on initialization branches – Kada je prev_supply == 0, potpuno ignorišite keševe i obnovite virtual balances iz stvarnih token bilansa i aktuelnih oracle stopa.
  • Minting sanity bounds – Reverujte ako lpToMint > depositValue × MAX_INIT_RATIO ili ako jedna transakcija mintuje >X% istorijske ponude dok su ukupni depoziti ispod minimalnog praga.
  • Rounding-residue drains – Agregirajte per-token dust u sink (treasury/burn) kako ponovljena proporcionalna podešavanja ne bi odmakla keševe od stvarnih bilansa.
  • Differential tests – Za svaku tranziciju stanja (add/remove/swap), ponovo izračunajte isti invariant off-chain koristeći visokopreciznu matematiku i potvrdite jednakost unutar uskog epsilona čak i nakon potpunog pražnjenja likvidnosti.

Nadzor & odgovor

  • Multi-transaction detection – Pratite sekvence skoro-simetričnih deposit/withdraw događaja koji ostavljaju pool sa niskim balansima ali visokim keširanim stanjem, praćene supply == 0. Detektori anomalija koji gledaju samo pojedinačne transakcije propuštaju ove kampanje zatrovanja.
  • Runtime simulations – Pre izvođenja add_liquidity(), ponovo izračunajte virtual balances od nule i uporedite sa keširanim sumama; revert-ujte ili pauzirajte ako delte prelaze prag u basis-point-ima.
  • Flash-loan aware alerts – Označavajte transakcije koje kombinuju velike flash loans, iscrpna povlačenja iz pool-a i dust-veličine finalni depozit; blokirajte ili zahtevajte ručnu odobrenje.

References

Tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks