Per imparare Solidity la strada più rapida è prendere un solo contratto piccolo e portarlo fino in fondo: scriverlo, compilarlo, testarlo, attaccarlo, correggerlo. È questo blockchain that runs exactly as written when it is called, holding and moving funds according to its code, without an intermediary able to change the outcome.">smart contract holding reserves of two or more tokens, supplied by users, that a decentralized exchange uses to execute trades automatically at a price set by a formula.">pool diverge, compared with simply holding the same tokens in a wallet. It becomes permanent when the provider withdraws.">il tutorial. In circa un’ora, sul tuo computer, avrai un contratto «salvadanaio» che custodisce ether per ogni utente, una suite di test che controlla depositi, prelievi, errori e diritti di accesso, un test di fuzzing che gli lancia addosso importi casuali e una copia volutamente difettosa che un secondo contratto svuota con un attacco di rientranza. Niente portafoglio, niente denaro vero, nessuna rete pubblica.
Una regola regge tutto il resto. Uno smart contract (in italiano «contratto intelligente», ma si dice quasi sempre smart contract) è codice pubblico che custodisce denaro e che, una volta pubblicato sulla rete, non si corregge di nascosto: un errore diventa quindi pubblico, permanente e spesso costoso. Per questo qui test e attacchi arrivano nella prima ora, non dopo la sintassi.
Tutto gira su una blockchain locale. La pubblicazione su una rete di prova pubblica e il collegamento del contratto a una pagina web sono materia di una lezione successiva di questo corso.
Solidity: cos’è e perché si impara con i test
Solidity è un linguaggio di programmazione ad alto livello e orientato agli oggetti, pensato per scrivere smart contract. Gira su Ethereum e sulle reti che ne copiano la macchina virtuale, la EVM. La sintassi ricorda JavaScript e C, ma il modello è diverso: ogni funzione che scrive dati ha un costo in gas, lo stato resta sulla blockchain tra una chiamata e l’altra e il codice è leggibile da chiunque.
Un’avvertenza pratica per chi legge in italiano: la documentazione ufficiale di Solidity non ha una traduzione italiana (l’indirizzo con il prefisso «it» della documentazione risponde con un errore 404 al 27 ottobre 2026). Esistono versioni in altre lingue, tra cui il francese, ma possono restare indietro rispetto all’inglese, quindi guarda sempre il numero di versione indicato in alto nella pagina. Anche le guide di Foundry, di OpenZeppelin e di Remix sono in inglese. La pagina di ethereum.org sui linguaggi per smart contract, invece, esiste in italiano. Il codice che trovi qui sotto non cambia da una lingua all’altra: gli identificatori e i comandi restano in inglese, e se vuoi puoi tradurre i commenti a // nel tuo progetto.
Cosa serve prima di iniziare
È la lezione 3 del percorso carriere e presuppone la precedente: la nostra guida su come diventare sviluppatore blockchain spiega perché Solidity è il linguaggio di Ethereum e delle catene che ne copiano la macchina virtuale, e che cosa studiare per primo. In concreto, devi trovarti a tuo agio con un linguaggio di programmazione (JavaScript, Python, Go o Rust si trasferiscono bene), con il terminale e con Git.
Serve anche un modello mentale della catena. Un contratto gira su Ethereum o su una rete compatibile; fa qualcosa solo quando lo chiama una transazione o un altro contratto; ogni chiamata è firmata da un conto che possiede una chiave privata. Se più avanti msg.sender ti sembra astratto, leggi prima come si costruisce e si firma una transazione crypto. Su una rete vera ogni passaggio che scrive dati costa gas; sulla blockchain di prova usata qui il gas viene misurato ma pagato con ether finti, quindi non spendi un centesimo.
Ambiente per programmare in Solidity: Remix nel browser, Foundry sul tuo computer
Vale la pena conoscere due configurazioni. Remix non richiede alcuna installazione ed è il modo più veloce per vedere un contratto in funzione. Foundry è un insieme di strumenti da riga di comando ed è quello che questa lezione usa per il progetto, perché i test si scrivono in Solidity stesso.
Remix: prova un contratto in 20 minuti senza installare nulla
Remix (documentazione in inglese) è un ambiente di sviluppo che gira nel browser, all’indirizzo remix.ethereum.org; esiste anche un’applicazione desktop. Crei un file, incolli un contratto, lo compili dal pannello laterale e lo pubblichi sulla Remix VM, che la documentazione di Remix descrive come una blockchain sandbox nel browser. Lo stato della sandbox viene conservato tra un ricaricamento e l’altro, a meno che tu non disattivi l’opzione nelle impostazioni, ma la memoria del browser è fragile: tutto ciò che conta va messo in un repository Git.
Usa Remix per provare un’idea o seguire un tutorial breve. Per un progetto con test, controllo di versione e librerie, passa a uno strumento locale.
Foundry: installare forge, cast, anvil e chisel con foundryup
Foundry è un insieme di quattro strumenti da riga di comando: forge compila e testa i contratti, cast parla con una blockchain dal terminale, anvil avvia un nodo Ethereum locale e chisel è una shell Solidity per provare singole righe. La pagina ufficiale di installazione (in inglese) indica due passaggi su macOS e Linux:
curl -L https://foundry.paradigm.xyz | bash
Poi apri un nuovo terminale (oppure ricarica il profilo della shell, per esempio con source ~/.zshrc) ed esegui:
foundryup
forge --version
La prima riga scarica l’installatore foundryup; foundryup recupera poi l’ultima versione stabile dei quattro strumenti e, secondo la documentazione, verifica l’hash SHA-256 di ogni file eseguibile confrontandolo con l’attestazione della release su GitHub. Passare a una shell uno script scaricato da internet è una scelta di fiducia: l’indirizzo qui sopra è quello indicato dalla documentazione di Foundry, e puoi aprire lo script e leggerlo prima, oppure usare l’immagine Docker ufficiale (ghcr.io/foundry-rs/foundry:latest).
Su Windows la documentazione chiede Git Bash o WSL; PowerShell e il Prompt dei comandi non sono supportati. Al 27 ottobre 2026 l’ultima versione stabile di Foundry su GitHub è la v1.8.6 (21 ottobre 2026, una release di manutenzione con correzioni di sicurezza alle dipendenze), e le versioni notturne escono ogni giorno. Ogni tanto rilancia foundryup per aggiornare.
Perché questa lezione usa Foundry e non Hardhat
Hardhat è l’altro grande strumento per i contratti Ethereum, e la lezione precedente li presentava entrambi. Sono due strumenti seri e mantenuti, e la scelta è questione di gusto. Il motivo qui è pratico: in Foundry un test è un altro contratto Solidity, quindi in ogni test che scrivi eserciti il linguaggio che stai imparando, e non c’è un livello JavaScript da configurare accanto. Niente di ciò che impari va perso se un futuro datore di lavoro usa Hardhat: il codice del contratto è lo stesso.
Creare il progetto e leggere che cosa ha fatto forge init
Crea la cartella del progetto. La prima forma conserva il contratto di esempio Counter di Foundry, comodo per una prima occhiata; la seconda parte da zero.
forge init piggybank
oppure, senza i file di esempio:
forge init --empty piggybank
cd piggybank
forge build
forge test
Abbiamo letto il codice sorgente di forge init nella versione v1.8.6 di Foundry per verificare che cosa crea. Ottieni src/ per i contratti, test/ per i test, script/ per gli script di pubblicazione, lib/ con la libreria di test forge-std già installata, un file di configurazione foundry.toml e un README. Se non passi --no-git, la cartella diventa anche un repository Git e, quando mancano, vengono aggiunti un .gitignore e un flusso di lavoro GitHub Actions che esegue i test. Senza --empty i file di esempio sono src/Counter.sol, test/Counter.t.sol e script/Counter.s.sol: lancia forge test una volta su di essi per controllare che l’installazione funzioni, poi cancellali.
Un’avvertenza sui tutorial più vecchi: molti mostrano forge init --no-commit o forge install --no-commit. Le versioni recenti di Foundry non fanno più commit in automatico, il riferimento attuale elenca solo --commit, quindi lascia perdere il vecchio flag. La libreria forge-std installata da forge init era alla v1.17.0 (30 settembre 2026) al 27 ottobre 2026.
Per l’editor, VS Code con un’estensione per Solidity offre evidenziazione della sintassi e salto alla definizione. forge init --vscode scrive un .vscode/settings.json e un remappings.txt perché l’estensione trovi le librerie in lib/; secondo il codice di Foundry, queste impostazioni sono pensate per l’estensione vscode-solidity di Juan Blanco. Funzionano anche altre estensioni: scegline una ancora aggiornata.
Imparare Solidity scrivendo il primo contratto, riga per riga
Il contratto è un salvadanaio. Chiunque può depositare ether, ogni indirizzo può prelevare il proprio saldo e nient’altro, e il proprietario (il conto che l’ha pubblicato) può bloccare i nuovi depositi. È abbastanza corto da leggerlo per intero e ogni riga insegna una caratteristica del linguaggio. Eccolo completo; salvalo come src/PiggyBank.sol.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
/// @notice Each user can deposit ether and withdraw their own balance.
contract PiggyBank {
address public immutable owner;
bool public paused;
mapping(address => uint256) public balanceOf;
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
error NotOwner();
error IsPaused();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
error TransferFailed();
modifier onlyOwner() {
require(msg.sender == owner, NotOwner());
_;
}
constructor() {
owner = msg.sender;
}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
function deposit() external payable {
if (paused) revert IsPaused();
if (msg.value == 0) revert ZeroAmount();
balanceOf[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
uint256 available = balanceOf[msg.sender];
if (amount > available) revert InsufficientBalance(available, amount);
// Effects first: update the balance BEFORE sending ether out.
balanceOf[msg.sender] = available - amount;
emit Withdrawn(msg.sender, amount);
// Interaction last.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, TransferFailed());
}
}
L’intestazione: licenza SPDX e pragma
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
La prima riga è un’etichetta di licenza leggibile da una macchina. Il compilatore avvisa se manca; MIT è una scelta comune per il codice aperto. La seconda riga è il pragma: ^0.8.28 significa «qualsiasi compilatore da 0.8.28 fino a 0.9.0 escluso». Per impostazione predefinita (l’opzione auto_detect_solc è attiva) Foundry legge il pragma, sceglie una versione del compilatore che lo soddisfa e la scarica se non è ancora sul tuo computer.
Perché 0.8.28 come limite inferiore? Il contratto usa require con un errore personalizzato. Secondo il registro delle modifiche di Solidity, la 0.8.26 (maggio 2024) ha introdotto quella forma solo per la pipeline via-IR, e la 0.8.27 (settembre 2024) l’ha resa disponibile anche con la pipeline tradizionale, quella che usano quasi tutti i progetti. Il pragma indica il compilatore più vecchio con cui il tuo codice è noto per funzionare: abbiamo compilato ogni file di questa lezione con solc 0.8.28 e 0.8.38, senza errori e senza avvisi. Non scrivere un >=0.8.0 aperto su codice che custodisce denaro: prometteresti che compila e si comporta allo stesso modo su versioni che non hai mai provato.
Variabili di stato, mapping e visibilità
address public immutable owner;
bool public paused;
mapping(address => uint256) public balanceOf;
Queste tre righe sono la memoria del contratto, conservata sulla blockchain tra una chiamata e l’altra. owner è un indirizzo marcato immutable: viene fissato una volta nel costruttore e poi incorporato nel codice pubblicato, il che lo rende più economico da leggere e impossibile da cambiare. paused è un semplice booleano che parte da false.
Il mapping è il cuore della banca. Funziona come una tabella hash da un indirizzo a un numero, con una particolarità che la documentazione di Solidity sottolinea: ogni chiave possibile «esiste» già con valore zero, e non puoi elencare le chiavi. Quindi balanceOf[someone] vale 0 finché quella persona non deposita, e se ti serve l’elenco di chi ha depositato devi memorizzarlo tu.
La parola public fa scrivere al compilatore una funzione di lettura gratis, così chiunque può chiamare owner(), paused() o balanceOf(address). Le funzioni hanno quattro visibilità: public, external (richiamabile solo dall’esterno), internal (questo contratto e quelli che ne ereditano) e private (solo questo contratto). Privato non vuol dire segreto: tutta la memoria di un contratto può essere letta da chiunque interroghi la blockchain.
Eventi ed errori personalizzati
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
error NotOwner();
error IsPaused();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
error TransferFailed();
Gli eventi sono registri. Un contratto non può leggerli, ma portafogli, block explorer e le applicazioni decentralizzate costruite sopra un contratto li ascoltano per aggiornare una schermata senza rileggere tutta la memoria. Marcare account come indexed permette a un’app di filtrare il registro, per esempio «tutti i depositi di questo indirizzo».
Gli errori personalizzati danno a una chiamata fallita un nome e, se serve, dei dati. InsufficientBalance(available, requested) dice a chi chiama che cosa è andato storto, il che aiuta sia gli utenti sia i test. Costano anche meno delle lunghe stringhe di errore, perché viene restituito solo un selettore di 4 byte più gli argomenti.
Il modifier, il costruttore e msg.sender
modifier onlyOwner() {
require(msg.sender == owner, NotOwner());
_;
}
constructor() {
owner = msg.sender;
}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
msg.sender è l’indirizzo che ha fatto la chiamata in corso: un conto che firma una transazione, oppure un altro contratto che chiama questo. Nel costruttore, che gira una sola volta alla pubblicazione, è chi pubblica, e quell’indirizzo diventa il proprietario.
Un modifier avvolge una funzione. onlyOwner esegue il suo controllo, poi il segnaposto _; esegue il corpo della funzione che decora. require(condizione, NotOwner()) prende per prima la condizione che deve essere vera: se è falsa, la chiamata viene annullata con l’errore personalizzato. È l’immagine speculare della forma if (...) revert usata più sotto in deposit, dove l’if descrive la situazione che non deve verificarsi. Entrambe danno a chi chiama lo stesso errore: scegli quella che si legge meglio. Un revert annulla ogni modifica della chiamata, come se non fosse mai avvenuta. setPaused è external perché nulla dentro il contratto la richiama.
deposit: funzioni payable e msg.value
function deposit() external payable {
if (paused) revert IsPaused();
if (msg.value == 0) revert ZeroAmount();
balanceOf[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
Solo una funzione marcata payable può ricevere ether; se mandi ether a qualunque altra funzione, la chiamata viene annullata. msg.value è l’importo inviato, in wei (1 ether sono 1018 wei; nel codice si può scrivere direttamente 1 ether). La funzione controlla prima, con la forma if (...) revert Error();, l’altro modo di sollevare un errore personalizzato, valido su ogni compilatore 0.8 dalla 0.8.4 in poi. Poi aggiorna la memoria, poi emette l’evento.
withdraw: perché conta l’ordine di tre righe
function withdraw(uint256 amount) external {
uint256 available = balanceOf[msg.sender];
if (amount > available) revert InsufficientBalance(available, amount);
// Effects first: update the balance BEFORE sending ether out.
balanceOf[msg.sender] = available - amount;
emit Withdrawn(msg.sender, amount);
// Interaction last.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, TransferFailed());
}
Leggi l’ordine. Prima il controllo (chi chiama ha abbastanza?). Poi l’effetto: il saldo scende in memoria. Solo alla fine arriva l’interazione, l’invio di ether con il .call{value: amount}("") di basso livello, che restituisce un indicatore di successo da verificare: require(ok, TransferFailed()) annulla l’intero prelievo, aggiornamento del saldo compreso, quando il trasferimento fallisce. Questa sequenza ha un nome, checks-effects-interactions (controlli, effetti, interazioni), e la sezione sulla sicurezza più sotto mostra che cosa succede quando gli ultimi due passaggi si scambiano di posto.
È anche il modello «pull» che la documentazione di Solidity raccomanda: sono gli utenti a venire a prelevare il proprio denaro, invece di un contratto che spinge ether verso un elenco di persone, dove un solo destinatario difettoso potrebbe bloccare tutti.
Compilare con forge build
Cancella i file Counter se li avevi tenuti, salva il contratto ed esegui forge build. Se non c’è ancora un compilatore solc adatto, Foundry ne scarica uno; poi scrive gli artefatti del contratto in out/, la cartella di output predefinita secondo il riferimento sulla configurazione. Per ogni contratto l’artefatto contiene l’ABI (l’elenco di funzioni, eventi ed errori che gli altri programmi usano per chiamarlo) e il bytecode che verrebbe pubblicato. Un errore di battitura ferma la compilazione indicando file, riga e messaggio; gli avvisi non la fermano, ma trattali come segnalazioni di bug.

Una nota sui nostri controlli: abbiamo compilato questo contratto e ogni file di test di questa lezione con il compilatore ufficiale solc-js, versioni 0.8.28 e 0.8.38, insieme a forge-std v1.17.0 e OpenZeppelin Contracts v5.7.0. Tutto è stato compilato senza avvisi. Abbiamo seguito a mano la logica dei test ma non abbiamo eseguito forge su questa macchina, quindi non riportiamo l’output di forge: lancia i comandi e leggi il tuo.
Testare uno smart contract con Foundry: forge test, cheatcode e fuzzing
Un contratto non testato è un contratto non finito. In Foundry i test sono contratti Solidity nella cartella test/, in file che finiscono con .t.sol, come spiega la guida ai test di Foundry (in inglese). Salva quanto segue come test/PiggyBank.t.sol. La prima metà prepara la scena e verifica i percorsi che vanno a buon fine.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
import {PiggyBank} from "../src/PiggyBank.sol";
contract PiggyBankTest is Test {
PiggyBank bank;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Same event as in the contract, so the test can say what it expects.
event Deposited(address indexed account, uint256 amount);
function setUp() public {
bank = new PiggyBank(); // this test contract becomes the owner
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
}
function test_DepositUpdatesBalance() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
assertEq(bank.balanceOf(alice), 1 ether);
assertEq(address(bank).balance, 1 ether);
}
function test_DepositEmitsEvent() public {
vm.expectEmit(true, false, false, true);
emit Deposited(alice, 1 ether);
vm.prank(alice);
bank.deposit{value: 1 ether}();
}
function test_WithdrawReturnsEther() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
vm.prank(alice);
bank.withdraw(1 ether);
assertEq(alice.balance, 10 ether);
assertEq(bank.balanceOf(alice), 0);
}
// ... the revert and fuzz tests follow below
}
Come è fatto un test Foundry
Il contratto di test eredita Test da forge-std, che porta con sé le asserzioni e i cheatcode. setUp() gira prima di ogni singolo test, quindi ognuno parte da una banca nuova: i test non dipendono l’uno dall’altro. Ogni funzione il cui nome comincia con test è un test; la convenzione è test_ più ciò che dimostra.
assertEq(a, b) fa fallire il test quando i due valori differiscono. makeAddr("alice") restituisce un indirizzo nuovo e lo etichetta «alice» nelle tracce, così leggi nomi invece di esadecimale. In setUp è il contratto di test a pubblicare la banca, quindi il contratto di test stesso è il proprietario; il dettaglio conta per il test sulla pausa qui sotto.
Cheatcode: vm.prank, vm.deal e vm.expectEmit
I cheatcode sono funzioni speciali dell’ambiente di test, raggiunte tramite l’oggetto vm. Dietro di esso c’è un indirizzo fisso, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D, che Foundry intercetta. I tre usati finora:
vm.deal(alice, 10 ether)imposta dal nulla il saldo in ether di Alice. Esiste solo nell’ambiente di test.vm.prank(alice)fa partire da Alice la chiamata successiva. Con le parole del riferimento di Foundry, «sets msg.sender to the specified address for the next call». Solo la successiva: per questo il test del prelievo lo chiama due volte.vm.expectEmit(true, false, false, true)annuncia che la chiamata successiva deve emettere un evento. I quattro booleani indicano quali parti confrontare: il primo campo indicizzato (quiaccount), il secondo e il terzo (non usati) e i dati (amount). Poi emetti l’evento che ti aspetti, copiato nel contratto di test, e fai la chiamata.
La seconda metà del file mette alla prova ciò che deve fallire, più un test di fuzzing. Incollala al posto della riga di commento.
function test_RevertWhen_DepositIsZero() public {
vm.expectRevert(PiggyBank.ZeroAmount.selector);
vm.prank(alice);
bank.deposit();
}
function test_RevertWhen_WithdrawingTooMuch() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
vm.expectRevert(
abi.encodeWithSelector(PiggyBank.InsufficientBalance.selector, 1 ether, 2 ether)
);
vm.prank(alice);
bank.withdraw(2 ether);
}
function test_RevertWhen_NotOwnerPauses() public {
vm.expectRevert(PiggyBank.NotOwner.selector);
vm.prank(bob);
bank.setPaused(true);
}
function test_OwnerCanPause() public {
bank.setPaused(true); // msg.sender is this test contract, the owner
vm.expectRevert(PiggyBank.IsPaused.selector);
vm.prank(alice);
bank.deposit{value: 1 ether}();
}
function testFuzz_DepositThenWithdraw(uint256 amount) public {
amount = bound(amount, 1, 100 ether);
vm.deal(alice, amount);
vm.prank(alice);
bank.deposit{value: amount}();
vm.prank(alice);
bank.withdraw(amount);
assertEq(alice.balance, amount);
assertEq(bank.balanceOf(alice), 0);
}
vm.expectRevert dice che la chiamata successiva deve essere annullata, e con quale errore. Un errore personalizzato senza argomenti si riconosce dal selettore, PiggyBank.ZeroAmount.selector; uno con argomenti richiede l’errore codificato per intero, che abi.encodeWithSelector costruisce. La documentazione precisa che le altre chiamate a cheatcode tra expectRevert e la chiamata che fallisce vengono ignorate, quindi il vm.prank in mezzo non dà problemi. I test sui fallimenti contano quanto quelli sui successi: test_RevertWhen_NotOwnerPauses è quello che dimostra che il controllo degli accessi funziona.
Test di fuzzing: lascia che sia Foundry a inventare gli input
Una funzione di test con parametri diventa un test di fuzzing. Foundry chiama testFuzz_DepositThenWithdraw con molti valori casuali di amount, 256 esecuzioni per test per impostazione predefinita secondo il riferimento sulla configurazione dei test. Se un input rompe un’asserzione, il test fallisce e forge indica l’input responsabile.
bound(amount, 1, 100 ether) riporta un numero casuale qualsiasi nell’intervallo che ti interessa. L’alternativa, vm.assume(condition), scarta gli input che non corrispondono, e troppi scarti fanno fallire l’esecuzione (il limite predefinito è 65.536); le guide di Foundry stesse preferiscono bound. Per eseguire più casi, aggiungi questo a foundry.toml:
[fuzz]
runs = 1000
Eseguire la suite e leggere i fallimenti
forge test
forge test -vv
forge test --match-test testFuzz_DepositThenWithdraw -vvv
Ogni «v» in più mostra di più. La guida ai test elenca i livelli:
| Opzione | Che cosa mostra forge |
|---|---|
| (nessuna) | Riepilogo: quali test sono riusciti o falliti |
| -vv | I log emessi durante i test |
| -vvv | Le tracce di esecuzione dei test falliti |
| -vvvv | Le tracce di tutti i test, preparazione compresa |
| -vvvvv | Anche le modifiche alla memoria |
| –match-test, –match-contract, –match-path | Esegue solo i test il cui nome, contratto o file corrisponde |
Quando un test fallisce, eseguilo da solo con --match-test e -vvv: la traccia mostra ogni chiamata, chi l’ha fatta, che cosa ha restituito e dove è stata annullata.
Dopo il fuzzing viene il test di invarianti: una funzione che si chiama invariant_... afferma qualcosa che deve restare vero qualunque sia la sequenza di chiamate, per esempio «il saldo in ether della banca non è mai inferiore alla somma dei saldi degli utenti». Foundry chiama allora le funzioni del contratto in ordine casuale (256 esecuzioni di profondità 500 per impostazione predefinita) e controlla l’affermazione dopo ciascuna. La guida ai test di invarianti mostra lo schema; provalo quando i test unitari ti sembrano naturali.
Una blockchain locale con anvil, da interrogare con cast
I test girano nella EVM in memoria di forge, quindi non serve un nodo. Quando vuoi una blockchain che si comporti come una rete vera, avvia anvil in un secondo terminale. Secondo la pagina di riferimento, ascolta su 127.0.0.1:8545 e crea 10 conti di sviluppo con 10.000 ether di prova ciascuno. Poi fagli domande con cast:
anvil
In un altro terminale, con uno degli indirizzi di prova elencati da anvil al posto del segnaposto:
cast balance <address> --ether --rpc-url http://127.0.0.1:8545
Le chiavi dei conti di sviluppo di anvil sono di dominio pubblico: non mandare mai fondi veri a quegli indirizzi. In generale non incollare mai la chiave di un portafoglio vero in un progetto di prova, in un file .env o in un repository Git; se le chiavi sono una novità per te, leggi come funzionano chiave privata e chiave pubblica. La pubblicazione su anvil e poi su una rete di prova pubblica usa gli script di forge, tema di una lezione successiva.
Sicurezza in Solidity: rientranza, controllo degli accessi, tx.origin, overflow
I contratti vengono attaccati perché custodiscono valore e il loro codice è pubblico. La maggior parte delle perdite non nasce dalla rottura della crittografia della catena, ma da una logica del contratto che fa qualcosa che l’autore non si aspettava, come spiega la nostra guida su se una blockchain può essere hackerata. La documentazione di Solidity ha una pagina sulle considerazioni di sicurezza (in inglese) che ogni principiante dovrebbe leggere almeno una volta. Ecco i classici, a cominciare da uno che puoi eseguire.
Rientranza (reentrancy), dimostrata sul tuo computer
Qui sotto trovi una banca scritta male di proposito, un contratto attaccante e un test che dimostra l’attacco. È un esempio da manuale, eseguito solo sulla blockchain di prova di Foundry con indirizzi inventati. Salvalo come test/Reentrancy.t.sol; non pubblicare questo codice da nessuna parte.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
// BAD ON PURPOSE: sends ether before updating the balance.
contract VulnerableBank {
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balanceOf[msg.sender];
require(amount > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
balanceOf[msg.sender] = 0; // too late
}
}
contract Attacker {
VulnerableBank public immutable bank;
constructor(VulnerableBank _bank) {
bank = _bank;
}
function attack() external payable {
bank.deposit{value: msg.value}();
bank.withdraw();
}
// Runs every time the bank sends ether to this contract.
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
}
contract ReentrancyTest is Test {
function test_AttackerDrainsVulnerableBank() public {
VulnerableBank bank = new VulnerableBank();
address victim = makeAddr("victim");
address thief = makeAddr("thief");
vm.deal(victim, 10 ether);
vm.deal(thief, 1 ether);
vm.prank(victim);
bank.deposit{value: 10 ether}();
Attacker attacker = new Attacker(bank);
vm.prank(thief);
attacker.attack{value: 1 ether}();
assertEq(address(bank).balance, 0);
assertEq(address(attacker).balance, 11 ether);
}
}
Segui gli ether. La vittima deposita 10 ether. Il contratto attaccante ne deposita 1, quindi la banca ne custodisce 11 e registra 1 per l’attaccante. Poi chiama withdraw. La banca legge il saldo (1), invia 1 ether e solo dopo azzererebbe il saldo.
Ma inviare ether a un contratto fa girare la sua funzione receive, e la call di basso livello trasmette quasi tutto il gas rimasto. La receive dell’attaccante richiama withdraw. La banca ha ancora l’attaccante a registro con 1 ether, perché la riga che lo azzera non è ancora stata eseguita, e paga di nuovo. In tutto la banca paga undici volte, finché è vuota. Solo allora ogni chiamata annidata finisce e porta il saldo a zero, troppo tardi.
Le due asserzioni verificano il risultato: la banca finisce con 0 e l’attaccante con 11 ether, i 10 della vittima più il proprio 1. La pagina di sicurezza di Solidity dedica alla rientranza una sezione tutta sua, tra le prime delle sue insidie e subito dopo informazioni private e casualità, con un esempio vulnerabile molto vicino a questo.

