Σφάλματα λογιστικής σε DeFi AMM & Εκμετάλλευση Virtual Balance Cache

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

Επισκόπηση

Η πισίνα yETH του Yearn Finance (Νοέμ 2025) αποκάλυψε πώς οι gas-saving caches μέσα σε σύνθετα AMMs μπορούν να μετατραπούν σε όπλο όταν δεν εξομαλύνονται κατά τις μεταβάσεις οριακών καταστάσεων. Η weighted stableswap pool παρακολουθεί έως και 32 liquid staking derivatives (LSDs), τα μετατρέπει σε ETH-ισοδύναμα virtual balances (vb_i = balance_i × rate_i / PRECISION), και αποθηκεύει αυτές τις τιμές σε έναν packed storage πίνακα packed_vbs[]. Όταν όλα τα LP tokens καούν, το totalSupply μειώνεται σωστά σε μηδέν αλλά οι cached packed_vbs[i] θέσεις διατήρησαν τεράστιες ιστορικές τιμές. Ο επόμενος καταθέτης αντιμετωπίστηκε ως ο «πρώτος» liquidity provider παρόλο που η cache ακόμα κρατούσε φανταστική ρευστότητα, επιτρέποντας σε έναν επιτιθέμενο να mintάρει ~235 septillion yETH με μόλις 16 wei πριν αποστραγγίσει ≈USD 9M σε LSD collateral.

Κύρια συστατικά:

  • Derived-state caching: αποφεύγονται ακριβές oracle lookups με την επίμονη αποθήκευση virtual balances και την βαθμιαία ενημέρωσή τους.
  • Missing reset when supply == 0: τα proportional decrements στο remove_liquidity() άφησαν μη μηδενικά υπολείμματα στα packed_vbs[] μετά από κάθε κύκλο ανάληψης.
  • Initialization branch trusts the cache: το add_liquidity() καλεί _calc_vb_prod_sum() και απλώς διαβάζει τα packed_vbs[] όταν prev_supply == 0, υποθέτοντας ότι η cache έχει επίσης μηδενιστεί.
  • Flash-loan financed state poisoning: loops κατάθεσης/ανάληψης μεγέθυναν τα στρογγυλοποιητικά υπολείμματα χωρίς lockup κεφαλαίου, επιτρέποντας μια καταστροφική over-mint διαδρομή στο “first deposit” path.

Σχεδίαση cache & έλλειψη χειρισμού οριακών καταστάσεων

Η ευάλωτη ροή απλοποιείται παρακάτω:

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 – Δανειστείτε wstETH, rETH, cbETH, ETHx, WETH, κ.λπ. από Balancer/Aave για να αποφύγετε το δεσμευμένο κεφάλαιο ενώ χειρίζεστε την pool.
  2. Poison packed_vbs[] – Εκτελέστε επαναλαμβανόμενους κύκλους καταθέσεων και αναλήψεων σε οκτώ LSD assets. Κάθε μερική ανάληψη περικόπτει packed_vbs[i] − vb_share, αφήνοντας >0 υπολείμματα ανά token. Η επανάληψη του βρόχου διογκώνει φανταστικά υπόλοιπα ισοδύναμου ETH χωρίς να προκαλεί υποψία επειδή τα πραγματικά υπόλοιπα περίπου συμψηφίζονται.
  3. Force supply == 0 – Κάντε burn κάθε εναπομείναν LP token ώστε η pool να πιστέψει ότι είναι άδεια. Σφάλμα στην υλοποίηση αφήνει το μολυσμένο packed_vbs[] ανεπηρέαστο.
  4. Dust-size “first deposit” – Στείλτε συνολικά 16 wei κατανεμημένα στα υποστηριζόμενα LSD slots. Το add_liquidity() βλέπει prev_supply == 0, τρέχει _calc_vb_prod_sum() και διαβάζει την ξεπερασμένη cache αντί να αναπροσαρμόσει από τα πραγματικά υπόλοιπα. Επομένως ο υπολογισμός του mint συμπεριφέρεται σαν να εισήλθαν τρισεκατομμύρια USD, εκπέμποντας ~2.35×10^26 yETH.
  5. Drain & repay – Εξαγοράστε τη φουσκωμένη θέση LP για όλα τα vaulted LSDs, swap yETH→WETH στο Balancer, μετατρέψτε σε ETH μέσω Uniswap v3, αποπληρώστε flash loans/fees, και ξεπλύνετε το κέρδος (π.χ. μέσω Tornado Cash). Καθαρό κέρδος ≈USD 9M ενώ μόνο 16 wei ιδίων κεφαλαίων άγγιξαν ποτέ την pool.

