DeFi AMM ํšŒ๊ณ„ ๋ฒ„๊ทธ ๋ฐ ๊ฐ€์ƒ ์ž”์•ก ์บ์‹œ ์•…์šฉ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

๊ฐœ์š”

Yearn Finance์˜ yETH ํ’€(2025๋…„ 11์›”)์€ ๋ณต์žกํ•œ AMMs ๋‚ด๋ถ€์˜ ๊ฐ€์Šค ์ ˆ์•ฝ์šฉ ์บ์‹œ๊ฐ€ ๊ฒฝ๊ณ„ ์ƒํƒœ ์ „ํ™˜ ์ค‘์— ์ •์‚ฐ๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์–ด๋–ป๊ฒŒ ์•…์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ๋“œ๋Ÿฌ๋ƒˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ค‘ stableswap ํ’€์€ ์ตœ๋Œ€ 32๊ฐœ์˜ liquid staking derivatives (LSDs)๋ฅผ ์ถ”์ ํ•˜๊ณ , ์ด๋ฅผ ETH ๋™๋“ฑ๊ฐ’์ธ virtual balances (vb_i = balance_i ร— rate_i / PRECISION)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํŒจํ‚น๋œ ์Šคํ† ๋ฆฌ์ง€ ๋ฐฐ์—ด packed_vbs[]์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  LP ํ† ํฐ์ด ์†Œ๊ฐ๋˜๋ฉด totalSupply๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ 0์œผ๋กœ ๋–จ์–ด์ง€์ง€๋งŒ ์บ์‹œ๋œ packed_vbs[i] ์Šฌ๋กฏ๋“ค์€ ์—„์ฒญ๋‚œ ๊ณผ๊ฑฐ ๊ฐ’์„ ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ดํ›„์˜ ์˜ˆ๊ธˆ์ž๋Š” ์บ์‹œ๊ฐ€ ์œ ๋ น ์œ ๋™์„ฑ์„ ์—ฌ์ „ํžˆ ๋ณด์œ ํ•˜๊ณ  ์žˆ์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  โ€œ์ฒซ ๋ฒˆ์งธโ€ ์œ ๋™์„ฑ ๊ณต๊ธ‰์ž๋กœ ์ทจ๊ธ‰๋˜์—ˆ๊ณ , ๊ณต๊ฒฉ์ž๋Š” ์•ฝ 235 septillion yETH์„ ๋‹จ์ง€ 16 wei๋กœ ๋ฏผํŠธํ•œ ๋’ค โ‰ˆUSD 9M ์ƒ๋‹น์˜ LSD ๋‹ด๋ณด๋ฅผ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์š”์†Œ:

  • Derived-state caching: ๋น„์‹ผ ์˜ค๋ผํด ์กฐํšŒ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์ƒ ์ž”์•ก์„ ์ง€์† ์ €์žฅํ•˜๊ณ  ์ ์ง„์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
  • Missing reset when supply == 0: remove_liquidity()์˜ ๋น„๋ก€ ๊ฐ์†Œ๊ฐ€ ๊ฐ ์ถœ๊ธˆ ์‚ฌ์ดํด ํ›„ packed_vbs[]์— 0์ด ์•„๋‹Œ ์ž”์—ฌ๊ฐ’์„ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค.
  • Initialization branch trusts the cache: add_liquidity()๊ฐ€ _calc_vb_prod_sum()์„ ํ˜ธ์ถœํ•˜๊ณ  prev_supply == 0์ผ ๋•Œ ์บ์‹œ๋„ 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์—ฌ ๋‹จ์ˆœํžˆ packed_vbs[]๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.
  • Flash-loan financed state poisoning: ์˜ˆ์น˜/์ถœ๊ธˆ ๋ฃจํ”„๊ฐ€ ์ž๋ณธ ์ž ๊ธˆ ์—†์ด ๋ฐ˜์˜ฌ๋ฆผ ์ž”์—ฌ๋ฅผ ์ฆํญ์‹œ์ผœ โ€˜์ฒซ ์˜ˆ์น˜โ€™ ๊ฒฝ๋กœ์—์„œ ์น˜๋ช…์ ์ธ ๊ณผ๋‹ค ๋ฏผํŠธ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์บ์‹œ ์„ค๊ณ„ ๋ฐ ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ

์ทจ์•ฝํ•œ ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‹จ์ˆœํ™”๋ฉ๋‹ˆ๋‹ค:

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 โ€“ ํ’€์„ ์กฐ์ž‘ํ•˜๋Š” ๋™์•ˆ ์ž๋ณธ์„ ๋ฌถ์ง€ ์•Š๊ธฐ ์œ„ํ•ด Balancer/Aave์—์„œ wstETH, rETH, cbETH, ETHx, WETH ๋“ฑ์„ ์ฐจ์šฉํ•œ๋‹ค.
  2. Poison packed_vbs[] โ€“ 8๊ฐœ LSD ์ž์‚ฐ์„ ๋Œ€์ƒ์œผ๋กœ ์˜ˆ์น˜/์ธ์ถœ์„ ๋ฐ˜๋ณตํ•œ๋‹ค. ๊ฐ ๋ถ€๋ถ„ ์ธ์ถœ์€ packed_vbs[i] โˆ’ vb_share๋ฅผ truncatesํ•˜์—ฌ ํ† ํฐ๋‹น >0์˜ ์ž”์—ฌ๋ฌผ์„ ๋‚จ๊ธด๋‹ค. ๋ฐ˜๋ณต ๋ฃจํ”„๋Š” ์‹ค์ œ ์ž”์•ก์ด ๋Œ€์ฒด๋กœ ์ƒ์‡„๋˜๊ธฐ ๋•Œ๋ฌธ์— ์˜์‹ฌ์„ ์œ ๋ฐœํ•˜์ง€ ์•Š์œผ๋ฉด์„œ ํŒฌํ…€ ETH ์ƒ๋‹น ์ž”์•ก์„ ๋ถ€ํ’€๋ฆฐ๋‹ค.
  3. Force supply == 0 โ€“ ๋‚จ์•„์žˆ๋Š” ๋ชจ๋“  LP ํ† ํฐ์„ ์†Œ๊ฐํ•ด ํ’€์ด ๋น„์–ด์žˆ๋‹ค๊ณ  ์ธ์‹ํ•˜๊ฒŒ ํ•œ๋‹ค. ๊ตฌํ˜„ ์ƒ์˜ ๋ˆ„๋ฝ์œผ๋กœ ์ธํ•ด ์˜ค์—ผ๋œ packed_vbs[]๋Š” ๊ทธ๋Œ€๋กœ ๋‚จ๋Š”๋‹ค.
  4. Dust-size โ€œfirst depositโ€ โ€“ ์ง€์›๋˜๋Š” LSD ์Šฌ๋กฏ๋“ค์— ๊ฑธ์ณ ์ด 16 wei๋ฅผ ๋ถ„๋ฐฐํ•ด์„œ ๋ณด๋‚ธ๋‹ค. add_liquidity()๋Š” prev_supply == 0์„ ๊ฐ์ง€ํ•˜๊ณ  _calc_vb_prod_sum()์„ ์‹คํ–‰ํ•˜๋ฉฐ ์‹ค์ œ ์ž”์•ก์—์„œ ์žฌ๊ณ„์‚ฐํ•˜์ง€ ์•Š๊ณ  ์˜ค๋ž˜๋œ ์บ์‹œ๋ฅผ ์ฝ๋Š”๋‹ค. ๋”ฐ๋ผ์„œ ๋ฏผํŠธ ๊ณ„์‚ฐ์€ ์ˆ˜์กฐ ๋‹ฌ๋Ÿฌ๊ฐ€ ๋“ค์–ด์˜จ ๊ฒƒ์ฒ˜๋Ÿผ ์ž‘๋™ํ•ด ~2.35ร—10^26 yETH๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.
  5. Drain & repay โ€“ ๋ถ€ํ’€๋ ค์ง„ LP ํฌ์ง€์…˜์„ ๋ชจ๋“  ๋ณด๊ด€๋œ LSD๋กœ ์ƒํ™˜ํ•˜๊ณ , Balancer์—์„œ yETHโ†’WETH๋กœ ์Šค์™‘ํ•œ ๋’ค Uniswap v3๋ฅผ ํ†ตํ•ด ETH๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ํ”Œ๋ž˜์‹œ๋ก /์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ์ƒํ™˜ํ•œ ๋’ค ์ด์ต์„ ์„ธํƒ(์˜ˆ: Tornado Cash)ํ•œ๋‹ค. ์ˆœ์ด์ต์€ ์•ฝ USD 9M์ด๊ณ  ๋ณธ์ธ ์ž๊ธˆ์€ ๋‹จ์ง€ 16 wei๋งŒ ํ’€์ด ์ ‘์ด‰ํ–ˆ๋‹ค.

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

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