DeFi AMM Błędy księgowe & Eksploatacja cache wirtualnych sald
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Przegląd
Yearn Finance’s yETH pool (Nov 2025) ujawnił, jak oszczędzające gaz cache’e wewnątrz złożonych AMM mogą zostać użyte jako broń, gdy nie są rekoncyliowane podczas przejść do stanów brzegowych. Ważony stableswap pool śledzi do 32 płynnych derywatów stakingowych (LSDs), konwertuje je do równowartości ETH jako wirtualne salda (vb_i = balance_i × rate_i / PRECISION) i przechowuje te wartości w spakowanej tablicy storage packed_vbs[]. Gdy wszystkie LP tokens są spalone, totalSupply poprawnie spada do zera, ale zbuforowane sloty packed_vbs[i] zachowywały ogromne historyczne wartości. Kolejny deponent był traktowany jako “pierwszy” dostawca płynności, mimo że cache nadal zawierał widmową płynność, co pozwoliło atakującemu wygenerować ~235 septylionów yETH za jedyne 16 wei przed drenowaniem ≈USD 9M w zabezpieczeniu LSD.
Kluczowe elementy:
- Derived-state caching: uniknięcie kosztownych zapytań do oracle przez trwałe przechowywanie wirtualnych sald i ich inkrementalną aktualizację.
- Missing reset when
supply == 0:remove_liquidity()wykonujące proporcjonalne dekrementy pozostawiało niezerowe resztki wpacked_vbs[]po każdym cyklu wypłat. - Initialization branch trusts the cache:
add_liquidity()wywołuje_calc_vb_prod_sum()i po prostu odczytujepacked_vbs[]kiedyprev_supply == 0, zakładając, że cache również został zresetowany do zera. - Flash-loan financed state poisoning: pętle depozyt/wypłata wzmacniały zaokrąglenia bez blokady kapitału, umożliwiając katastrofalne nadmintowanie na ścieżce “pierwszego depozytu”.
Projekt cache i brak obsługi warunków brzegowych
Wrażliwy przebieg został uproszczony poniżej:
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
}
}
Ponieważ remove_liquidity() stosowało tylko proporcjonalne dekrementy, każda iteracja zostawiała resztki zaokrągleń stałoprzecinkowych. Po ≳10 cyklach depozyt/wycofanie te pozostałości skumulowały się w bardzo duże fikcyjne wirtualne salda, podczas gdy on-chain salda tokenów były prawie puste. Spalenie ostatnich LP shares ustawiło totalSupply na zero, a cache pozostały wypełnione, przygotowując protokół do nieprawidłowej inicjalizacji.
Exploit playbook (studium przypadku yETH)
- Flash-loan working capital – Pożycz wstETH, rETH, cbETH, ETHx, WETH itp. z Balancer/Aave, żeby nie wiązać kapitału podczas manipulacji poolem.
- Poison
packed_vbs[]– Wykonuj pętle depozytów i wypłat w oparciu o osiem aktywów LSD. Każda częściowa wypłata obcinapacked_vbs[i] − vb_share, pozostawiając >0 resztki dla każdego tokena. Powtarzanie pętli nadmuchuje fikcyjne salda równoważne ETH bez wzbudzania podejrzeń, ponieważ rzeczywiste salda mniej więcej się kompensują. - Force
supply == 0– Spal wszystkie pozostałe LP tokeny, by pool wierzył, że jest pusty. Przeoczenie w implementacji pozostawia zatrutepacked_vbs[]nietknięte. - Dust-size “first deposit” – Wyślij łącznie 16 wei rozdzielone pomiędzy obsługiwane sloty LSD.
add_liquidity()widziprev_supply == 0, uruchamia_calc_vb_prod_sum()i odczytuje przestarzały cache zamiast przeliczyć wartości na podstawie faktycznych sald. Obliczenie mint działa więc, jakby wpłynęły tryliony USD, emitując ~2.35×10^26 yETH. - Drain & repay – Wykup nadmuchaną pozycję LP za wszystkie zmagazynowane LSD, zamień yETH→WETH na Balancer, skonwertuj na ETH przez Uniswap v3, spłać flash loans/opłaty i przepierz zysk (np. przez Tornado Cash). Zysk netto ≈ 9M USD przy tym, że do puli trafiło tylko 16 wei własnych środków.
Uogólnione warunki umożliwiające exploitację
Można nadużyć podobne AMM-y, gdy zachodzą wszystkie poniższe warunki:
- Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) utrzymują się między transakcjami dla oszczędności gazu.
- Partial updates truncate wyniki (dzielenie z obcięciem, zaokrąglanie stałopunktowe), pozwalając atakującemu na akumulację stanowych resztek przez symetryczne cykle depozyt/wypłata.
- Boundary conditions reuse caches zamiast przeliczać od podstaw, szczególnie gdy
totalSupply == 0,totalLiquidity == 0lub skład puli zostaje zresetowany. - Minting logic lacks ratio sanity checks (np. brak ograniczeń typu
expected_value/actual_value), więc pyłowy depozyt może wybić praktycznie całą historyczną podaż. - Cheap capital is available (flash loans lub wewnętrzny kredyt), by wykonać dziesiątki operacji zmieniających stan w jednej transakcji lub w ciasno zsynchronizowanym bundle.
Lista kontrolna obronnego inżynieringu
- Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}
Zastosuj to samo dla każdego cache’owanego akumulatora pochodzącego ze stanów sald lub danych oracle.
- Recompute on initialization branches – Gdy
prev_supply == 0, zignoruj cache i odbuduj virtual balances na podstawie faktycznych sald tokenów + aktualnych kursów z oracle. - Minting sanity bounds – Rzuć revert jeśli
lpToMint > depositValue × MAX_INIT_RATIOlub jeśli jedna transakcja bije >X% historycznej podaży, podczas gdy łączny depozyt jest poniżej minimalnego progu. - Rounding-residue drains – Agreguj per-tokenowy pył do sinku (treasury/burn), aby powtarzalne proporcjonalne korekty nie odrywały cache’ów od rzeczywistych sald.
- Differential tests – Dla każdej zmiany stanu (add/remove/swap) przelicz ten sam invariant off-chain z wysoką precyzją i sprawdź równość w wąskim epsilonie nawet po całkowitych drenażach płynności.
Monitoring & response
- Multi-transaction detection – Śledź sekwencje niemal symetrycznych zdarzeń depozyt/wypłata, które pozostawiają pulę z niskimi saldami, ale wysokim cache’em stanu, a następnie
supply == 0. Detektory jednorazowych transakcji przegapiają takie kampanie zatruwania. - Runtime simulations – Przed wykonaniem
add_liquidity()przelicz virtual balances od zera i porównaj z sumami w cache; revertuj lub wstrzymaj, jeśli delty przekraczają threshold rzędu kilku punktów bazowych. - Flash-loan aware alerts – Oznacz transakcje łączące duże flash loans, wyczerpujące wypłaty puli i pyłowy finalny depozyt; zablokuj albo wymagać ręcznej akceptacji.
References
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
HackTricks