Generalized exploitation conditions

You can abuse similar AMMs when all of the following hold:

  • Cached derivatives of balances (virtual balances, TWAP snapshots, invariant helpers) διατηρούνται μεταξύ συναλλαγών για εξοικονόμηση gas.
  • Partial updates truncate αποτελέσματα (floor division, fixed-point rounding), επιτρέποντας σε επιτιθέμενο να σωρεύσει υπολείμματα κατάστασης μέσω συμμετρικών κύκλων κατάθεσης/ανάληψης.
  • Boundary conditions reuse caches αντί να επαναυπολογίζουν από την ground-truth, ειδικά όταν totalSupply == 0, totalLiquidity == 0, ή όταν η σύνθεση της pool μηδενίζεται.
  • Minting logic lacks ratio sanity checks (π.χ. απουσία ορίων expected_value/actual_value) οπότε μια dust κατάθεση μπορεί να mint-άρει ουσιαστικά ολόκληρη την ιστορική προσφορά.
  • Cheap capital is available (flash loans ή internal credit) για την εκτέλεση δεκάδων λειτουργιών που τροποποιούν κατάσταση μέσα σε μία συναλλαγή ή σε ένα αυστηρά συγχρονισμένο 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;
}

Εφαρμόστε την ίδια μεταχείριση σε κάθε cached accumulator που προκύπτει από υπόλοιπα ή δεδομένα oracle.

  • Recompute on initialization branches – Όταν prev_supply == 0, αγνοήστε εντελώς τις caches και ανακατασκευάστε τα virtual balances από τα πραγματικά υπόλοιπα token + live oracle rates.
  • Minting sanity bounds – Κάνετε revert αν lpToMint > depositValue × MAX_INIT_RATIO ή αν μια μεμονωμένη συναλλαγή κάνει mint >X% της ιστορικής προσφοράς ενώ οι συνολικές καταθέσεις είναι κάτω από ένα ελάχιστο όριο.
  • Rounding-residue drains – Συγκεντρώστε το dust ανά token σε έναν sink (treasury/burn) ώστε οι επαναλαμβανόμενες αναλογικές προσαρμογές να μην εκτρέπουν τις caches από τα πραγματικά υπόλοιπα.
  • Differential tests – Για κάθε μετάβαση κατάστασης (add/remove/swap), επαναυπολογίστε το ίδιο invariant off-chain με αριθμητική υψηλής ακρίβειας και επιβεβαιώστε ισότητα εντός στενού epsilon ακόμα και μετά από πλήρη αποστράγγιση ρευστότητας.

Monitoring & response

  • Multi-transaction detection – Παρακολουθείστε ακολουθίες σχεδόν συμμετρικών γεγονότων deposit/withdraw που αφήνουν την pool με χαμηλά υπόλοιπα αλλά υψηλή cached κατάσταση, ακολουθούμενες από supply == 0. Οι ανιχνευτές ανωμαλιών μιας μίας συναλλαγής χάνουν αυτές τις εκστρατείες μόλυνσης.
  • Runtime simulations – Πριν εκτελέσετε το add_liquidity(), επαναυπολογίστε τα virtual balances από την αρχή και συγκρίνετέ τα με τα cached sums· κάντε revert ή παύση αν οι αποκλίσεις υπερβαίνουν ένα όριο basis-point.
  • Flash-loan aware alerts – Σημάνετε συναλλαγές που συνδυάζουν μεγάλα flash loans, εξαντλητικές αναλήψεις της pool, και ένα dust-sized τελικό deposit· μπλοκάρετε ή απαιτήστε χειροκίνητη έγκριση.

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