Gli errori aritmetici nei smart contract Solidity si verificano quando i calcoli di token, quote o prezzi utilizzano la formula sbagliata, la scala decimale errata, la direzione di arrotondamento inadeguata o un’ipotesi numerica errata, permettendo agli utenti di creare diritti in eccesso, prelevare più valore di quello depositato o causare il mancato rispetto degli invarianti contabili. La soluzione consiste nel definire esplicitamente le unità, utilizzare un’aritmetica limitata a precisione intera completa e testare gli invarianti economici che la formula deve rispettare.
Le conseguenze sono rilevanti. Cetus CLMM ha perso 223,0 milioni di dollari su Sui il 22-05-2025 in un incidente classificato come Errore Aritmetico che coinvolge contabilità di token e quote. La sicurezza aritmetica quindi non riguarda solo la prevenzione dei revert di overflow in Solidity, ma la garanzia che ogni quota emessa rappresenti un diritto difendibile sugli asset.
Cosa sono gli errori aritmetici in Solidity e come funzionano gli attacchi?
Gli errori aritmetici in Solidity sono difetti nei calcoli che traducono depositi, prelievi, saldi, prezzi, commissioni o liquidità in diritti economici. Un attaccante sfrutta la discrepanza tra il modello contabile previsto e la formula implementata, spesso scegliendo l’ordine delle transazioni, quantità di asset o stati di pool che rendono favorevole una conversione errata.
Nella Solidity moderna, overflow e underflow aritmetici fanno revert di default. Questa protezione è utile, ma non dimostra che una formula sia corretta economicamente. Un calcolo può restare entro i limiti di uint256 pur generando una quantità eccessiva di quote nel vault, caricando un collaterale insufficiente o arrotondando un prelievo a vantaggio dell’utente.
Un tipico attacco di quote in eccesso segue questa sequenza:
- Un vault ha asset e quote esistenti, quindi il prezzo per quota non è unitario.
- Il vault riceve rendimento, commissioni, donazioni o un aumento del saldo dovuto a rebasing.
- Il prezzo delle quote aumenta perché gli asset totali aumentano mentre le quote totali no.
- Un attaccante deposita asset usando una formula che assume che una quota valga sempre un asset.
- Il vault emette più quote di quelle che il deposito dovrebbe comprare al prezzo attuale.
- L’attaccante riscatta quelle quote per una frazione sproporzionata degli asset nel pool.
L’invariante fondamentale è semplice: dopo un deposito, la frazione di proprietà del nuovo utente deve corrispondere al valore che ha contribuito. Se un deposito pari all’1% degli asset del pool produce più del circa 1% delle quote in circolazione, i possessori esistenti vengono diluiti.
Quali pattern in Solidity causano errori di contabilità su token e quote?
Gli errori di contabilità su token e quote derivano solitamente dalla miscelazione di unità, dalla troncatura di valori intermedi o dal considerare il prezzo di una quota variabile come fisso. Le funzioni più a rischio sono deposit, mint, withdraw, redeem, limiti di prestito, regolamento delle commissioni, calcolo della liquidità e distribuzione delle ricompense.
Un pattern pericoloso è emettere quote direttamente dalle unità di asset depositate:
shares = assets * 1e18 / assetScale;
Quella conversione può essere appropriata solo per il primo deposito, quando una quota è definita deliberatamente come un’unità di asset normalizzata. Una volta che un vault guadagna rendimento o subisce perdite, i depositi devono usare il tasso di cambio corrente:
shares = assets * totalShares / totalAssets;
Anche questa espressione richiede attenzione. Moltiplicare prima può causare overflow se posto dentro unchecked, mentre dividere prima può far perdere precisione. La normalizzazione decimale introduce un’altra fonte di errore. Asset in stile USDC usano generalmente meno decimali di un token quota a 18 decimali e un protocollo deve distinguere costantemente le unità raw dei token dalle unità contabili normalizzate.
Cause comuni includono:
- Hard-coding di
1e18per un asset con precisione decimale diversa. - Minting di quote a un tasso fisso uno a uno dopo che il prezzo della quota è cambiato.
- Divisione eseguita prima della moltiplicazione con perdita silenziosa di precisione significativa.
- Arrotondamento verso l’alto di depositi o prelievi, causando perdite cumulative di valore.
- Uso di
uncheckedattorno a valori controllati dall’utente o fattori di scala. - Assunzione che i saldi dei token non possano cambiare al di fuori dei normali flussi di deposito e prelievo.
- Dimenticanza che token con fee on transfer, rebasing e yield possono invalidare la contabilità memorizzata.
Come si correggono gli errori aritmetici nei calcoli delle quote in Solidity?
Per correggere gli errori aritmetici in Solidity, normalizza le unità degli asset una volta, calcola le quote rispetto al rapporto attuale del pool, usa moltiplicazione e divisione a precisione intera completa e scegli un arrotondamento che non trasferisca valore dai detentori esistenti a un nuovo depositante. Il contratto deve inoltre trattare il suo saldo asset e la sua offerta di quote come stato contabile collegato.
Il vault vulnerabile qui sotto accetta un token con decimali configurabili. Emette quote normalizzate a un tasso fisso anche dopo che reportYield aumenta gli asset. Un depositante può ricevere troppe quote dopo che si è accumulato rendimento, quindi riscattarle contro il pool più grande.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
}
contract VulnerableShareVault {
IERC20 public immutable asset;
uint8 public immutable assetDecimals;
uint256 public totalAssets;
uint256 public totalShares;
mapping(address => uint256) public shares;
constructor(address asset_, uint8 decimals_) {
asset = IERC20(asset_);
assetDecimals = decimals_;
}
function deposit(uint256 assets) external {
require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
uint256 scale = 10 ** assetDecimals;
// VULNERABILE: ignora il prezzo attuale delle quote esistenti.
uint256 minted = (assets * 1e18) / scale;
totalAssets += assets;
totalShares += minted;
shares[msg.sender] += minted;
}
function reportYield(uint256 assets) external {
require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
totalAssets += assets;
}
function withdraw(uint256 shareAmount) external {
uint256 assets = (shareAmount * totalAssets) / totalShares;
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
totalAssets -= assets;
require(asset.transfer(msg.sender, assets), "transfer failed");
}
}
La versione corretta converte le unità raw degli asset in unità contabili a 18 decimali ed emette quote al tasso di cambio corrente. Usa Math.mulDiv di OpenZeppelin per moltiplicazioni e divisioni a precisione completa, arrotonda per difetto sia depositi che prelievi e limita i valori decimali supportati.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
}
contract FixedShareVault {
IERC20 public immutable asset;
uint256 public immutable assetScale;
uint256 public totalAssets;
uint256 public totalShares;
mapping(address => uint256) public shares;
constructor(address asset_, uint8 decimals_) {
require(decimals_ <= 18, "unsupported decimals");
asset = IERC20(asset_);
assetScale = 10 ** decimals_;
}
function deposit(uint256 assets) external {
require(assets > 0, "zero assets");
require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
uint256 normalizedAssets = Math.mulDiv(assets, 1e18, assetScale);
uint256 normalizedPool = Math.mulDiv(totalAssets, 1e18, assetScale);
// CORRETTO: prezza le quote rispetto al rapporto del pool prima del deposito.
uint256 minted = totalShares == 0
? normalizedAssets
: Math.mulDiv(normalizedAssets, totalShares, normalizedPool);
require(minted > 0, "deposit too small");
totalAssets += assets;
totalShares += minted;
shares[msg.sender] += minted;
}
function withdraw(uint256 shareAmount) external {
uint256 assets = Math.mulDiv(shareAmount, totalAssets, totalShares);
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
totalAssets -= assets;
require(asset.transfer(msg.sender, assets), "transfer failed");
}
}
Se il tuo protocollo prezza depositi, prelievi, collaterali o liquidità usando aritmetica personalizzata, un audit e valutazione della sicurezza smart-contract può tracciare conversioni di unità e invarianti economici lungo tutto il ciclo di vita della transazione.
Come possono gli sviluppatori evitare errori aritmetici?
Gli sviluppatori smettono di fare errori aritmetici specificando le unità contabili prima di implementare le formule, applicando un arrotondamento conservativo e trattando ogni conversione come un confine di sicurezza. Una formula dovrebbe indicare cosa rappresenta ogni input, la sua precisione decimale, l’intervallo consentito e chi beneficia dell’arrotondamento.
| Tecnica | Cosa impedisce | Costo in gas o complessità | Limitazione |
|---|---|---|---|
| Normalizzazione esplicita delle unità | Miscelazione di unità raw del token con unità quote a 18 decimali | Moltiplicazioni o divisioni extra | Richiede una politica documentata di decimali |
Math.mulDiv |
Overflow e perdita di precisione in a * b / c |
Più gas rispetto operatori semplici | Non convalida la formula economica |
| Arrotondamento per difetto di depositi e prelievi | Emissione eccessiva di quote e riscatti eccessivi a causa di arrotondamenti | Può creare saldi di dust | Serve una politica di recupero del dust |
| Soglia minima per minting quote | Depositi che arrotondano a zero quote | Una sola comparazione | Non deve bloccare utenti legittimi piccoli inaspettatamente |
| Input e decimali limitati | Esponenziazioni non sicure e input estremi degli utenti | Controlli di validazione | I limiti devono corrispondere agli asset supportati |
| Test sugli invarianti | Transizioni di stato che violano la contabilità del pool | Sforzo nella scrittura dei test | Invarianti devono coprire sequenze avversarie |
Usa l’arrotondamento per difetto dove l’alternativa potrebbe assegnare valore che il pool non può supportare. Ad esempio, un vault normalmente arrotonda verso il basso le quote depositate e gli asset riscattati. Se un prodotto richiede arrotondamento a favore dell’utente, dovrebbe finanziare esplicitamente il sussidio risultante anziché imporlo silenziosamente agli altri depositanti.
Insight sulla sicurezza: Secondo la metodologia Soken, la revisione aritmetica parte dalle unità e dalle leggi di conservazione, non dagli operatori. Una moltiplicazione controllata può essere comunque sfruttabile se il risultato rappresenta l’asset, la quota, il prezzo o il diritto di proprietà errato.
Quali incidenti reali mostrano l’impatto degli errori aritmetici?
Gli incidenti di Errore Aritmetico dimostrano che la contabilità di token e quote è una proprietà di sicurezza a livello di protocollo e non solo una questione locale di qualità del codice. La logica coinvolta può essere nelle riserve AMM, nelle ricompense di prestito, nelle quote di vault, nei calcoli di liquidità o nelle routine di conversione asset.
| Protocollo | Data | Importo perso | Chain |
|---|---|---|---|
| Cetus CLMM | 2025-05-22 | $223.0M | Sui |
| Compound V2 | 2021-09-29 | $147.0M | Ethereum |
| Uranium Finance | 2021-04-28 | $57.2M | BSC |
| ValueDefi | 2021-05-07 | $11.0M | BSC |
| KiiChain | 2026-08-22 | $9.7M | KiiChain |
| Truebit | 2026-01-08 | $26.4M | Ethereum |
| Notional V2 | 2026-09-04 | $1.7M | Ethereum |
| USM | 2026-08-09 | $136K | Ethereum |
Questi incidenti condividono una caratteristica critica: un calcolo governava la distribuzione o la valutazione del valore on-chain. Il fallimento non è stato necessariamente un’addizione uint256 non controllata. Nella contabilità di token e quote, la condizione vulnerabile è spesso una relazione errata tra riserve, offerta, prezzo e diritti degli utenti.
DefiLlama registra 35 incidenti smart-contract di Errore Aritmetico dal 2020 al 2026, con perdite combinate di 561,0 milioni di dollari. Per i team di protocollo, questa storia supporta la revisione dei percorsi contabili con la stessa priorità del controllo degli accessi e della gestione delle chiamate esterne.
Come si rilevano gli errori aritmetici prima del deploy?
Gli errori aritmetici si rilevano meglio rivedendo le transizioni di stato e testando le proprietà economiche su molte sequenze, non controllando manualmente formule isolate. L’analisi statica può identificare operatori rischiosi e blocchi unchecked, mentre fuzzing e test sugli invarianti stabiliscono se il modello contabile regge sotto depositi, prelievi, rendimento, perdite e valori decimali insoliti.
Usa questa checklist mirata:
- Elenca ogni variabile di asset, quota, prezzo, tasso e commissione con la sua unità e decimali.
- Verifica che ogni deposito usi il tasso di cambio pre-deposito.
- Verifica che ogni prelievo usi un tasso coerente con le quote bruciate.
- Cerca
*,/,uncheckede costanti di scala hardcoded nelle vie contabili. - Testa asset con precisioni decimali diverse e depositi prossimi alle dimensioni minime.
- Conferma che gli arrotondamenti non generino quote non supportate o rilascino asset non supportati.
- Modella i cambi di saldo dovuti a rebasing, donazioni, commissioni e rendimento prima di ogni azione utente.
- Controlla che i diritti totali degli utenti non superino gli asset disponibili per il riscatto.
Slither può segnalare pattern aritmetici pericolosi, blocchi unchecked e operazioni di precisione sospette, ma non può stabilire se una formula del prezzo delle quote sia economicamente corretta. Il fuzzing con Foundry è più efficace se combinato con asserzioni come che totalAssets copra tutte le richieste di riscatto e che un depositante non possa trarre profitto da un ciclo immediato di deposito-prelievo senza rendimento legittimo.
I test sugli invarianti devono generare sequenze multi-utente: deposito, report di rendimento, prelievo, prelievo parziale, donazione e depositi piccoli ripetuti. La pratica di revisione Soken confronta anche il modello matematico previsto con ogni aggiornamento di stato, poiché una formula corretta applicata prima piuttosto che dopo un cambio di stato può comunque valutare male le quote.
La sicurezza aritmetica è la disciplina di preservare la verità economica in ogni conversione. Un vault può usare l’aritmetica controllata di Solidity 0.8 ed essere comunque vulnerabile se emette diritti al prezzo sbagliato, assume decimali errati o arrotonda il valore verso l’esterno.
Inizia scrivendo unità e invarianti per ogni variabile contabile, quindi codifica quegli invarianti in test Foundry prima di aggiungere nuovi tipi di asset o percorsi di prezzo. Per approfondimenti sulla sicurezza Solidity, visita il Soken Hub.