Bentornati al Vulnerability Spotlight di Sherlock, dove evidenziamo una vulnerabilità significativa scoperta durante un audit di Sherlock. Questa settimana, esaminiamo un calcolo errato del livello di collaterale trovato nel contest di @plaza_finance da @0xadrii, @KupiaSecurity, @f, @farman1094_, e @0xnovaman33.
Riepilogo della vulnerabilità: In getRedeemAmount(...), il contratto calcola il livello di collaterale per i riscatti di BOND utilizzando lo stato post-trade: // codice attuale (vulnerabile) collateralLevel = ((tvl - (depositAmount * BOND_TARGET_PRICE)) * PRECISION) / ((bondSupply - depositAmount) * BOND_TARGET_PRICE); Poiché il prezzo di riscatto utilizza quindi questo collateralLevel stimato per decidere se pagare il prezzo massimo ($100), un attaccante può acquistare BOND a tassi di creazione misti (a volte scontati) fino a quando il livello di collaterale (attuale) scende ≤ 1.2, e poi riscattare tutti i BOND al limite di $100—calcolando con un depositAmount creato che spinge il livello di collaterale stimato di nuovo sopra 1.2. Questo consente all'attaccante di estrarre ETH (spread "senza rischio") fino a quando il livello di collaterale attuale del pool non raggiunge la soglia.
Passaggi dell'attacco: 1) Fase di configurazione Esempio di parametri del pool: - poolReserve = 120 ETH, bondSupply = 3000, levSupply = 200, prezzo ETH = $3075 Regole di pricing: Creare (comprare) BOND: - Se collateralLevel ≤ 1.2: creationRate = tvl * 0.8 / bondSupply - Altrimenti: creationRate = $100 Riscattare (vendere) BOND: - Se estimated collateralLevel ≤ 1.2: redeemRate = tvl * 0.8 / bondSupply - Altrimenti: redeemRate = $100
2) Fase A – Acquista BOND a tassi misti/scontati - Acquista BOND monitorando (l'attuale) livello di collateral = tvl / (fornitura di bond * 100). - Il primo acquisto avviene quando il livello di collateral > 1.2 → conia vicino a $100 per BOND. - Gli acquisti successivi portano il livello di collateral attuale sotto 1.2 → conia a un tasso di creazione scontato (ad es., ~$94.07 in), accumulando un grande saldo di BOND a basso costo.
3) Fase B – Riscatta tutti i BOND al prezzo massimo - Chiama redeem(BOND, depositAmount = attackerBondBalance, ...) - Il contratto calcola il livello di collaterale stimato utilizzando lo stato post-riscatto (bondSupply - depositAmount) e (tvl - depositAmount * 100) e (nell'esempio) ottiene un valore > 1.2 - Poiché quel valore stimato supera la soglia, il redeemRate è impostato al massimo di $100, consentendo all'attaccante di incassare tutti i BOND accumulati a $100
4) Realizzazione del Profitto - La differenza tra acquisti scontati e vendite da $100 genera un profitto netto in ETH. - Nei numeri del PoC: spendere 60 ETH in due acquisti restituisce ~61.89 ETH al momento del riscatto → ~1.89 ETH di profitto. - L'attaccante può iterare fino a quando il livello di collaterale attuale del pool scende a ~1.2, estraendo approssimativamente: USD estraibili ≈ ethPrice × poolReserve − 120 × bondSupply
Qual è l'impatto? Estrazione diretta di fondi / perdita di valore: L'attaccante conia a tariffe scontate e riscatta a $100, drenando ETH dal pool. Illimitato fino al limite: Può continuare fino a quando il livello attuale di collaterale del pool non si riduce a ≈ 1.2. Incoerenza sistemica dei prezzi: Crea arbitraggio che gli utenti onesti pagano con riserve/esiti peggiori del pool.
La causa principale: Utilizzo dello stato post-trade per la valutazione dei rimborsi Il codice calcola collateralLevel come se il rimborso fosse già avvenuto: // utilizza (tvl - depositAmount*100) e (bondSupply - depositAmount) collateralLevel = ((tvl - (depositAmount * BOND_TARGET_PRICE)) * PRECISION) / ((bondSupply - depositAmount) * BOND_TARGET_PRICE); 1. Questo consente a un attaccante di scegliere depositAmount in modo che il livello stimato superi la soglia (> 1.2), sbloccando il $100 redeemRate, anche quando lo stato attuale del pool non lo giustificherebbe. 2. Soglia di gating su una metrica manipolata La decisione sul limite di $100 dipende da questo livello di collateral stimato manipolabile piuttosto che dal livello attuale (pre-trade), consentendo il gioco sui prezzi.
La Mitigazione: Calcola il prezzo di riscatto dallo stato attuale del pool, non dai saldi stimati post-riscatto. La soluzione suggerita dal progetto è corretta: - collateralLevel = ((tvl - (depositAmount * BOND_TARGET_PRICE)) * PRECISION) - / ((bondSupply - depositAmount) * BOND_TARGET_PRICE); + collateralLevel = (tvl * PRECISION) / (bondSupply * BOND_TARGET_PRICE);
Indurimento aggiuntivo (raccomandato): 1. Monotonicità del prezzo: Assicurati che il prezzo di riscatto non aumenti all'aumentare dell'importo del deposito (niente "vendi di più, ottieni un prezzo unitario migliore"). 2. Prezzi basati su invarianti: Deriva la creazione/riscatto da un singolo invariante in modo che la simmetria acquisto/vendita prevenga l'arbitraggio unilaterale. 3. Controlli di slippage: Richiedi tassi min/max forniti dall'utente sia per la creazione che per il riscatto. 4. Limiti e throttling: Limiti di riscatto per transazione e per blocco per limitare i danni se si avvicinano le soglie. 5. Coerenza tra i percorsi: Allinea i percorsi di creazione e riscatto per utilizzare la stessa definizione del livello di garanzia (solo stato attuale).
Siamo orgogliosi di aver contribuito a garantire la sicurezza di @plaza_finance attraverso questa scoperta. Quando deve essere assolutamente sicuro, Sherlock è la scelta giusta.
3,28K