Hai mai visto uno smart contract andare in fumo per una semplice rientranza? Noi sì. Un cliente aveva un pool DeFi con un bug di reentrancy: in pochi secondi perso il 40% del liquidity pool. Era un contratto scritto da un team 'esperto', ma senza pattern di sicurezza. Ecco perché abbiamo deciso di scrivere questa guida. Non vogliamo che tu perda soldi (e credibilità).
Questa guida parte da zero: variabili, funzioni, eventi. Poi entra nei pattern di sicurezza che ogni sviluppatore Solidity deve conoscere. Non è un corso di teoria — è quello che noi di Meteora Web abbiamo applicato in progetti reali, dopo anni di contabilità e gestione ERP: ogni riga di codice ha un costo, ogni bug può bruciare capitale. Parliamo di roba concreta.
Come si dichiarano variabili, funzioni ed eventi in Solidity?
Variabili di stato e tipi base
In Solidity le variabili di stato sono memorizzate sulla blockchain. Ogni scrittura costa gas. Ecco un esempio minimo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Base {
// variabile di stato pubblica – genera automaticamente un getter
uint256 public count;
// variabile privata – accessibile solo all'interno del contratto
address private owner;
// variabile immutabile – impostata una sola volta al deploy
uint256 public immutable MAX_SUPPLY = 1000;
}
Attenzione: usa uint256 invece di uint per chiarezza. Evita var implicito (deprecato). I tipi address e address payable sono diversi: solo il secondo può ricevere ether.
Sponsored Protocol
Funzioni: visibilità e modificatori
Le funzioni in Solidity hanno quattro livelli di visibilità: public, internal, external, private. Attenzione a public vs external: external costa meno gas per chiamate esterne perché i parametri non vengono copiati in memoria.
contract Funzioni {
uint256 public data;
// funzione esterna – chiamabile solo dall'esterno
function setData(uint256 _newData) external {
data = _newData;
}
// funzione pubblica – chiamabile anche internamente
function getData() public view returns (uint256) {
return data;
}
// funzione interna – solo contratti derivati
function _internalHelper() internal pure returns (string memory) {
return "solo figli";
}
}
Modificatori di funzione: view (non modifica stato), pure (non legge né scrive stato), payable (può ricevere ether). Usali sempre per documentare e ottimizzare il gas.
Eventi: tracciare le modifiche sulla chain
Gli eventi permettono di loggare azioni in modo efficiente (più economico delle variabili). I client (frontend, DApp) li ascoltano per reagire.
contract Events {
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address _to, uint256 _value) external {
// logica di trasferimento
emit Transfer(msg.sender, _to, _value);
}
}
I parametri indexed (massimo 3) permettono di filtrare gli eventi. Gli eventi non sono accessibili on-chain, ma sono fondamentali per l'auditing.
Sponsored Protocol
Quali sono i pattern di sicurezza indispensabili per uno smart contract?
Dopo anni a gestire vulnerabilità (anche in progetti che ci sono arrivati per riparazioni), abbiamo tre pattern che consideriamo obbligatori.
1. Checks-Effects-Interactions (CEI)
È il pattern numero uno: prima controlli le condizioni (checks), poi modifichi lo stato (effects), infine interagisci con contratti esterni (interactions). Previene il reentrancy.
contract CEIPattern {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "no balance");
// effects
balances[msg.sender] = 0;
// interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "transfer failed");
}
}
Vedi? Abbiamo azzerato il saldo prima di inviare ether. Se il mittente è un contratto malevolo e richiama withdraw nel suo receive, il saldo è già zero. Non può rientrare.
2. Guard Check con modificatori personalizzati
Ripetere require(owner == msg.sender) ovunque è noioso e rischioso. Crea modificatori (modifier) riutilizzabili.
contract GuardCheck {
address public owner;
modifier onlyOwner() {
require(owner == msg.sender, "not owner");
_;
}
modifier nonZeroAddress(address _addr) {
require(_addr != address(0), "zero address");
_;
}
function setOwner(address _newOwner) external onlyOwner nonZeroAddress(_newOwner) {
owner = _newOwner;
}
}
Regola operativa: usa _ per rappresentare il corpo della funzione. I modificatori si concatenano: vengono eseguiti in ordine di dichiarazione.
Sponsored Protocol
3. Pausabilità e emergency stop
Quando un bug viene scoperto, devi poter fermare il contratto. Il pattern più diffuso è ereditare da OpenZeppelin's Pausable.
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyContract is Pausable {
function doSomething() external whenNotPaused {
// logica
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
Usa whenNotPaused per le funzioni critiche. Il proprietario può mettere in pausa il contratto in emergenza. Ma attenzione: l'owner diventa un punto di centralizzazione. Valuta se usare un multisig per la pausa.
Come prevenire attacchi comuni come il reentrancy o l'overflow?
Reentrancy: oltre al pattern CEI
Il reentrancy classico è risolto da CEI, ma esistono varianti come il reentrancy cross-funzione. Usa un blocco di reentrancy (mutex) per sicurezza extra:
Sponsored Protocol
contract ReentrancyGuard {
bool private locked;
modifier noReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function withdraw() external noReentrant {
// logica
}
}
OpenZeppelin fornisce ReentrancyGuard già testato. Noi lo consigliamo sempre su contratti che gestiscono fondi.
Overflow e underflow
Prima di Solidity 0.8, gli overflow causavano wraparound. Ora di default il compilatore controlla gli overflow e reverta. Ma attenzione: in blocchi unchecked i controlli sono disattivati (utili per risparmiare gas).
function safeMath() external pure {
// in Solidity 0.8+ questo reverta se overflow
uint256 a = type(uint256).max;
uint256 b = 1;
// revert: a + b overflow
// uint256 c = a + b;
// unchecked permette overflow ma attenzione
unchecked {
uint256 c = a + b; // c = 0 (wraparound)
}
}
Regola: usa unchecked solo dove sei sicuro che l'overflow non possa accadere (es. incrementi che sai non supereranno un limite). Altrimenti lascia i controlli automatici.
Access control: non solo owner
Il pattern onlyOwner è semplice, ma spesso insufficiente. Per ruoli più granulari usa OpenZeppelin's AccessControl.
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBased is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint() external onlyRole(MINTER_ROLE) {
// logica
}
}
Questo permette di delegare la gestione a più account, revocare ruoli, e avere un audit trail on-chain dei permessi. Molto più robusto di un singolo owner.
Sponsored Protocol
Cosa fare adesso
Non fermarti alla lettura. Ecco tre azioni concrete:
- Scrivi un contratto test su Remix o Hardhat. Implementa i tre pattern (CEI, modificatori, pausa) e verifica con un attacco di reentrancy simulato. Il nostro consiglio: usa il tutorial ufficiale di Hardhat per ambienti locali.
- Integra OpenZeppelin Contracts nel tuo progetto. Non reinventare la ruota: usa le librerie ufficiali per ReentrancyGuard, AccessControl, Pausable, SafeERC20 (per token). Riduci il rischio di bug a zero.
- Esegui un audit preventivo prima del deploy. Noi, di Meteora Web, abbiamo visto contratti con vulnerabilità che un semplice static analyzer (Slither) avrebbe trovato in 10 secondi. Fallo eseguire su ogni contratto.
Ricorda: ogni variabile non inizializzata, ogni call non verificato, ogni owner senza multisig è un potenziale incidente. Tratta il codice come denaro — perché su blockchain lo è davvero.
Se vuoi approfondire, leggi la nostra Pillar su Blockchain e Web3.