Approfondimenti
Audit smart contract: checklist di preparazione
30 controlli prima dell'audit di uno smart contract: perimetro e documentazione, test, fuzz e invariant test Foundry, triage di Slither, accessi e deployment.
Team di engineering di sigmacode.io8 min di lettura
In questa pagina (9)
Un audit di smart contract è costoso e ha una durata rigidamente prefissata. Gli auditor hanno a disposizione un numero fisso di giorni, e ogni ora che passano a capire che cosa dovrebbe fare il codice, a rincorrere un commit che cambia o a scrivere i test che non avete scritto voi è un'ora sottratta alla logica che può davvero far perdere fondi. Il report finale si riempie così di NatSpec mancanti, pragma flottanti e percorsi di revert non testati, invece dei problemi per cui state pagando.
Una codebase ben preparata ottiene, a parità di budget, un audit più approfondito. Questa checklist raccoglie ciò che predisponiamo prima di consegnare il codice a un auditor esterno: 30 controlli concreti su documentazione, pulizia del codice, test, controllo degli accessi, integrazioni, deployment e logistica. Non sostituisce l'audit; fa in modo che il tempo dell'audit vada dove conta.
1. Perimetro e documentazione#
Gli auditor possono verificare il comportamento solo rispetto a un intento che comprendono, quindi l'intento va messo per iscritto.
- Congelate un commit hash. Date agli auditor un unico commit con tag e non modificate il codice in esame per tutta la durata dell'incarico. Le correzioni vanno su un branch separato e vengono esaminate nel round di fix review.
- Pubblicate l'elenco dei file nel perimetro con le nSLOC. Elencate ogni contratto nel perimetro con il suo percorso e le righe di codice sorgente normalizzate, e dichiarate esplicitamente che cosa resta fuori (librerie, mock, script, file già sottoposti ad audit). Gli auditor fanno preventivi e pianificano in base alle nSLOC: un conteggio accurato evita sorprese da entrambe le parti.
- Scrivete una panoramica dell'architettura. Una o due pagine che descrivano i contratti, come si chiamano tra loro, quali contratti custodiscono fondi e i principali flussi utente. Un semplice diagramma delle chiamate e dei flussi di token risparmia agli auditor ore di reverse engineering.
- Specificate in prosa comportamento e invarianti. Descrivete che cosa deve fare ogni funzione external e le proprietà che devono valere sempre, ad esempio «la somma dei saldi degli utenti è pari a
totalAssets» oppure «solo il timelock può modificare le commissioni». Queste affermazioni diventano la base sia della revisione degli auditor sia dei vostri invariant test. - Documentate i problemi noti e i rischi accettati. Elencate le limitazioni di cui siete già a conoscenza e le scelte progettuali che accettate, come i compromessi sulla centralizzazione o i tipi di token non supportati. Così restano fuori dal report e gli auditor vedono dove avete già ragionato a fondo.
2. Pulizia del codice e analisi statica#
Il rumore nella codebase costa tempo di audit e produce rilievi di scarso valore: eliminatelo prima che lo vedano gli auditor.
- Fissate il compilatore e arrivate a zero warning. Usate un
pragma solidity 0.8.x;esatto invece di un^0.8.0flottante, e fate in modo che versione, optimizer runs,via_iredevm_versioninfoundry.tomlcorrispondano a ciò di cui farete il deployment. Ogni warning del compilatore va corretto o motivato consapevolmente. - Fissate ed elencate le dipendenze. Bloccate OpenZeppelin Contracts e ogni altra libreria su una release esatta, ad esempio un submodule con tag v5.x o una versione esatta in
package.json. Elencate tutte le dipendenze nella documentazione per l'audit e segnalate i file che avete copiato o modificato. - Rimuovete codice morto, TODO e output di debug. Eliminate dai contratti nel perimetro funzioni inutilizzate, codice commentato, import di
console.loge helper di test rimasti in giro. I TODO aperti segnalano logica incompleta e come tale verranno riportati. - Completate il NatSpec sulle funzioni external e public. Ogni funzione external e public dovrebbe avere
@notice,@param,@returne, dove pertinente, una nota su controllo degli accessi e revert. Usate custom error ed emettete eventi per ogni cambiamento di stato rilevante off-chain. - Eseguite Slither e fate il triage di ogni rilievo. Eseguite
slithersul commit congelato e classificate ogni rilievo come corretto, falso positivo o accettato, con una motivazione di una riga. Un secondo strumento come Aderyn intercetta spesso pattern diversi, e condividere l'output già sottoposto a triage dice agli auditor quali rilievi automatici sono già gestiti.
3. Test#
I test mostrano agli auditor come va usato il codice e permettono loro di scrivere rapidamente i proof of concept.
- Coprite ogni funzione external e pubblicate il report. Ogni funzione external e public richiede almeno un test dell'happy path. Generate un report di copertura per righe e branch con
forge coveragee spiegate i branch non coperti invece di nasconderli. - Testate i percorsi di revert e i casi limite. Verificate che chiamate non autorizzate, parametri non validi, importi a zero e valori limite vadano in revert con il custom error atteso tramite
vm.expectRevert. È nei percorsi di revert non testati che si nascondono i bug di validazione e di controllo degli accessi. - Eseguite fork test sulle integrazioni reali. Se il protocollo dialoga con contratti esterni come DEX, mercati di lending o oracoli, testate su un fork della mainnet o di una L2 con numeri di blocco fissati. I mock dimostrano soltanto che il vostro codice funziona con le vostre ipotesi sulla controparte.
4. Fuzz test e invariant test#
Il fuzzing trova le combinazioni di input a cui nessuno aveva pensato, e gli invariant test verificano che il sistema resti coerente lungo sequenze di chiamate.
- Fate fuzzing su ogni funzione che riceve input numerici. Scrivete fuzz test Foundry per importi, timestamp, calcoli delle quote e matematica delle commissioni, e vincolate gli input con
bound()anziché con un uso eccessivo divm.assume. Direzione dell'arrotondamento e overflow sui valori estremi sono i bersagli tipici. - Scrivete invariant test stateful con handler. Usate l'invariant testing di Foundry con contratti handler che chiamano il sistema in sequenze realistiche da più attori. Controllate solvibilità, conservazione del valore e proprietà di controllo degli accessi dopo ogni sequenza di chiamate.
- Tenete allineati gli invarianti scritti e la suite di test. A ogni invariante della vostra specifica deve corrispondere un invariant test, e ogni invariant test deve essere riconducibile alla specifica. Eseguite la suite in CI con un numero significativo di run e di depth, non solo in locale con i valori predefiniti.
5. Controllo degli accessi e upgradeability#
Funzioni privilegiate e upgrade possono spostare o bloccare ogni asset del sistema, quindi il modello di fiducia deve essere esplicito.
- Elencate ogni ruolo e ogni funzione privilegiata. Documentate ciascun ruolo, quali funzioni può chiamare, chi lo detiene e qual è lo scenario peggiore se la sua chiave viene compromessa. È la sezione sulle trust assumption che gli auditor chiederanno per prima.
- Mettete i poteri di admin dietro un multisig e un timelock. Parametri critici e upgrade dovrebbero essere controllati da un multisig come Safe, con un
TimelockControllero un ritardo equivalente per le modifiche che toccano i fondi degli utenti. Documentate la soglia dei firmatari e il ritardo. - Verificate il layout dello storage dei contratti upgradeable. Per i proxy usate i contratti upgradeable di OpenZeppelin v5 con namespaced storage ERC-7201 e validate gli upgrade con gli strumenti OpenZeppelin dedicati. Se un upgrade rientra nel perimetro, includete il diff del layout dello storage tra le versioni.
- Proteggete initializer e funzioni di upgrade. Chiamate
_disableInitializers()nel costruttore dell'implementazione, usate correttamente i modifierinitializerereinitializere limitate_authorizeUpgradenei proxy UUPS (ERC-1822). Un contratto di implementazione non protetto è un rilievo che gli auditor non dovrebbero mai trovarsi a riportare.
6. Integrazioni esterne#
Ogni chiamata esterna è un'ipotesi sul codice di qualcun altro, e ogni ipotesi va messa per iscritto e testata.
- Gestite dati obsoleti e guasti dell'oracolo. Confrontate
updatedAtcon l'heartbeat del feed, rifiutate prezzi nulli o negativi e, sulle L2, controllate il sequencer uptime feed. Decidete che cosa fa il protocollo quando l'oracolo non è disponibile e documentatelo. - Tenete conto delle particolarità dei token. Dichiarate quali tipi di token sono supportati e gestite, oppure escludete esplicitamente, i token fee-on-transfer, rebasing, con decimali diversi da 18 ed ERC-20 non standard. Usate
SafeERC20per i trasferimenti e misurate le differenze di saldo dove conta l'importo effettivamente ricevuto. - Mappate le superfici di reentrancy. Elencate ogni chiamata esterna e ogni trasferimento di token, seguite il pattern checks-effects-interactions e usate
ReentrancyGuard(oReentrancyGuardTransientcon EIP-1153) dove lo stato è condiviso. Considerate la reentrancy cross-function e read-only e le callback dei token ERC-721, ERC-1155 ed ERC-777. - Considerate MEV e front-running. Aggiungete limiti di slippage e deadline a swap e depositi, proteggete i vault ERC-4626 dall'inflation attack del primo depositante ed esaminate ogni logica che dipenda dall'ordine delle transazioni. Documentate quali rischi di ordinamento accettate.
7. Deployment e operatività#
Il codice sottoposto ad audit è sicuro solo quanto lo sono il suo deployment e la sua gestione.
- Rendete riproducibili gli script di deployment e documentate i parametri. Fate il deployment con gli script di Foundry dal commit congelato, tenete ogni parametro di costruttore e initializer in file di configurazione sotto controllo di versione e includete gli script nel perimetro dell'audit o quantomeno nella consegna. Un contratto corretto il cui deployment usa parametri sbagliati è comunque un contratto rotto.
- Fate una prova generale su testnet con sorgenti verificati. Eseguite lo stesso identico script di deployment su una testnet e su un fork della mainnet, verificate i sorgenti sul block explorer e controllate che i ruoli siano finiti agli indirizzi previsti. Date agli auditor gli indirizzi sulla testnet, così possono interagire con un deployment reale.
- Preparate pausa, runbook e monitoraggio. Decidete chi può mettere in pausa che cosa, scrivete un breve runbook per gli incidenti e configurate alert per cambi di ruolo, upgrade, prelievi consistenti e stati di pausa. Gli auditor esamineranno i percorsi di emergenza, che quindi devono esistere prima dell'audit.
8. Logistica dell'audit#
Una buona logistica tiene in movimento l'audit e fa sì che il round di correzioni venga davvero sfruttato.
- Indicate un referente tecnico e un canale. Assegnate uno sviluppatore che conosca la codebase e possa rispondere alle domande nel giro di poche ore, e concordate un canale condiviso per tutta la durata dell'audit. Ogni domanda senza risposta ferma la revisione.
- Riservate tempo per le correzioni e per un round di fix review. Pianificate la capacità degli sviluppatori subito dopo l'arrivo del report e concordate in anticipo che gli auditor esaminino le correzioni. Correzioni non riesaminate possono introdurre nuovi bug dopo che l'audit è stato chiuso.
- Comunicate audit e revisioni precedenti. Condividete i report degli audit passati, lo stato delle relative correzioni ed eventuali revisioni interne. Gli auditor possono così concentrarsi su ciò che è cambiato e verificare che i rilievi precedenti non si siano ripresentati.
Come possiamo aiutarvi#
Offriamo un Audit-Readiness Sprint a perimetro fisso, della durata di una o due settimane: threat model, analisi delle lacune della suite di test, fuzz test e invariant test con Foundry, triage di Slither, un elenco di correzioni in ordine di priorità e un pacchetto di consegna per l'audit con perimetro, documentazione e problemi noti. Non siamo una società di audit e lo sprint non sostituisce un audit esterno; prepara il vostro codice in modo che gli auditor possano dedicare il loro tempo alla logica. I dettagli sono nella nostra pagina Prezzi e nei nostri servizi Blockchain & Web3.
Se desiderate una stima gratuita per la vostra codebase, raccontateci il vostro progetto. Siamo lieti di firmare un NDA reciproco prima che condividiate qualsiasi codice.