Sicurezza dei contratti intelligenti
Ultimo aggiornamento della pagina: 26 febbraio 2026
I contratti intelligenti sono estremamente flessibili e in grado di controllare grandi quantità di valore e dati, eseguendo al contempo una logica immutabile basata sul codice distribuito sulla blockchain. Questo ha creato un vivace ecosistema di applicazioni decentralizzate e trustless che offrono molti vantaggi rispetto ai sistemi tradizionali. Rappresentano anche delle opportunità per gli aggressori che cercano di trarre profitto sfruttando le vulnerabilità nei contratti intelligenti.
Le blockchain pubbliche, come Ethereum, complicano ulteriormente il problema della sicurezza dei contratti intelligenti. Il codice del contratto distribuito di solito non può essere modificato per correggere le falle di sicurezza, mentre gli asset rubati dai contratti intelligenti sono estremamente difficili da rintracciare e per lo più irrecuperabili a causa dell'immutabilità.
Sebbene le cifre varino, si stima che l'importo totale del valore rubato o perso a causa di difetti di sicurezza nei contratti intelligenti superi facilmente il miliardo di dollari. Questo include incidenti di alto profilo, come l'attacco informatico alla DAO (opens in a new tab) (3,6 milioni di ETH rubati, per un valore di oltre 1 miliardo di dollari ai prezzi odierni), l'attacco informatico al portafoglio multifirma di Parity (opens in a new tab) (30 milioni di dollari persi a causa degli hacker) e il problema del portafoglio congelato di Parity (opens in a new tab) (oltre 300 milioni di dollari in ETH bloccati per sempre).
I problemi sopracitati rendono imperativo per gli sviluppatori investire sforzi nella creazione di contratti intelligenti sicuri, robusti e resilienti. La sicurezza dei contratti intelligenti è una questione seria, che ogni sviluppatore farà bene a imparare. Questa guida tratterà le considerazioni sulla sicurezza per gli sviluppatori di Ethereum ed esplorerà le risorse per migliorare la sicurezza dei contratti intelligenti.
Prerequisiti
Assicurati di avere familiarità con i fondamenti dello sviluppo di contratti intelligenti prima di affrontare la sicurezza.
Linee guida per creare contratti intelligenti di Ethereum sicuri
1. Progettare controlli di accesso adeguati
Nei contratti intelligenti, le funzioni contrassegnate come public o external possono essere chiamate da qualsiasi account controllato esternamente (EOA) o account di contratto. Specificare la visibilità pubblica per le funzioni è necessario se si desidera che altri interagiscano con il proprio contratto. Le funzioni contrassegnate come private, tuttavia, possono essere chiamate solo da funzioni all'interno del contratto intelligente e non da account esterni. Dare a ogni partecipante della rete l'accesso alle funzioni del contratto può causare problemi, specialmente se significa che chiunque può eseguire operazioni sensibili (ad es., coniare nuovi token).
Per prevenire l'uso non autorizzato delle funzioni del contratto intelligente, è necessario implementare controlli di accesso sicuri. I meccanismi di controllo degli accessi limitano la capacità di utilizzare determinate funzioni in un contratto intelligente a entità approvate, come gli account responsabili della gestione del contratto. Il modello Ownable e il controllo basato sui ruoli sono due modelli utili per implementare il controllo degli accessi nei contratti intelligenti:
Modello Ownable
Nel modello Ownable, un indirizzo viene impostato come "proprietario" (owner) del contratto durante il processo di creazione del contratto. Alle funzioni protette viene assegnato un modificatore OnlyOwner, che assicura che il contratto autentichi l'identità dell'indirizzo chiamante prima di eseguire la funzione. Le chiamate alle funzioni protette da altri indirizzi diversi dal proprietario del contratto vengono sempre annullate (revert), impedendo accessi indesiderati.
Controllo degli accessi basato sui ruoli
Registrare un singolo indirizzo come Owner in un contratto intelligente introduce il rischio di centralizzazione e rappresenta un singolo punto di vulnerabilità (single point-of-failure). Se le chiavi dell'account del proprietario vengono compromesse, gli aggressori possono attaccare il contratto posseduto. Questo è il motivo per cui l'utilizzo di un modello di controllo degli accessi basato sui ruoli con più account amministrativi potrebbe essere un'opzione migliore.
Nel controllo degli accessi basato sui ruoli, l'accesso alle funzioni sensibili è distribuito tra un insieme di partecipanti fidati. Ad esempio, un account potrebbe essere responsabile di coniare token, mentre un altro account esegue aggiornamenti o mette in pausa il contratto. Decentralizzare il controllo degli accessi in questo modo elimina i singoli punti di vulnerabilità e riduce le presunzioni di fiducia per gli utenti.
Utilizzo di portafogli multifirma
Un altro approccio per implementare un controllo degli accessi sicuro è l'utilizzo di un account multifirma per gestire un contratto. A differenza di un normale EOA, gli account multifirma sono di proprietà di più entità e richiedono le firme di un numero minimo di account (ad esempio 3 su 5) per eseguire le transazioni.
L'utilizzo di un multifirma per il controllo degli accessi introduce un ulteriore livello di sicurezza poiché le azioni sul contratto di destinazione richiedono il consenso di più parti. Ciò è particolarmente utile se è necessario utilizzare il modello Ownable, in quanto rende più difficile per un aggressore o un insider disonesto manipolare le funzioni sensibili del contratto per scopi dannosi.
2. Utilizzare le istruzioni require(), assert() e revert() per proteggere le operazioni del contratto
Come accennato, chiunque può chiamare funzioni pubbliche nel tuo contratto intelligente una volta che è stato distribuito sulla blockchain. Poiché non puoi sapere in anticipo come gli account esterni interagiranno con un contratto, è ideale implementare salvaguardie interne contro operazioni problematiche prima della distribuzione. Puoi imporre un comportamento corretto nei contratti intelligenti utilizzando le istruzioni require(), assert() e revert() per attivare eccezioni e annullare le modifiche di stato se l'esecuzione non soddisfa determinati requisiti.
require(): i require sono definiti all'inizio delle funzioni e assicurano che le condizioni predefinite siano soddisfatte prima che la funzione chiamata venga eseguita. Un'istruzione require può essere utilizzata per convalidare gli input dell'utente, controllare le variabili di stato o autenticare l'identità dell'account chiamante prima di procedere con una funzione.
assert(): assert() viene utilizzato per rilevare errori interni e verificare la presenza di violazioni di "invarianti" nel codice. Un invariante è un'asserzione logica sullo stato di un contratto che dovrebbe rimanere vera per tutte le esecuzioni di funzioni. Un esempio di invariante è l'offerta totale massima o il saldo di un contratto di token. L'utilizzo di assert() assicura che il tuo contratto non raggiunga mai uno stato vulnerabile e, se lo fa, tutte le modifiche alle variabili di stato vengono annullate.
revert(): revert() può essere utilizzato in un'istruzione if-else che attiva un'eccezione se la condizione richiesta non è soddisfatta. Il contratto di esempio seguente utilizza revert() per proteggere l'esecuzione delle funzioni:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error Unauthorized();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("Not enough Ether provided.");9 // Perform the purchase.10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert Unauthorized();1415 payable(msg.sender).transfer(address(this).balance);16 }17}Mostra tutto3. Testare i contratti intelligenti e verificare la correttezza del codice
L'immutabilità del codice in esecuzione nella macchina virtuale di Ethereum significa che i contratti intelligenti richiedono un livello più elevato di valutazione della qualità durante la fase di sviluppo. Testare ampiamente il tuo contratto e osservarlo per eventuali risultati imprevisti migliorerà notevolmente la sicurezza e proteggerà i tuoi utenti a lungo termine.
Il metodo usuale è scrivere piccoli test unitari utilizzando dati fittizi (mock) che il contratto dovrebbe ricevere dagli utenti. Il test unitario è utile per testare la funzionalità di determinate funzioni e garantire che un contratto intelligente funzioni come previsto.
Sfortunatamente, il test unitario è minimamente efficace per migliorare la sicurezza dei contratti intelligenti se utilizzato in isolamento. Un test unitario potrebbe dimostrare che una funzione viene eseguita correttamente per i dati fittizi, ma i test unitari sono efficaci solo quanto i test che vengono scritti. Ciò rende difficile rilevare casi limite mancati e vulnerabilità che potrebbero compromettere la sicurezza del tuo contratto intelligente.
Un approccio migliore è combinare il test unitario con il test basato sulle proprietà eseguito utilizzando l'analisi statica e dinamica. L'analisi statica si basa su rappresentazioni di basso livello, come i grafi del flusso di controllo (opens in a new tab) e gli alberi sintattici astratti (opens in a new tab) per analizzare gli stati del programma raggiungibili e i percorsi di esecuzione. Nel frattempo, le tecniche di analisi dinamica, come il fuzzing dei contratti intelligenti (opens in a new tab), eseguono il codice del contratto con valori di input casuali per rilevare operazioni che violano le proprietà di sicurezza.
La verifica formale è un'altra tecnica per verificare le proprietà di sicurezza nei contratti intelligenti. A differenza dei test regolari, la verifica formale può dimostrare in modo conclusivo l'assenza di errori in un contratto intelligente. Ciò si ottiene creando una specifica formale che cattura le proprietà di sicurezza desiderate e dimostrando che un modello formale dei contratti aderisce a questa specifica.
4. Richiedere una revisione indipendente del proprio codice
Dopo aver testato il tuo contratto, è buona norma chiedere ad altri di controllare il codice sorgente per eventuali problemi di sicurezza. I test non scopriranno ogni difetto in un contratto intelligente, ma ottenere una revisione indipendente aumenta la possibilità di individuare le vulnerabilità.
Audit
Commissionare un audit del contratto intelligente è un modo per condurre una revisione indipendente del codice. I revisori (auditor) svolgono un ruolo importante nel garantire che i contratti intelligenti siano sicuri e privi di difetti di qualità ed errori di progettazione.
Detto questo, dovresti evitare di trattare gli audit come una soluzione miracolosa. Gli audit dei contratti intelligenti non rileveranno ogni bug e sono progettati principalmente per fornire un ulteriore ciclo di revisioni, che può aiutare a rilevare problemi sfuggiti agli sviluppatori durante lo sviluppo e i test iniziali. Dovresti anche seguire le migliori pratiche per lavorare con i revisori, come documentare correttamente il codice e aggiungere commenti in linea, per massimizzare i vantaggi di un audit del contratto intelligente.
- Suggerimenti e trucchi per l'audit dei contratti intelligenti (opens in a new tab) - @tinchoabbate
- Ottieni il massimo dal tuo audit (opens in a new tab) - Inference
Programmi di bug bounty
L'impostazione di un programma di bug bounty è un altro approccio per implementare revisioni del codice esterne. Un bug bounty è una ricompensa finanziaria data a individui (di solito hacker whitehat) che scoprono vulnerabilità in un'applicazione.
Se utilizzati correttamente, i bug bounty incentivano i membri della comunità di hacker a ispezionare il tuo codice alla ricerca di difetti critici. Un esempio reale è il "bug dei soldi infiniti" che avrebbe permesso a un aggressore di creare una quantità illimitata di ether su Optimism (opens in a new tab), un protocollo di livello 2 in esecuzione su Ethereum. Fortunatamente, un hacker whitehat ha scoperto il difetto (opens in a new tab) e ha informato il team, guadagnando un ingente compenso nel processo (opens in a new tab).
Una strategia utile è impostare il compenso di un programma di bug bounty in proporzione alla quantità di fondi in gioco. Descritto come "bug bounty scalabile (opens in a new tab)", questo approccio fornisce incentivi finanziari agli individui per divulgare responsabilmente le vulnerabilità invece di sfruttarle.
5. Seguire le migliori pratiche durante lo sviluppo dei contratti intelligenti
L'esistenza di audit e bug bounty non ti esime dalla responsabilità di scrivere codice di alta qualità. Una buona sicurezza dei contratti intelligenti inizia seguendo processi di progettazione e sviluppo adeguati:
-
Archivia tutto il codice in un sistema di controllo della versione, come git
-
Apporta tutte le modifiche al codice tramite pull request
-
Assicurati che le pull request abbiano almeno un revisore indipendente; se stai lavorando da solo a un progetto, considera di trovare altri sviluppatori e scambiare revisioni del codice
-
Utilizza un ambiente di sviluppo per testare, compilare e distribuire contratti intelligenti
-
Esegui il tuo codice attraverso strumenti di analisi del codice di base, come Cyfrin Aderyn (opens in a new tab), Mythril e Slither. Idealmente, dovresti farlo prima che ogni pull request venga unita e confrontare le differenze nell'output
-
Assicurati che il tuo codice venga compilato senza errori e che il compilatore Solidity non emetta avvisi
-
Documenta correttamente il tuo codice (utilizzando NatSpec (opens in a new tab)) e descrivi i dettagli sull'architettura del contratto in un linguaggio di facile comprensione. Ciò renderà più facile per gli altri controllare e revisionare il tuo codice.
6. Implementare solidi piani di ripristino di emergenza
Progettare controlli di accesso sicuri, implementare modificatori di funzione e altri suggerimenti possono migliorare la sicurezza dei contratti intelligenti, ma non possono escludere la possibilità di exploit dannosi. Costruire contratti intelligenti sicuri richiede di "prepararsi al fallimento" e avere un piano di riserva per rispondere efficacemente agli attacchi. Un piano di ripristino di emergenza adeguato incorporerà alcuni o tutti i seguenti componenti:
Aggiornamenti del contratto
Sebbene i contratti intelligenti di Ethereum siano immutabili per impostazione predefinita, è possibile ottenere un certo grado di mutabilità utilizzando modelli di aggiornamento. L'aggiornamento dei contratti è necessario nei casi in cui un difetto critico rende inutilizzabile il tuo vecchio contratto e la distribuzione di una nuova logica è l'opzione più fattibile.
I meccanismi di aggiornamento del contratto funzionano in modo diverso, ma il "modello proxy" è uno degli approcci più popolari per l'aggiornamento dei contratti intelligenti. I modelli proxy (opens in a new tab) dividono lo stato e la logica di un'applicazione tra due contratti. Il primo contratto (chiamato 'contratto proxy') memorizza le variabili di stato (ad es., i saldi degli utenti), mentre il secondo contratto (chiamato 'contratto logico') contiene il codice per l'esecuzione delle funzioni del contratto.
Gli account interagiscono con il contratto proxy, che invia tutte le chiamate di funzione al contratto logico utilizzando la chiamata di basso livello delegatecall() (opens in a new tab). A differenza di una normale chiamata di messaggio, delegatecall() assicura che il codice in esecuzione all'indirizzo del contratto logico venga eseguito nel contesto del contratto chiamante. Ciò significa che il contratto logico scriverà sempre nell'archiviazione del proxy (invece che nella propria archiviazione) e i valori originali di msg.sender e msg.value vengono preservati.
Delegare le chiamate al contratto logico richiede la memorizzazione del suo indirizzo nell'archiviazione del contratto proxy. Pertanto, l'aggiornamento della logica del contratto è solo questione di distribuire un altro contratto logico e memorizzare il nuovo indirizzo nel contratto proxy. Poiché le chiamate successive al contratto proxy vengono instradate automaticamente al nuovo contratto logico, avresti "aggiornato" il contratto senza modificarne effettivamente il codice.
Maggiori informazioni sull'aggiornamento dei contratti.
Arresti di emergenza
Come accennato, audit e test approfonditi non possono scoprire tutti i bug in un contratto intelligente. Se una vulnerabilità appare nel tuo codice dopo la distribuzione, correggerla è impossibile poiché non puoi modificare il codice in esecuzione all'indirizzo del contratto. Inoltre, i meccanismi di aggiornamento (ad es., i modelli proxy) potrebbero richiedere tempo per essere implementati (spesso richiedono l'approvazione di diverse parti), il che dà solo agli aggressori più tempo per causare ulteriori danni.
L'opzione nucleare è implementare una funzione di "arresto di emergenza" che blocca le chiamate alle funzioni vulnerabili in un contratto. Gli arresti di emergenza in genere comprendono i seguenti componenti:
-
Una variabile booleana globale che indica se il contratto intelligente è in uno stato di arresto o meno. Questa variabile è impostata su
falsedurante la configurazione del contratto, ma tornerà atrueuna volta che il contratto viene arrestato. -
Funzioni che fanno riferimento alla variabile booleana nella loro esecuzione. Tali funzioni sono accessibili quando il contratto intelligente non è arrestato e diventano inaccessibili quando viene attivata la funzione di arresto di emergenza.
-
Un'entità che ha accesso alla funzione di arresto di emergenza, che imposta la variabile booleana su
true. Per prevenire azioni dannose, le chiamate a questa funzione possono essere limitate a un indirizzo fidato (ad es., il proprietario del contratto).
Una volta che il contratto attiva l'arresto di emergenza, alcune funzioni non saranno chiamabili. Ciò si ottiene avvolgendo funzioni selezionate in un modificatore che fa riferimento alla variabile globale. Di seguito è riportato un esempio (opens in a new tab) che descrive un'implementazione di questo modello nei contratti:
1// Questo codice non è stato sottoposto ad audit professionale e non offre alcuna garanzia di sicurezza o correttezza. Utilizzare a proprio rischio.23contract EmergencyStop {45 bool isStopped = false;67 modifier stoppedInEmergency {8 require(!isStopped);9 _;10 }1112 modifier onlyWhenStopped {13 require(isStopped);14 _;15 }1617 modifier onlyAuthorized {18 // Verifica l'autorizzazione di msg.sender qui19 _;20 }2122 function stopContract() public onlyAuthorized {23 isStopped = true;24 }2526 function resumeContract() public onlyAuthorized {27 isStopped = false;28 }2930 function deposit() public payable stoppedInEmergency {31 // La logica di deposito avviene qui32 }3334 function emergencyWithdraw() public onlyWhenStopped {35 // Il prelievo di emergenza avviene qui36 }37}Mostra tuttoQuesto esempio mostra le caratteristiche di base degli arresti di emergenza:
-
isStoppedè un booleano che restituiscefalseall'inizio etruequando il contratto entra in modalità di emergenza. -
I modificatori di funzione
onlyWhenStoppedestoppedInEmergencycontrollano la variabileisStopped.stoppedInEmergencyviene utilizzato per controllare le funzioni che dovrebbero essere inaccessibili quando il contratto è vulnerabile (ad es.,deposit()). Le chiamate a queste funzioni verranno semplicemente annullate.
onlyWhenStopped viene utilizzato per le funzioni che dovrebbero essere chiamabili durante un'emergenza (ad es., emergencyWithdraw()). Tali funzioni possono aiutare a risolvere la situazione, da qui la loro esclusione dall'elenco delle "funzioni limitate".
L'utilizzo di una funzionalità di arresto di emergenza fornisce un efficace espediente per affrontare gravi vulnerabilità nel tuo contratto intelligente. Tuttavia, aumenta la necessità per gli utenti di fidarsi degli sviluppatori affinché non la attivino per motivi egoistici. A tal fine, decentralizzare il controllo dell'arresto di emergenza sottoponendolo a un meccanismo di voto on-chain, a un blocco temporale (timelock) o all'approvazione da un portafoglio multifirma sono possibili soluzioni.
Monitoraggio degli eventi
Gli eventi (opens in a new tab) ti consentono di tracciare le chiamate alle funzioni del contratto intelligente e monitorare le modifiche alle variabili di stato. È ideale programmare il tuo contratto intelligente per emettere un evento ogni volta che una parte intraprende un'azione critica per la sicurezza (ad es., il prelievo di fondi).
La registrazione degli eventi e il loro monitoraggio fuori catena forniscono informazioni sulle operazioni del contratto e aiutano a scoprire più rapidamente le azioni dannose. Ciò significa che il tuo team può rispondere più rapidamente agli hack e intraprendere azioni per mitigare l'impatto sugli utenti, come mettere in pausa le funzioni o eseguire un aggiornamento.
Puoi anche optare per uno strumento di monitoraggio pronto all'uso che inoltra automaticamente avvisi ogni volta che qualcuno interagisce con i tuoi contratti. Questi strumenti ti consentiranno di creare avvisi personalizzati basati su diversi trigger, come il volume delle transazioni, la frequenza delle chiamate di funzione o le funzioni specifiche coinvolte. Ad esempio, potresti programmare un avviso che arriva quando l'importo prelevato in una singola transazione supera una determinata soglia.
7. Progettare sistemi di governance sicuri
Potresti voler decentralizzare la tua applicazione cedendo il controllo dei contratti intelligenti principali ai membri della comunità. In questo caso, il sistema di contratti intelligenti includerà un modulo di governance: un meccanismo che consente ai membri della comunità di approvare azioni amministrative tramite un sistema di governance on-chain. Ad esempio, una proposta per aggiornare un contratto proxy a una nuova implementazione potrebbe essere votata dai possessori di token.
La governance decentralizzata può essere vantaggiosa, soprattutto perché allinea gli interessi degli sviluppatori e degli utenti finali. Tuttavia, i meccanismi di governance dei contratti intelligenti possono introdurre nuovi rischi se implementati in modo errato. Uno scenario plausibile è se un aggressore acquisisce un enorme potere di voto (misurato in numero di token posseduti) stipulando un prestito lampo (flash loan) e fa approvare una proposta dannosa.
Un modo per prevenire i problemi relativi alla governance on-chain è utilizzare un blocco temporale (timelock) (opens in a new tab). Un blocco temporale impedisce a un contratto intelligente di eseguire determinate azioni fino a quando non trascorre un periodo di tempo specifico. Altre strategie includono l'assegnazione di un "peso di voto" a ciascun token in base a quanto tempo è stato bloccato, o la misurazione del potere di voto di un indirizzo in un periodo storico (ad esempio, 2-3 blocchi nel passato) invece del blocco corrente. Entrambi i metodi riducono la possibilità di accumulare rapidamente potere di voto per influenzare i voti on-chain.
Maggiori informazioni sulla progettazione di sistemi di governance sicuri (opens in a new tab), sui diversi meccanismi di voto nelle DAO (opens in a new tab) e sui comuni vettori di attacco alle DAO che sfruttano la DeFi (opens in a new tab) nei link condivisi.
8. Ridurre al minimo la complessità del codice
Gli sviluppatori di software tradizionali hanno familiarità con il principio KISS ("keep it simple, stupid"), che sconsiglia di introdurre complessità non necessarie nella progettazione del software. Questo segue il pensiero di lunga data secondo cui "i sistemi complessi falliscono in modi complessi" e sono più suscettibili a errori costosi.
Mantenere le cose semplici è di particolare importanza quando si scrivono contratti intelligenti, dato che i contratti intelligenti controllano potenzialmente grandi quantità di valore. Un suggerimento per ottenere semplicità quando si scrivono contratti intelligenti è riutilizzare le librerie esistenti, come OpenZeppelin Contracts (opens in a new tab), ove possibile. Poiché queste librerie sono state ampiamente controllate e testate dagli sviluppatori, il loro utilizzo riduce le possibilità di introdurre bug scrivendo nuove funzionalità da zero.
Un altro consiglio comune è scrivere piccole funzioni e mantenere i contratti modulari dividendo la logica aziendale su più contratti. Scrivere codice più semplice non solo riduce la superficie di attacco in un contratto intelligente, ma rende anche più facile ragionare sulla correttezza del sistema complessivo e rilevare tempestivamente possibili errori di progettazione.
9. Difendersi dalle comuni vulnerabilità dei contratti intelligenti
Rientranza (Reentrancy)
L'EVM non consente la concorrenza, il che significa che due contratti coinvolti in una chiamata di messaggio non possono essere eseguiti contemporaneamente. Una chiamata esterna mette in pausa l'esecuzione e la memoria del contratto chiamante fino al ritorno della chiamata, a quel punto l'esecuzione procede normalmente. Questo processo può essere formalmente descritto come il trasferimento del flusso di controllo (opens in a new tab) a un altro contratto.
Sebbene per lo più innocuo, il trasferimento del flusso di controllo a contratti non attendibili può causare problemi, come la rientranza. Un attacco di rientranza si verifica quando un contratto dannoso richiama un contratto vulnerabile prima che l'invocazione della funzione originale sia completata. Questo tipo di attacco è meglio spiegato con un esempio.
Considera un semplice contratto intelligente ('Victim') che consente a chiunque di depositare e prelevare ether:
1// Questo contratto è vulnerabile. Non utilizzare in produzione23contract Victim {4 mapping (address => uint256) public balances;56 function deposit() external payable {7 balances[msg.sender] += msg.value;8 }910 function withdraw() external {11 uint256 amount = balances[msg.sender];12 (bool success, ) = msg.sender.call.value(amount)("");13 require(success);14 balances[msg.sender] = 0;15 }16}Mostra tuttoQuesto contratto espone una funzione withdraw() per consentire agli utenti di prelevare gli ETH precedentemente depositati nel contratto. Durante l'elaborazione di un prelievo, il contratto esegue le seguenti operazioni:
- Controlla il saldo in ETH dell'utente
- Invia i fondi all'indirizzo chiamante
- Azzera il loro saldo a 0, impedendo ulteriori prelievi da parte dell'utente
La funzione withdraw() nel contratto Victim segue un modello "controlli-interazioni-effetti" (checks-interactions-effects). Controlla se le condizioni necessarie per l'esecuzione sono soddisfatte (ovvero, l'utente ha un saldo in ETH positivo) ed esegue l'interazione inviando ETH all'indirizzo del chiamante, prima di applicare gli effetti della transazione (ovvero, riducendo il saldo dell'utente).
Se withdraw() viene chiamata da un account controllato esternamente (EOA), la funzione viene eseguita come previsto: msg.sender.call.value() invia ETH al chiamante. Tuttavia, se msg.sender è un account di contratto intelligente che chiama withdraw(), l'invio di fondi utilizzando msg.sender.call.value() attiverà anche l'esecuzione del codice memorizzato a quell'indirizzo.
Immagina che questo sia il codice distribuito all'indirizzo del contratto:
1 contract Attacker {2 function beginAttack() external payable {3 Victim(victim_address).deposit.value(1 ether)();4 Victim(victim_address).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(victim_address).withdraw();10 }11 }12}Mostra tuttoQuesto contratto è progettato per fare tre cose:
- Accettare un deposito da un altro account (probabilmente l'EOA dell'aggressore)
- Depositare 1 ETH nel contratto Victim
- Prelevare l'1 ETH memorizzato nel contratto intelligente
Non c'è niente di sbagliato qui, tranne che Attacker ha un'altra funzione che chiama di nuovo withdraw() in Victim se il gas rimasto dal msg.sender.call.value in entrata è superiore a 40.000. Ciò dà ad Attacker la capacità di rientrare in Victim e prelevare più fondi prima che la prima invocazione di withdraw sia completata. Il ciclo si presenta così:
1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`3- `Attacker` calls `withdraw() in `Victim`4- `Victim` checks `Attacker`’s balance (1 ETH)5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0Mostra tuttoIl riassunto è che poiché il saldo del chiamante non viene impostato a 0 fino al completamento dell'esecuzione della funzione, le invocazioni successive avranno esito positivo e consentiranno al chiamante di prelevare il proprio saldo più volte. Questo tipo di attacco può essere utilizzato per prosciugare i fondi di un contratto intelligente, come è successo nell'hack della DAO del 2016 (opens in a new tab). Gli attacchi di rientranza sono ancora oggi un problema critico per i contratti intelligenti, come mostrano gli elenchi pubblici di exploit di rientranza (opens in a new tab).
Come prevenire gli attacchi di rientranza
Un approccio per affrontare la rientranza è seguire il modello controlli-effetti-interazioni (opens in a new tab). Questo modello ordina l'esecuzione delle funzioni in modo tale che il codice che esegue i controlli necessari prima di procedere con l'esecuzione venga per primo, seguito dal codice che manipola lo stato del contratto, con il codice che interagisce con altri contratti o EOA che arriva per ultimo.
Il modello controlli-effetti-interazioni viene utilizzato in una versione rivista del contratto Victim mostrata di seguito:
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}Questo contratto esegue un controllo sul saldo dell'utente, applica gli effetti della funzione withdraw() (azzerando il saldo dell'utente a 0) e procede a eseguire l'interazione (inviando ETH all'indirizzo dell'utente). Ciò assicura che il contratto aggiorni la sua archiviazione prima della chiamata esterna, eliminando la condizione di rientranza che ha consentito il primo attacco. Il contratto Attacker potrebbe ancora richiamare NoLongerAVictim, ma poiché balances[msg.sender] è stato impostato a 0, ulteriori prelievi genereranno un errore.
Un'altra opzione è utilizzare un blocco di mutua esclusione (comunemente descritto come "mutex") che blocca una parte dello stato di un contratto fino al completamento dell'invocazione di una funzione. Questo viene implementato utilizzando una variabile booleana che viene impostata su true prima dell'esecuzione della funzione e torna a false dopo che l'invocazione è terminata. Come si vede nell'esempio seguente, l'utilizzo di un mutex protegge una funzione dalle chiamate ricorsive mentre l'invocazione originale è ancora in elaborazione, fermando efficacemente la rientranza.
1pragma solidity ^0.7.0;23contract MutexPattern {4 bool locked = false;5 mapping(address => uint256) public balances;67 modifier noReentrancy() {8 require(!locked, "Blocked from reentrancy.");9 locked = true;10 _;11 locked = false;12 }13 // Questa funzione è protetta da un mutex, quindi le chiamate rientranti dall'interno di `msg.sender.call` non possono chiamare di nuovo `withdraw`.14 // L'istruzione `return` restituisce `true` ma valuta comunque l'istruzione `locked = false` nel modificatore15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");1718 balances[msg.sender] -= _amount;19 (bool success, ) = msg.sender.call{value: _amount}("");20 require(success);2122 return true;23 }24}Mostra tuttoPuoi anche utilizzare un sistema di pagamenti pull (opens in a new tab) che richiede agli utenti di prelevare fondi dai contratti intelligenti, invece di un sistema di "pagamenti push" che invia fondi agli account. Ciò rimuove la possibilità di attivare inavvertitamente codice a indirizzi sconosciuti (e può anche prevenire determinati attacchi denial-of-service).
Underflow e overflow di interi
Un overflow di interi si verifica quando i risultati di un'operazione aritmetica non rientrano nell'intervallo di valori accettabile, facendolo "riavvolgere" al valore rappresentabile più basso. Ad esempio, un uint8 può memorizzare solo valori fino a 2^8-1=255. Le operazioni aritmetiche che producono valori superiori a 255 andranno in overflow e ripristineranno uint a 0, in modo simile a come il contachilometri di un'auto si azzera a 0 una volta raggiunto il chilometraggio massimo (999999).
Gli underflow di interi si verificano per motivi simili: i risultati di un'operazione aritmetica scendono al di sotto dell'intervallo accettabile. Supponiamo che tu abbia provato a decrementare 0 in un uint8, il risultato si riavvolgerebbe semplicemente al valore massimo rappresentabile (255).
Sia gli overflow che gli underflow di interi possono portare a modifiche impreviste alle variabili di stato di un contratto e provocare un'esecuzione non pianificata. Di seguito è riportato un esempio che mostra come un aggressore può sfruttare l'overflow aritmetico in un contratto intelligente per eseguire un'operazione non valida:
1pragma solidity ^0.7.6;23// This contract is designed to act as a time vault.4// User can deposit into this contract but cannot withdraw for at least a week.5// User can also extend the wait time beyond the 1 week waiting period.67/*81. Deploy TimeLock92. Deploy Attack with address of TimeLock103. Call Attack.attack sending 1 ether. You will immediately be able to11 withdraw your ether.1213What happened?14Attack caused the TimeLock.lockTime to overflow and was able to withdraw15before the 1 week waiting period.16*/1718contract TimeLock {19 mapping(address => uint) public balances;20 mapping(address => uint) public lockTime;2122 function deposit() external payable {23 balances[msg.sender] += msg.value;24 lockTime[msg.sender] = block.timestamp + 1 weeks;25 }2627 function increaseLockTime(uint _secondsToIncrease) public {28 lockTime[msg.sender] += _secondsToIncrease;29 }3031 function withdraw() public {32 require(balances[msg.sender] > 0, "Insufficient funds");33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");3435 uint amount = balances[msg.sender];36 balances[msg.sender] = 0;3738 (bool sent, ) = msg.sender.call{value: amount}("");39 require(sent, "Failed to send Ether");40 }41}4243contract Attack {44 TimeLock timeLock;4546 constructor(TimeLock _timeLock) {47 timeLock = TimeLock(_timeLock);48 }4950 fallback() external payable {}5152 function attack() public payable {53 timeLock.deposit{value: msg.value}();54 /*55 if t = current lock time then we need to find x such that56 x + t = 2**256 = 057 so x = -t58 2**256 = type(uint).max + 159 so x = type(uint).max + 1 - t60 */61 timeLock.increaseLockTime(62 type(uint).max + 1 - timeLock.lockTime(address(this))63 );64 timeLock.withdraw();65 }66}Mostra tuttoCome prevenire underflow e overflow di interi
A partire dalla versione 0.8.0, il compilatore Solidity rifiuta il codice che genera underflow e overflow di interi. Tuttavia, i contratti compilati con una versione del compilatore inferiore dovrebbero eseguire controlli sulle funzioni che coinvolgono operazioni aritmetiche o utilizzare una libreria (ad es., SafeMath (opens in a new tab)) che controlla la presenza di underflow/overflow.
Manipolazione dell'oracolo
Gli oracoli reperiscono informazioni fuori catena e le inviano on-chain affinché i contratti intelligenti le utilizzino. Con gli oracoli, puoi progettare contratti intelligenti che interagiscono con sistemi fuori catena, come i mercati dei capitali, espandendo notevolmente la loro applicazione.
Ma se l'oracolo è corrotto e invia informazioni errate on-chain, i contratti intelligenti verranno eseguiti in base a input errati, il che può causare problemi. Questa è la base del "problema dell'oracolo", che riguarda il compito di assicurarsi che le informazioni provenienti da un oracolo della blockchain siano accurate, aggiornate e tempestive.
Un problema di sicurezza correlato è l'utilizzo di un oracolo on-chain, come un exchange decentralizzato, per ottenere il prezzo spot di un asset. Le piattaforme di prestito nel settore della finanza decentralizzata (DeFi) lo fanno spesso per determinare il valore della garanzia di un utente per stabilire quanto può prendere in prestito.
I prezzi dei DEX sono spesso accurati, in gran parte grazie agli arbitraggisti che ripristinano la parità nei mercati. Tuttavia, sono aperti alla manipolazione, in particolare se l'oracolo on-chain calcola i prezzi degli asset in base a modelli di trading storici (come di solito accade).
Ad esempio, un aggressore potrebbe pompare artificialmente il prezzo spot di un asset stipulando un prestito lampo (flash loan) subito prima di interagire con il tuo contratto di prestito. Interrogare il DEX per il prezzo dell'asset restituirebbe un valore superiore al normale (a causa del grande "ordine di acquisto" dell'aggressore che distorce la domanda per l'asset), consentendogli di prendere in prestito più di quanto dovrebbe. Tali "attacchi di prestito lampo" sono stati utilizzati per sfruttare la dipendenza dagli oracoli di prezzo tra le applicazioni DeFi, costando ai protocolli milioni in fondi persi.
Come prevenire la manipolazione dell'oracolo
Il requisito minimo per evitare la manipolazione dell'oracolo (opens in a new tab) è utilizzare una rete di oracoli decentralizzata che interroga le informazioni da più fonti per evitare singoli punti di vulnerabilità. Nella maggior parte dei casi, gli oracoli decentralizzati hanno incentivi criptoeconomici integrati per incoraggiare i nodi dell'oracolo a riportare informazioni corrette, rendendoli più sicuri degli oracoli centralizzati.
Se prevedi di interrogare un oracolo on-chain per i prezzi degli asset, considera di utilizzarne uno che implementi un meccanismo di prezzo medio ponderato nel tempo (TWAP). Un oracolo TWAP (opens in a new tab) interroga il prezzo di un asset in due diversi momenti nel tempo (che puoi modificare) e calcola il prezzo spot in base alla media ottenuta. La scelta di periodi di tempo più lunghi protegge il tuo protocollo dalla manipolazione dei prezzi poiché i grandi ordini eseguiti di recente non possono influire sui prezzi degli asset.
Risorse sulla sicurezza dei contratti intelligenti per gli sviluppatori
Strumenti per analizzare i contratti intelligenti e verificare la correttezza del codice
-
Strumenti e librerie di test - Raccolta di strumenti e librerie standard del settore per eseguire test unitari, analisi statica e analisi dinamica sui contratti intelligenti.
-
Strumenti di verifica formale - Strumenti per verificare la correttezza funzionale nei contratti intelligenti e controllare gli invarianti.
-
Servizi di auditing dei contratti intelligenti - Elenco di organizzazioni che forniscono servizi di auditing dei contratti intelligenti per i progetti di sviluppo su Ethereum.
-
Piattaforme di bug bounty - Piattaforme per coordinare i bug bounty e ricompensare la divulgazione responsabile di vulnerabilità critiche nei contratti intelligenti.
-
Fork Checker (opens in a new tab) - Uno strumento online gratuito per controllare tutte le informazioni disponibili riguardo a un contratto biforcato.
-
ABI Encoder (opens in a new tab) - Un servizio online gratuito per codificare le funzioni del tuo contratto Solidity e gli argomenti del costruttore.
-
Aderyn (opens in a new tab) - Analizzatore statico per Solidity, che attraversa gli Alberi Sintattici Astratti (AST) per individuare vulnerabilità sospette e stampare i problemi in un formato markdown facile da consultare.
Strumenti per il monitoraggio dei contratti intelligenti
- Tenderly Real-Time Alerting (opens in a new tab) - Uno strumento per ricevere notifiche in tempo reale quando si verificano eventi insoliti o inaspettati sui tuoi contratti intelligenti o portafogli.
Strumenti per l'amministrazione sicura dei contratti intelligenti
-
Safe (opens in a new tab) - Portafoglio di contratti intelligenti in esecuzione su Ethereum che richiede un numero minimo di persone per approvare una transazione prima che possa avvenire (M-di-N).
-
OpenZeppelin Contracts (opens in a new tab) - Librerie di contratti per implementare funzionalità amministrative, tra cui la proprietà del contratto, gli aggiornamenti, i controlli di accesso, la governance, la possibilità di pausa e altro ancora.
Servizi di auditing dei contratti intelligenti
-
ConsenSys Diligence (opens in a new tab) - Servizio di auditing dei contratti intelligenti che aiuta i progetti in tutto l'ecosistema blockchain ad assicurarsi che i loro protocolli siano pronti per il lancio e costruiti per proteggere gli utenti.
-
CertiK (opens in a new tab) - Azienda di sicurezza blockchain pioniera nell'uso di tecnologie di verifica formale all'avanguardia sui contratti intelligenti e sulle reti blockchain.
-
Trail of Bits (opens in a new tab) - Azienda di sicurezza informatica che combina la ricerca sulla sicurezza con una mentalità da attaccante per ridurre i rischi e fortificare il codice.
-
PeckShield (opens in a new tab) - Azienda di sicurezza blockchain che offre prodotti e servizi per la sicurezza, la privacy e l'usabilità dell'intero ecosistema blockchain.
-
QuantStamp (opens in a new tab) - Servizio di auditing che facilita l'adozione di massa della tecnologia blockchain attraverso servizi di sicurezza e valutazione dei rischi.
-
OpenZeppelin (opens in a new tab) - Azienda di sicurezza dei contratti intelligenti che fornisce audit di sicurezza per sistemi distribuiti.
-
Runtime Verification (opens in a new tab) - Azienda di sicurezza specializzata nella modellazione formale e nella verifica dei contratti intelligenti.
-
Hacken (opens in a new tab) - Revisore di sicurezza informatica web3 che porta un approccio a 360 gradi alla sicurezza della blockchain.
-
Nethermind (opens in a new tab) - Servizi di auditing per Solidity e Cairo, che garantiscono l'integrità dei contratti intelligenti e la sicurezza degli utenti su Ethereum e Starknet.
-
HashEx (opens in a new tab) - HashEx si concentra sull'auditing della blockchain e dei contratti intelligenti per garantire la sicurezza delle criptovalute, fornendo servizi come lo sviluppo di contratti intelligenti, test di penetrazione e consulenza blockchain.
-
Code4rena (opens in a new tab) - Piattaforma di audit competitiva che incentiva gli esperti di sicurezza dei contratti intelligenti a trovare vulnerabilità e contribuire a rendere il web3 più sicuro.
-
CodeHawks (opens in a new tab) - Piattaforma di audit competitiva che ospita competizioni di auditing dei contratti intelligenti per i ricercatori di sicurezza.
-
Cyfrin (opens in a new tab) - Potenza della sicurezza web3, che incuba la sicurezza crittografica attraverso prodotti e servizi di auditing dei contratti intelligenti.
-
ImmuneBytes (opens in a new tab) - Azienda di sicurezza web3 che offre audit di sicurezza per sistemi blockchain attraverso un team di revisori esperti e strumenti di prim'ordine.
-
Oxorio (opens in a new tab) - Audit di contratti intelligenti e servizi di sicurezza blockchain con esperienza in EVM, Solidity, ZK e tecnologie cross-chain per aziende crittografiche e progetti DeFi.
-
Inference (opens in a new tab) - Azienda di auditing di sicurezza, specializzata nell'auditing di contratti intelligenti per blockchain basate su EVM. Grazie ai suoi revisori esperti, identificano potenziali problemi e suggeriscono soluzioni attuabili per risolverli prima della distribuzione.
Piattaforme di bug bounty
-
Immunefi (opens in a new tab) - Piattaforma di bug bounty per contratti intelligenti e progetti DeFi, dove i ricercatori di sicurezza esaminano il codice, divulgano le vulnerabilità, vengono pagati e rendono le criptovalute più sicure.
-
HackerOne (opens in a new tab) - Piattaforma di coordinamento delle vulnerabilità e bug bounty che connette le aziende con penetration tester e ricercatori di sicurezza informatica.
-
HackenProof (opens in a new tab) - Piattaforma esperta di bug bounty per progetti crittografici (DeFi, contratti intelligenti, portafogli, CEX e altro), dove i professionisti della sicurezza forniscono servizi di triage e i ricercatori vengono pagati per segnalazioni di bug rilevanti e verificate.
-
Sherlock (opens in a new tab) - Sottoscrittore nel web3 per la sicurezza dei contratti intelligenti, con pagamenti per i revisori gestiti tramite contratti intelligenti per garantire che i bug rilevanti vengano pagati equamente.
-
CodeHawks (opens in a new tab) - Piattaforma competitiva di bug bounty in cui i revisori partecipano a concorsi e sfide di sicurezza e (presto) ai propri audit privati.
Pubblicazioni di vulnerabilità ed exploit noti dei contratti intelligenti
-
ConsenSys: Smart Contract Known Attacks (opens in a new tab) - Spiegazione adatta ai principianti delle vulnerabilità più significative dei contratti, con codice di esempio per la maggior parte dei casi.
-
SWC Registry (opens in a new tab) - Elenco curato di elementi della Common Weakness Enumeration (CWE) che si applicano ai contratti intelligenti di Ethereum.
-
Rekt (opens in a new tab) - Pubblicazione regolarmente aggiornata di hack ed exploit crittografici di alto profilo, insieme a rapporti post-mortem dettagliati.
Sfide per imparare la sicurezza dei contratti intelligenti
-
Awesome BlockSec CTF (opens in a new tab) - Elenco curato di wargame sulla sicurezza della blockchain, sfide e competizioni Capture The Flag (opens in a new tab) e resoconti delle soluzioni.
-
Damn Vulnerable DeFi (opens in a new tab) - Wargame per imparare la sicurezza offensiva dei contratti intelligenti DeFi e sviluppare competenze nella caccia ai bug e nell'auditing di sicurezza.
-
Ethernaut (opens in a new tab) - Wargame basato su web3/Solidity in cui ogni livello è un contratto intelligente che deve essere "hackerato".
-
HackenProof x HackTheBox (opens in a new tab) - Sfida di hacking di contratti intelligenti, ambientata in un'avventura fantasy. Il completamento con successo della sfida dà anche accesso a un programma privato di bug bounty.
Migliori pratiche per proteggere i contratti intelligenti
-
ConsenSys: Ethereum Smart Contract Security Best Practices (opens in a new tab) - Elenco completo di linee guida per proteggere i contratti intelligenti di Ethereum.
-
Nascent: Simple Security Toolkit (opens in a new tab) - Raccolta di guide pratiche incentrate sulla sicurezza e liste di controllo per lo sviluppo di contratti intelligenti.
-
Solidity Patterns (opens in a new tab) - Utile raccolta di modelli sicuri e migliori pratiche per il linguaggio di programmazione dei contratti intelligenti Solidity.
-
Solidity Docs: Security Considerations (opens in a new tab) - Linee guida per scrivere contratti intelligenti sicuri con Solidity.
-
Smart Contract Security Verification Standard (opens in a new tab) - Lista di controllo in quattordici parti creata per standardizzare la sicurezza dei contratti intelligenti per sviluppatori, architetti, revisori della sicurezza e fornitori.
-
Learn Smart Contract Security and Auditing (opens in a new tab) - Corso definitivo sulla sicurezza e l'auditing dei contratti intelligenti, creato per gli sviluppatori di contratti intelligenti che desiderano migliorare le loro migliori pratiche di sicurezza e diventare ricercatori di sicurezza.