La correzione è l’ordine di righe che hai già scritto in PiggyBank.withdraw: aggiornare il saldo prima della chiamata esterna. Quando l’attaccante rientra, il suo saldo è già zero e il controllo lo ferma. Quell’ordine è il modello checks-effects-interactions, che la documentazione di Solidity raccomanda nell’elenco dei consigli generali. Un secondo livello è una protezione che rifiuta del tutto la rientranza. ReentrancyGuard di OpenZeppelin la offre come modifier nonReentrant:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract FixedBank is ReentrancyGuard {
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw() external nonReentrant {
uint256 amount = balanceOf[msg.sender];
require(amount > 0, "nothing to withdraw");
balanceOf[msg.sender] = 0; // effect before interaction
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
}
Usali entrambi: l’ordine giusto è la correzione vera, la protezione ti tutela il giorno in cui qualcuno aggiunge una funzione nell’ordine sbagliato. OpenZeppelin fornisce anche ReentrancyGuardTransient, una variante più economica che si appoggia alla memoria transitoria, per le catene che la supportano.
Controllo degli accessi: chi può chiamare che cosa
PiggyBank controlla il proprietario a mano con un modifier, il che va bene per una sola regola. Nei progetti veri usa Ownable di OpenZeppelin. Dalla versione 5 il suo costruttore riceve il proprietario iniziale come argomento, così dichiari sempre chi possiede il contratto:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract OwnedSwitch is Ownable {
bool public paused;
constructor() Ownable(msg.sender) {}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
}
Per più ruoli (un conto che mette in pausa, un altro che emette nuovi token) la stessa libreria ha AccessControl. Qualunque cosa usi, scrivi il test negativo, come fa test_RevertWhen_NotOwnerPauses. Una funzione senza il suo controllo basta per consegnare a chiunque il comando di un contratto, e un test sul percorso felice non lo mostrerà mai.
Non autorizzare mai con tx.origin
tx.origin è il conto che ha firmato la transazione, anche quando la chiamata è arrivata al tuo contratto passando per altri contratti; msg.sender è il chiamante immediato. Se il tuo contratto portafoglio controlla tx.origin == owner, un contratto malevolo che ti convincono a chiamare può a sua volta chiamare il tuo portafoglio, e il controllo passa. La documentazione di Solidity lo dice in cinque parole, in inglese: «Never use tx.origin for authorization.» Usa msg.sender.
Interi: controlli di overflow dalla 0.8.0 e ciò che non coprono
Dalla versione 0.8.0 di Solidity le operazioni aritmetiche vengono annullate in caso di overflow e underflow, come indica la pagina sulle modifiche incompatibili della 0.8.0. Sottrarre 2 da un saldo di 1 ora annulla la chiamata invece di tornare a capo su un numero enorme, il che ha eliminato un’intera famiglia di bug presenti nel codice più vecchio. Un blocco unchecked { ... } disattiva i controlli per risparmiare gas, e dovresti vederlo solo dove il codice dimostra che l’operazione non può andare in overflow.
I controlli non ti salvano dagli errori di logica (una somma corretta dei numeri sbagliati), dagli arrotondamenti nelle divisioni (Solidity non ha decimali, e 7 / 2 fa 3) né dalle conversioni esplicite a tipi più piccoli: convertire un grande uint256 in un uint96 conserva solo i bit bassi, senza annullare la chiamata.
Altre cinque insidie da conoscere per nome
- I cicli su una lista che gli utenti possono allungare possono un giorno finire il gas e bloccare la funzione per sempre. Evita i cicli senza limite sulla memoria.
- Tutto ciò che sta in memoria è leggibile da chiunque, anche le variabili private. Non conservare mai in un contratto una password o una risposta segreta.
- I dati del blocco, come l’ora, non sono una fonte di casualità: chi produce i blocchi può influenzarli. Usa un servizio di casualità verificabile quando il denaro dipende dal caso.
- Cancellare una struttura che contiene un mapping non svuota il mapping. La documentazione di Solidity lo elenca sotto «clearing mappings».
- Un contratto può ricevere ether senza che nessuna delle sue funzioni giri, quindi non dare mai per scontato che il saldo in ether del contratto coincida con quello della tua contabilità interna.
Usare librerie verificate e lanciare Slither
Non riscrivere da te token, gestione dei proprietari o protezioni. OpenZeppelin Contracts (documentazione in inglese) è una libreria molto usata di componenti sottoposti ad audit; la sua ultima versione stabile è la v5.7.0, pubblicata il 29 luglio 2026. Installa una versione con etichetta e mappa il percorso di importazione; la documentazione sconsiglia di installare dal ramo master.
forge install OpenZeppelin/[email protected]
echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt
Poi lancia un analizzatore statico. Slither, sviluppato in modo aperto sotto l’organizzazione crytic su GitHub, legge il codice senza eseguirlo e segnala schemi di bug noti, rientranza compresa. La sua ultima versione è la 0.11.6 (28 luglio 2026) e richiede Python 3.10 o successivo. In un progetto Foundry assicurati prima che forge build passi, poi:
python3 -m pip install slither-analyzer
slither .
Il suo elenco di rilevatori comprende diversi controlli sulla rientranza. Leggi ogni segnalazione, correggila o annota perché è innocua, e aspettati dei falsi positivi: il punto è guardarle una per una.
Ciò che test e strumenti non vedono
Una suite di test tutta verde dimostra i casi a cui hai pensato. I team esperti aggiungono audit esterni, concorsi di audit pubblici e bug bounty prima che entri denaro vero; la lezione precedente di questo percorso spiega come funzionano quei concorsi e perché sono un buon posto per imparare. Per i tuoi progetti, tieni piccolo l’importo che un contratto può custodire finché il codice non è stato letto da qualcuno che non sei tu.
Quale versione di Solidity usare a ottobre 2026
Al 27 ottobre 2026 l’ultima versione di Solidity è la 0.8.38, pubblicata il 22 ottobre 2026, secondo le release su GitHub del progetto. L’annuncio della versione (in inglese) la descrive come la correzione di un «low-severity bug in the Yul optimizer»: quando una variabile usata per calcolare un indirizzo di memoria o di storage veniva riassegnata, l’ottimizzatore poteva conservare conoscenze superate e sostituire una lettura con un valore vecchio o eliminare una scrittura necessaria. Riguarda il codice compilato con l’ottimizzatore attivo, dalla 0.8.19 alla 0.8.37: con la pipeline via-IR può essere interessato qualsiasi codice, con la pipeline predefinita soprattutto le funzioni di supporto generate dal compilatore e certe parti di assembly inline.
| Strumento | Ultima versione | Data di uscita |
|---|---|---|
| Compilatore Solidity | 0.8.38 | 22 ottobre 2026 |
| Solidity, precedente | 0.8.37 | 10 settembre 2026 |
| Foundry (stabile) | v1.8.6 | 21 ottobre 2026 |
| forge-std | v1.17.0 | 30 settembre 2026 |
| OpenZeppelin Contracts | v5.7.0 | 29 luglio 2026 |
| Slither | 0.11.6 | 28 luglio 2026 |
Versioni come risultano sulla pagina delle release GitHub di ciascun progetto, controllate il 27 ottobre 2026.
In pratica: compila con l’ultima versione 0.8, tieni un intervallo di pragma il cui limite inferiore hai davvero provato e, quando pubblichi qualcosa di reale, fissa una versione esatta del compilatore in foundry.toml perché il bytecode sia riproducibile e verificabile. Quando un tutorial usa una versione molto più vecchia, come la 0.6 o la 0.7, la sintassi può ancora compilare ma le regole di sicurezza sono diverse, a cominciare dai controlli di overflow; leggi l’introduzione a Solidity per la 0.8.38 (in inglese) invece di un vecchio post di blog.
Cosa costruire dopo il primo contratto
Hai ora il ciclo che ogni sviluppatore Solidity ripete, e il modo migliore per continuare a imparare è rifarlo più volte: scrivere, compilare, testare successi e fallimenti, fare fuzzing, leggere le tracce, lanciare l’analizzatore. Esercitati su delle varianti prima di andare avanti. Aggiungi un limite di prelievo giornaliero. Fai in modo che il proprietario non possa prelevare nulla ma possa mettere tutto in pausa. Scrivi un test di invarianti per cui la banca custodisce sempre almeno la somma dei saldi. Ogni modifica romperà un test, ed è proprio questo il punto.
Due lezioni successive di questo corso ripartono da qui: una raccoglie libri, documentazione e certificazioni che meritano il tuo tempo, un’altra collega un contratto a una pagina web e lo pubblica su una rete di prova pubblica. Se vuoi rivedere il piano mese per mese, è nella nostra guida per diventare sviluppatore blockchain, e la guida al lavoro blockchain mostra i ruoli in cui queste competenze servono.
Un’ultima precisazione per chi legge dall’Italia. Questa lezione parla di codice, non di norme. Un contratto che raccoglie denaro degli utenti o emette token può sollevare questioni regolamentari, che sono un altro studio; un progetto didattico su una blockchain locale non ne solleva nessuna.



