DeFi AMM Accounting Bugs & Virtual Balance Cache Exploitation

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Genel Bakış

Yearn Finance’ın yETH havuzu (Kasım 2025), karmaşık AMM’lerde gaz tasarrufu amaçlı önbelleklerin sınır-durumu geçişleri sırasında uzlaştırılmadığında nasıl silah haline getirilebileceğini gösterdi. Ağırlıklı stableswap havuzu 32’ye kadar liquid staking derivative (LSD) izliyor, bunları ETH-eşdeğerine çeviriyor — virtual balances (vb_i = balance_i × rate_i / PRECISION) — ve bu değerleri packed_vbs[] adlı paketlenmiş bir storage dizisinde saklıyordu. Tüm LP token’ları yakıldığında totalSupply doğru şekilde sıfıra düşse de cached packed_vbs[i] slotları büyük tarihsel değerleri tutmaya devam etti. Ardından gelen depositor, önbellek hâlâ hayalet likidite içeriyor olmasına rağmen “ilk” liquidity provider olarak kabul edildi; bu da bir saldırganın sadece 16 wei karşılığında ≈235 septilyon yETH mint etmesine ve ≈USD 9M değerindeki LSD teminatını boşaltmasına izin verdi.

Temel bileşenler:

  • Derived-state caching: pahalı oracle sorgularından kaçınmak için virtual balances saklanır ve artımlı olarak güncellenir.
  • Missing reset when supply == 0: remove_liquidity() orantılı azalmalar sonrası packed_vbs[] içinde sıfır olmayan kalıntılar bıraktı.
  • Initialization branch trusts the cache: add_liquidity() _calc_vb_prod_sum() çağırır ve prev_supply == 0 olduğunda packed_vbs[]’i sadece okur, önbelleğin de sıfırlandığını varsayar.
  • Flash-loan financed state poisoning: deposit/withdraw döngüleri yuvarlama kalıntılarını sermaye kilidi olmadan büyütüp “first deposit” yolunda yıkıcı bir over-mint’e olanak sağladı.

Önbellek tasarımı ve eksik sınır durumu işleme

Zayıf akış aşağıda basitleştirilmiştir:

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 sabit nokta yuvarlama artıkları. 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.

Sömürü oyun kitabı (yETH vaka çalışması)

  1. Flash-loan working capital – Balancer/Aave’ten wstETH, rETH, cbETH, ETHx, WETH vb. ödünç alarak havuzu manipüle ederken sermayeyi bağlamaktan kaçının.
  2. Poison packed_vbs[] – Sekiz LSD varlığı arasında deposit/withdraw döngüleri yapın. Her kısmi çekim packed_vbs[i] − vb_share değerini kırparak token başına >0 artık bırakır. Döngüyü tekrarlamak, gerçek bakiyeler kabaca dengelenirken hayalet ETH-eşdeğer bakiyeleri şişirir.
  3. Force supply == 0 – Havuzun boş olduğuna inanması için kalan tüm LP token’ları yakın. Uygulamadaki göz ardı edilen hata zehirlenmiş packed_vbs[]’i dokunulmadan bırakır.
  4. Dust-size “first deposit” – Desteklenen LSD slotlarına bölünmüş toplam 16 wei gönderin. add_liquidity() prev_supply == 0 olduğunu görür, _calc_vb_prod_sum() çalıştırılır ve gerçek bakiyelerden yeniden hesaplamak yerine bayat cache’i okur. Bu yüzden mint hesabı trilyonlarca USD girmiş gibi davranır ve ~2.35×10^26 yETH basar.
  5. Drain & repay – Şişirilmiş LP pozisyonunu tüm vault’lanmış LSD’ler için itfa edin, yETH→WETH’i Balancer’da takas edin, Uniswap v3 ile ETH’ye çevirin, flash loan/ücretleri geri ödeyin ve kârı aklayın (ör. Tornado Cash aracılığıyla). Net kâr ≈USD 9M; havuza temas eden kendi fon yalnızca 16 wei idi.

Genelleştirilmiş sömürü koşulları

Benzer AMM’leri aşağıdaki koşulların tümü sağlandığında suistimal edebilirsiniz:

  • Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) gaz tasarrufu için işlemler arasında kalıcı tutuluyor.
  • Partial updates truncate sonuçları (floor division, fixed-point rounding), saldırganın simetrik deposit/withdraw döngüleriyle durumlu artıkları biriktirmesine izin veriyor.
  • Boundary conditions reuse caches yerine gerçek yeniden hesaplama yapılmıyor; özellikle totalSupply == 0, totalLiquidity == 0 veya havuz bileşimi sıfırlandığında.
  • Minting logic lacks ratio sanity checks (ör. expected_value/actual_value sınırlarının olmaması), böylece bir toz depozit neredeyse tüm tarihsel arzı basabiliyor.
  • Cheap capital is available (flash loans veya iç kredi) tek bir işlem içinde veya sıkı koordine edilmiş paketlerle düzinelerce durum-düzenleyici işlemi çalıştırmaya olanak veriyor.

Savunma mühendisliği kontrol listesi

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

Bakiyelerden veya oracle verilerinden türetilmiş her önbelleğe alınmış birikiciye aynı muameleyi uygulayın.

  • Recompute on initialization branchesprev_supply == 0 durumunda cache’leri tamamen yoksayın ve virtual balances’ı gerçek token bakiyeleri + canlı oracle oranlarından yeniden inşa edin.
  • Minting sanity boundslpToMint > depositValue × MAX_INIT_RATIO ise revert edin veya tek bir işlemde tarihsel arzın >X% basılmasına izin vermeyin; toplam depozitler minimum eşik altındaysa işlemi reddedin.
  • Rounding-residue drains – Token başına yuvarlama artığını bir sink’e (treasury/burn) toplayın, böylece tekrarlanan orantılı ayarlamalar cache’leri gerçek bakiyelerden uzaklaştırmaz.
  • Differential tests – Her durum geçişi (add/remove/swap) için aynı invariant’ı off-chain, yüksek hassasiyetli matematikle yeniden hesaplayın ve tam likidite boşalmalarından sonra bile sıkı bir epsilon içinde eşitliği doğrulayın.

İzleme ve müdahale

  • Multi-transaction detection – Havuzu düşük bakiye ama yüksek cache durumuyla bırakan ve ardından supply == 0 ile sonuçlanan neredeyse simetrik deposit/withdraw dizilerini izleyin. Tek işlemli anomali tespitçileri bu zehirleme kampanyalarını gözden kaçırır.
  • Runtime simulationsadd_liquidity() çalıştırılmadan önce virtual balances’ı sıfırdan yeniden hesaplayın ve cached toplamlarla karşılaştırın; farklar baz puan eşiğini aşarsa revert veya pause uygulayın.
  • Flash-loan aware alerts – Büyük flash loan’lar, kapsamlı havuz çekimleri ve toz büyüklüğünde bir son depoziti birleştiren işlemleri işaretleyin; bunları engelleyin veya manuel onay gerektirin.

References

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin