Vai al contenuto

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)
  1. 1. Perimetro e documentazione
  2. 2. Pulizia del codice e analisi statica
  3. 3. Test
  4. 4. Fuzz test e invariant test
  5. 5. Controllo degli accessi e upgradeability
  6. 6. Integrazioni esterne
  7. 7. Deployment e operatività
  8. 8. Logistica dell'audit
  9. Come possiamo aiutarvi

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.0 flottante, e fate in modo che versione, optimizer runs, via_ir ed evm_version in foundry.toml corrispondano 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.log e 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, @return e, 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 slither sul 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 coverage e 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 di vm.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 TimelockController o 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 modifier initializer e reinitializer e limitate _authorizeUpgrade nei 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 updatedAt con 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 SafeERC20 per 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 (o ReentrancyGuardTransient con 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.

Avete un progetto in mente?

Raccontateci che cosa state costruendo. Uno sviluppatore senior vi risponde entro 24 ore nei giorni lavorativi; l'offerta scritta, a prezzo fisso o a milestone, arriva di norma entro pochi giorni lavorativi.

Preferite prima scriverci? Scriveteci

Parlerete direttamente con Ing. Ismet Mesic, Tech lead.