Баги обліку DeFi AMM та експлуатація кешу віртуальних балансів
Tip
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Огляд
Пул yETH від Yearn Finance (Nov 2025) показав, як кеші для економії газу всередині складних AMM можна використати як зброю, якщо вони не узгоджуються під час переходів через граничні стани. Зважений stableswap-пул відстежує до 32 liquid staking derivatives (LSDs), конвертує їх у еквівалент ETH — virtual balances (vb_i = balance_i × rate_i / PRECISION) — і зберігає ці значення в пакованому масиві сховища packed_vbs[]. Коли всі LP токени були знищені, totalSupply коректно падає до нуля, але кешовані слоти packed_vbs[i] зберегли величезні історичні значення. Наступний депонент вважався «першим» постачальником ліквідності, хоча кеш усе ще містив фантомну ліквідність, що дозволило атакуючому надрукувати ~235 септильйонів yETH лише за 16 wei перед тим, як витягнути ≈USD 9M у LSD-колатералі.
Ключові складові:
- Derived-state caching: дорогі виклики оркулів уникаються шляхом збереження virtual balances і їх поетапного оновлення.
- Missing reset when
supply == 0: пропорційні зменшення вremove_liquidity()залишали ненульові залишки вpacked_vbs[]після кожного циклу зняття. - Initialization branch trusts the cache:
add_liquidity()викликає_calc_vb_prod_sum()і просто читаєpacked_vbs[], колиprev_supply == 0, припускаючи, що кеш також обнулено. - Flash-loan financed state poisoning: цикли депозитів/зняттів підсилювали залишки через округлення без блокування капіталу, що дозволило катастрофічне over-mint у шляху «першого депозиту».
Дизайн кешу та відсутня обробка граничних умов
Нижче спрощено вразливий потік:
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 рештки округлення у фіксованій комі. Після ≳10 циклів депозит/виведення ці залишки накопичувалися у дуже великі фантомні віртуальні баланси, тоді як ончейн-баланси токенів були майже порожні. Спалення останніх LP-шарів встановлювало totalSupply у нуль, але кеші залишалися заповненими, готуючи протокол до некоректної ініціалізації.
Exploit playbook (yETH case study)
- Flash-loan working capital – Позичте wstETH, rETH, cbETH, ETHx, WETH тощо з Balancer/Aave, щоб не зв’язувати власний капітал під час маніпуляцій пулом.
- Poison
packed_vbs[]– Робіть циклічні депозити й зняття по восьми LSD-активах. Кожне часткове зняття обрізаєpacked_vbs[i] − vb_share, залишаючи >0 залишків для кожного токена. Повторення циклу надуває фантомні ETH-еквівалентні баланси, не викликаючи підозри, оскільки реальні баланси приблизно компенсують один одного. - Force
supply == 0– Спаліть усі залишкові LP-токени, щоб пул вважав себе порожнім. Помилка реалізації лишає отруєнийpacked_vbs[]нетронутим. - Dust-size “first deposit” – Надішліть загалом 16 wei, розподілених по підтримуваних LSD-слотах.
add_liquidity()бачитьprev_supply == 0, викликає_calc_vb_prod_sum()і читає застарілий кеш замість того, щоб перерахувати з фактичних балансів. Отже розрахунок mint поводиться так, ніби вхідні кошти — трильйони USD, емитуючи приблизно ~2.35×10^26 yETH. - Drain & repay – Викупіть надміру згенерований LP-позицію за всі забанкувані LSD, поміняйте yETH→WETH на Balancer, конвертуйте в ETH через Uniswap v3, поверніть flash loans/комісії і відмийте прибуток (наприклад, через Tornado Cash). Чистий прибуток ≈USD 9M при тому, що в пул торкнулося лише 16 wei власних коштів.
Generalized exploitation conditions
Подібні AMM можна зловживати, коли одночасно виконуються всі наступні умови:
- Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) зберігаються між транзакціями задля економії газу.
- Partial updates truncate результати (floor division, fixed-point rounding), що дозволяє атакуючому накопичувати станні залишки через симетричні цикли депозит/виведення.
- Boundary conditions reuse caches замість перевирахування за істинними даними, особливо коли
totalSupply == 0,totalLiquidity == 0або склад пулу скидається. - Minting logic lacks ratio sanity checks (наприклад, відсутність меж
expected_value/actual_value), тож пиловий депозит може змінтити фактично весь історичний supply. - Cheap capital is available (flash loans або внутрішній кредит) для запуску десятків операцій, що змінюють стан, в межах однієї транзакції або тісно скоординованого бандлу.
Defensive engineering checklist
- Explicit resets when supply/lpShares hit zero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}
Застосуйте таке ж скидання до кожного кешованого акумулятора, похідного від балансів або оракульних даних.
- Recompute on initialization branches – Коли
prev_supply == 0, ігноруйте кеші повністю і відновлюйте віртуальні баланси з фактичних токен-балансів + живих оракульних курсів. - Minting sanity bounds – Ревертіть, якщо
lpToMint > depositValue × MAX_INIT_RATIOабо якщо одна транзакція емитує >X% історичного supply при загальних депозитах нижче мінімального порогу. - Rounding-residue drains – Агрегуйте по-токенові залишки в sink (treasury/burn), щоби повторні пропорційні корекції не відводили кеші від реальних балансів.
- Differential tests – Для кожного переходу стану (add/remove/swap) перераховуйте той самий інваріант оффчейн з високою точністю і перевіряйте рівність у межах жорсткого епсилон навіть після повного зливу ліквідності.
Monitoring & response
- Multi-transaction detection – Відстежуйте послідовності майже симетричних подій депозит/виведення, які лишають пул з низькими балансами, але завищеним кешованим станом, з наступним
supply == 0. Детектори аномалій, що працюють на одну транзакцію, пропускають такі кампанії отруєння. - Runtime simulations – Перед виконанням
add_liquidity()перераховуйте віртуальні баланси з нуля і порівнюйте з кешованими сумами; ревертіть або призупиніть, якщо дельти перевищують поріг у базисних пунктах. - Flash-loan aware alerts – Позначайте транзакції, що поєднують великі flash loans, тотальні зняття пулу та пиловий фінальний депозит; блоковуйте або вимагайте ручного погодження.
References
Tip
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.


