🛡️ Sécurité & Audit de Smart Contracts
Analyse dense : Vecteurs d'attaque, Prévention (OpenZeppelin), Outils (Slither, Mythril) et Tests (Fuzzing).
Vecteurs d'Attaque (Les Risques)
"Code is Law" : un bug est fatal. Analyse des failles : Réentrance, Over/Underflow, `tx.origin`, appels non vérifiés...
RéentranceThe DAO HackOverflowPrévention : OpenZeppelin
Ne réinventez pas la roue. La bibliothèque de contrats (ERC20, ERC721, Ownable, ReentrancyGuard) auditée et standardisée.
OpenZeppelinERC721Ownable.solAnalyse Statique (SAST) : Slither
Le "linter" de sécurité. Analyse du code source *avant* déploiement pour trouver les failles "low-hanging fruit". (Trail of Bits).
SlitherSASTCI/CDExécution Symbolique : Mythril
Le "détective" mathématique. Exploration de *tous* les chemins d'exécution possibles pour trouver des états vulnérables.
MythrilSymbolic ExecutionAnalyse Dynamique (DAST) : Fuzzing
Le "chaos monkey". Bombarder le contrat avec des millions d'inputs aléatoires pour briser les "invariants". (Echidna, Foundry).
FuzzingEchidnaInvariantsLe Processus d'Audit Manuel
Pourquoi les outils ne suffisent pas. L'audit humain : analyse de la logique business, des incitations économiques et des failles subtiles.
AuditManual ReviewLogique BusinessPost-Déploiement : Monitoring
La sécurité "runtime". Surveiller le Mempool et les blocs pour détecter les menaces en temps réel (ex: Forta Network).
MonitoringFortaRuntimeSynthèse : Le Secure SDLC
Le "Software Development Lifecycle" de la sécurité Web3. De la conception (OZ) au déploiement (Audit) et à la production (Forta).
SDLCBest PracticesL'immuabilité de la blockchain signifie que les hacks sont instantanés, irréversibles et catastrophiques. Comprendre les failles historiques est la première étape pour les prévenir.
1. Réentrance (L'Attaque "The DAO")
La Faille : La vulnérabilité la plus célèbre. Elle se produit lorsqu'un contrat (A) appelle un contrat externe (B), et que ce contrat B (l'attaquant) peut *rappeler* le contrat A *avant* que la première exécution ne soit terminée, permettant de contourner les vérifications.
Code Vulnérable (Simplifié) :
mapping(address => uint) public userBalances;
function withdraw() public {
// 1. Vérification
uint amount = userBalances[msg.sender];
require(amount > 0);
// 2. Interaction (L'ERREUR EST ICI)
// Envoi de l'ETH à l'attaquant *avant* de mettre à jour le solde
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
// 3. Effet (Mise à jour de l'état - TROP TARD)
userBalances[msg.sender] = 0;
}L'Attaque :
- L'attaquant (un smart contract) appelle `withdraw()`.
- Étape 2 : Le contrat lui envoie son ETH. Les contrats attaquants ont une **`fallback() function`** qui est déclenchée lorsqu'ils reçoivent de l'ETH.
- Dans sa `fallback() function`, l'attaquant **rappelle `withdraw()`** (ré-entre dans le contrat).
- Étape 1 (Appel 2) : Le contrat vérifie le solde. L'Étape 3 (
userBalances = 0) n'ayant *pas encore* eu lieu, le solde est *toujours* plein ! - Étape 2 (Appel 2) : Le contrat envoie *à nouveau* l'ETH.
- ... L'attaquant répète cela jusqu'à ce que le contrat soit vidé de ses fonds.
La Solution (Pattern "Checks-Effects-Interactions") :
function secureWithdraw() public {
// 1. Checks (Vérifications)
uint amount = userBalances[msg.sender];
require(amount > 0);
// 2. Effects (Mise à jour de l'état IMMÉDIATE)
userBalances[msg.sender] = 0;
// 3. Interactions (Appel externe EN DERNIER)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
userBalances[msg.sender] = 0;
}Autre Solution : Utiliser le modificateur nonReentrant d'OpenZeppelin (voir 2.1).
2. Arithmetic Overflow / Underflow (Solidity < 0.8.0)
La Faille : Avant Solidity 0.8.0, les opérations arithmétiques n'étaient pas vérifiées. Un `uint8` (entier de 8 bits) va de 0 à 255.
uint8 myValue = 255; myValue = myValue + 1; // En Solidity 0.7.0, myValue == 0 (Overflow) uint8 myValue = 0; myValue = myValue - 1; // En Solidity 0.7.0, myValue == 255 (Underflow)
L'Attaque : Un attaquant pouvait exploiter cela dans un contrat de token pour (par exemple) "underflower" son solde de 0 à un nombre immense, ou faire "overflow" la `totalSupply`.
La Solution :
- Historique (Pré-0.8) : Utiliser la bibliothèque
SafeMath.sold'OpenZeppelin, qui remplaçait `a + b` par `a.add(b)` (une fonction qui vérifiait l'overflow). - Moderne (Post-0.8) : Le compilateur Solidity (versions 0.8.0 et supérieures) **inclut nativement** ces vérifications. Toute opération `+`, `-`, `*` qui overflow/underflow provoquera un `revert()` (annulation de la transaction). Cette faille est donc "résolue" si vous utilisez un compilateur moderne.
3. Authentification par `tx.origin`
La Faille : Solidity a deux variables globales pour l'identité : `msg.sender` et `tx.origin`.
msg.sender(Le Bon) : L'appelant **direct** (peut être un utilisateur EOA ou un autre Contrat).tx.origin(Le Mauvais) : L'**initiateur** de la transaction (toujours un utilisateur EOA).
Code Vulnérable (Portefeuille) :
contract MyWallet {
address public owner;
// Seul le propriétaire doit pouvoir transférer
function transfer(address to, uint amount) public {
// ERREUR : Authentification avec tx.origin
require(tx.origin == owner, "Not owner!");
payable(to).transfer(amount);
}
}L'Attaque (Phishing) :
- Un hacker déploie un `Contract_Attacker`.
- Il vous (l'
owner) appâte pour que vous appeliez une fonction (ex: `claimAirdrop()`) sur *son* contrat `Contract_Attacker`. - La fonction `claimAirdrop()` du hacker **appelle** la fonction `transfer()` de *votre* `MyWallet`.
- Analyse de l'Appel :
- Contrat (MyWallet) : Exécute `transfer()`.
- Qui est
msg.sender? -> L'adresse du `Contract_Attacker`. - Qui est
tx.origin? -> **Vous** (l'owner), car vous avez initié la chaîne d'appels.
- La vérification `require(tx.origin == owner)` **réussit !** Le contrat du hacker vous force à vider votre propre portefeuille.
La Solution : NE JAMAIS UTILISER `tx.origin` POUR L'AUTHENTIFICATION. Toujours utiliser msg.sender.
4. Appels Externes Non Vérifiés
La Faille : En Solidity, les appels externes bas niveau (`.call()`, `.delegatecall()`, `.send()`) ne provoquent pas de `revert()` s'ils échouent. Ils renvoient simplement un `booléen` (succès/échec).
// CODE VULNÉRABLE
function myFunc(address target, uint amount) public {
// ... logique ...
// Le 'send' peut échouer (ex: le destinataire n'accepte pas l'ETH)
// Mais le code NE S'ARRÊTE PAS.
target.send(amount);
// La logique (ex: `myState = true`) continue comme si
// le transfert avait réussi.
}La Solution : Toujours **vérifier** le booléen de retour.
(bool success, ) = target.call{value: amount}("");
require(success, "External call failed");5. Manipulation d'Oracle (Flash Loan Attacks)
La Faille : Se produit lorsqu'un protocole DeFi (ex: un prêteur) se base sur une source de prix (un "Oracle") qui peut être facilement manipulée.
Source de Prix Vulnérable : Le prix spot d'un pool Uniswap V2 (`reserve0 / reserve1`).
L'Attaque (Flash Loan) :
- Étape 1 (Atomique) : L'attaquant emprunte 100M$ d'USDC (un "Flash Loan" d'Aave).
- Étape 2 : Il utilise ces 100M$ pour faire un swap massif sur le pool Uniswap ETH/USDC (il achète l'ETH).
- Étape 3 : Le pool est maintenant déséquilibré (plein d'USDC, vide d'ETH). Le prix spot `ETH/USDC` (que le protocole vulnérable utilise) est *artificiellement* très élevé.
- Étape 4 : L'attaquant utilise son ETH (maintenant "surévalué") comme collatéral sur le protocole vulnérable pour emprunter *tous* ses actifs (ex: vider le protocole de ses DAI).
- Étape 5 : L'attaquant re-swap l'ETH contre des USDC (le prix revient à la normale).
- Étape 6 : L'attaquant rembourse le Flash Loan (100M$) et repart avec le butin (les DAI).
La Solution : Ne **jamais** utiliser un prix spot (instantané) comme oracle. Utiliser des oracles robustes (ex: **Chainlink**, qui agrège des dizaines de sources) ou des oracles **TWAP** (Time-Weighted Average Price, ex: Uniswap V3) qui sont résistants à la manipulation sur un seul bloc.
La première règle de la sécurité en cryptographie est : **"Ne roulez pas votre propre crypto" (Don't roll your own crypto)**. La première règle de la sécurité des smart contracts est : **"N'écrivez pas un ERC20 de zéro."**
**OpenZeppelin** est une entreprise de sécurité (qui fait aussi des audits) qui maintient la **bibliothèque de smart contracts la plus utilisée et la plus auditée au monde**. C'est une collection de briques de base (implémentations des EIPs, utilitaires de sécurité) que vous importez dans votre projet (`npm install @openzeppelin/contracts`).
1. Implémentations des Standards (Tokens)
Au lieu de réécrire (et de mal implémenter) les EIPs, vous héritez simplement des contrats OZ.
// Création d'un Token ERC-20 standard
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("My Token", "MTK") {
// Le constructeur Ownable définit 'msg.sender' comme propriétaire
}
// Fonction de "mint" sécurisée,
// que seul le propriétaire (défini par Ownable) peut appeler.
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount); // _mint est la fonction interne sécurisée d'OZ
}
}ERC20.sol: Implémentation complète d'un token fongible (`transfer`, `approve`, `balanceOf`...).ERC721.sol: Implémentation complète d'un NFT (`_safeMint`, `ownerOf`, `setApprovalForAll`...).ERC1155.sol: Implémentation du standard multi-token.
2. Contrôle d'Accès (Access Control)
Comment gérer les "permissions" dans un contrat ? (Qui a le droit de minter ? Qui peut changer les frais ?)
Ownable.sol (Le plus simple)
Le contrat a **un seul propriétaire** (owner). Généralement, l'adresse qui l'a déployé.
- Fonctions :
transferOwnership(),renounceOwnership(). - Modificateur :
onlyOwner. Vous l'ajoutez à vos fonctions pour les protéger.
function withdrawFees() public onlyOwner {
// Seul le propriétaire peut exécuter ce code
}
AccessControl.sol (Plus granulaire)
Permet de créer des **Rôles** (comme RBAC). C'est beaucoup plus flexible pour les projets complexes.
- Rôles (bytes32) :
DEFAULT_ADMIN_ROLE,MINTER_ROLE,PAUSER_ROLE... - Fonctions :
grantRole(),revokeRole(),hasRole(). - Modificateur :
onlyRole(MINTER_ROLE).
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
// Le constructeur donne le rôle "Minter" à celui qui déploie
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
3. Utilitaires de Sécurité Critiques
ReentrancyGuard.sol
La solution directe au "DAO Hack" (voir 1.1).
Mécanisme : Fournit un modificateur nonReentrant. Ce modificateur utilise une variable d'état (un "mutex" ou "verrou") pour s'assurer qu'une fonction ne peut pas être rappelée avant d'être terminée.
// 1. Importer et hériter
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyBank is ReentrancyGuard {
mapping(address => uint) public userBalances;
// 2. Ajouter le modificateur
function secureWithdraw() public nonReentrant {
uint amount = userBalances[msg.sender];
require(amount > 0);
userBalances[msg.sender] = 0; // (Checks-Effects-Interactions est mieux...)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
}
}
// Si l'attaquant tente de rappeler "secureWithdraw" depuis sa fallback,
// le modificateur "nonReentrant" verra que le verrou est déjà posé
// et fera "revert" la transaction.
SafeMath.sol (Legacy)
La bibliothèque qui a sauvé l'écosystème des Overflows avant Solidity 0.8.0. Elle enveloppait les opérations (`a.add(b)`). **Elle n'est plus nécessaire si vous utilisez `pragma solidity ^0.8.0;`**.
Pausable.sol
Fournit un "interrupteur d'urgence". Permet à un rôle (ex: `PAUSER_ROLE`) de "mettre en pause" le contrat en cas de découverte d'un hack. Fournit les modificateurs whenNotPaused et whenPaused.
**Slither** est l'outil d'**Analyse Statique (SAST)** le plus populaire pour Solidity. Développé par la firme d'audit *Trail of Bits*, c'est un "linter" de sécurité avancé.
SAST (Static Analysis Security Testing) signifie qu'il analyse le **code source** (le texte) pour trouver des *schémas* de vulnérabilités connues, **sans exécuter le code**.
Fonctionnement de Slither
Slither est beaucoup plus puissant qu'un linter (comme Solhint) car il ne se contente pas de regarder la syntaxe. Il comprend la logique du contrat :
- 1. Parsing : Il lit le code Solidity.
- 2. SlithIR : Il convertit le code en une **Représentation Intermédiaire (IR)** appelée **SlithIR**. C'est une version simplifiée du code, plus facile à analyser pour une machine.
- 3. Détecteurs : Il exécute une suite de **plus de 100 "détecteurs"** (plugins) sur ce code IR.
- 4. Rapport : Il génère un rapport des failles potentielles, classées par sévérité.
Ce qu'il trouve (Les "Low-Hanging Fruits")
Slither est excellent pour trouver les erreurs "classiques" que l'œil humain peut manquer :
slither-check-reentrancy: Détecte les schémas de réentrance (interaction *avant* effet).slither-check-tx-origin: Détecte l'utilisation de `tx.origin` pour l'authentification.unprotected-upgrade: Vérifie si un contrat proxy a une fonction d'initialisation non protégée.solc-version: Alerte si une version de compilateur obsolète (ex: 0.7.0) est utilisée.unused-state: Variables d'état déclarées mais jamais utilisées (gaspillage de gas).shadowing-state: Une variable locale qui a le même nom qu'une variable d'état (source de bugs).
Exemple d'Utilisation (CI/CD)
Slither est un outil en ligne de commande (Python). On l'installe (`pip install slither-analyzer`) et on le lance à la racine du projet (Hardhat/Foundry).
$ slither . (Slither analyse tous les contrats du projet)
Exemple de Rapport Slither
VulnerableWallet.sol:8: MyWallet.transfer(address,uint256)
- [High] Use of tx.origin
VulnerableWallet.sol:11: require(tx.origin == owner, "Not owner!")
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#txorigin
VulnerableBank.sol:6: VulnerableBank.withdraw()
- [High] Reentrancy in VulnerableBank.withdraw()
VulnerableBank.sol:12: (bool success, ) = msg.sender.call{value: amount}("")
VulnerableBank.sol:15: userBalances[msg.sender] = 0
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy
...
2 vulnerability(ies) found (2 High, 0 Medium, 0 Low)
Limites
Slither est **bruyant**. Il génère beaucoup de "faux positifs". Il ne comprend pas l'**intention** (la logique business). Il peut vous dire "cette fonction est publique", mais il ne peut pas vous dire si elle *devrait* l'être.
C'est un outil **indispensable** pour un audit, mais il ne **remplace pas** un audit. Il est parfait pour être intégré dans un pipeline de CI/CD (ex: Github Actions) pour rejeter un commit qui introduit une faille évidente.
**Mythril** (ou MythX) est un outil de sécurité (maintenu par ConsenSys) qui utilise une technique différente et plus avancée : l'**Exécution Symbolique** (ou "Analyse Dynamique Symbolique").
Là où Slither (Statique) lit le code comme un "livre", Mythril (Symbolique) tente d'explorer *tous les chemins d'exécution possibles* comme un "labyrinthe".
Fonctionnement : "Que se passerait-il si... ?"
Au lieu d'exécuter le code avec des valeurs concrètes (ex: `amount = 100`), l'Exécution Symbolique exécute le code avec des **variables symboliques** (des inconnues mathématiques).
Exemple :
function myFunc(uint a) {
if (a > 10) {
// Path 1
} else {
// Path 2
}
}
Mythril exécute `myFunc(X)` (où X est une variable symbolique).
- Il arrive au `if` et voit une "bifurcation" (un "fork" d'état).
- Chemin 1 : Il explore ce chemin et ajoute une **contrainte** : `(X > 10)`.
- Chemin 2 : Il explore cet autre chemin et ajoute une contrainte : `(X <= 10)`.
Résolution de Contraintes (Constraint Solving)
Mythril explore ainsi l'arbre de tous les états possibles. Son objectif est de répondre à la question : "Existe-t-il un chemin, et un ensemble d'inputs (de contraintes), qui mènent à un état *dangereux* ?"
Un "état dangereux" peut être :
- `selfdestruct(attacker_address)`
- Une violation d'un `assert()` (ex: `assert(totalSupply <= 1000)`)
- Un `SSTORE` (écriture) sur une variable d'état critique (ex: `owner`) par un non-propriétaire.
Si Mythril trouve un chemin (ex: `Path 1 -> Path 7 -> Path 22`) qui mène à un état dangereux, il utilise un "Solver" (Z3) pour résoudre les contraintes (`X > 10` ET `Y == 5` ET `Z < X`). S'il trouve une solution, il a **prouvé** qu'il existe un exploit, et il peut même vous **donner les inputs (la transaction)** pour le déclencher.
Avantages & Inconvénients
Avantages :
- Trouve des failles logiques profondes : Peut trouver des bugs complexes qu'un humain ou Slither manqueraient, car il explore des "edge cases" (cas limites) improbables.
- Preuve d'Exploit : Ne se contente pas de dire "c'est peut-être une faille" (comme Slither), il dit "c'est une faille, et voici comment l'exécuter".
Inconvénients :
- Explosion d'États (State Explosion) : Le nombre de chemins dans un contrat complexe (ex: Uniswap V3) est astronomique. Mythril ne peut pas (par défaut) explorer 100% des chemins. Il doit s'arrêter après un certain temps (timeout) ou une certaine profondeur.
- Boucles (Loops) : L'exécution symbolique gère très mal les boucles dont la longueur dépend d'une variable (`for(i=0; i < X; i++)`), car cela crée une explosion d'états infinie.
- Complexité : Beaucoup plus lent et plus complexe à exécuter que Slither.
Exemple d'Utilisation (CLI)
$ myth analyze MyContract.sol (Mythril analyse le bytecode compilé) ==== Unchecked CALL Return Value ==== SWC ID: 104 Severity: Low Contract: MyContract Function name: myFunc(address,uint256) PC address: 435 Estimated Gas Usage: 630 - 1030 A call to a user-supplied address is executed. The return value of the call is not checked. ...
Le **Fuzzing** (ou "Tests Stochastiques") est une technique d'**Analyse Dynamique (DAST)**. Au lieu de lire le code (Statique) ou de l'explorer (Symbolique), le Fuzzing **exécute réellement** le code, mais en lui envoyant des **millions d'inputs aléatoires et inattendus** pour essayer de le faire "crasher" ou de violer ses règles.
Dans le Web3, le Fuzzing est plus spécifiquement du **Property-Based Testing** (Test Basé sur les Propriétés).
Tests Unitaires vs. Tests Basés sur les Propriétés
- Test Unitaire (Classique) : Vous écrivez UN scénario. "SI Alice (solde 100) transfère 10 à Bob, ALORS je m'attends à ce que le solde d'Alice soit 90." C'est **spécifique**.
- Property-Based Testing (Fuzzing) : Vous écrivez UNE **règle générale** (une "Propriété" ou "Invariant") qui doit être **TOUJOURS VRAIE**, peu importe ce qui se passe.
Exemples de Propriétés (Invariants) pour un Token ERC20 :
- Invariant 1 (Conservation) :
(totalSupply == old(totalSupply)) || (totalSupply == old(totalSupply) + mintAmount). (La masse monétaire totale ne doit jamais changer, sauf si `mint` ou `burn` est appelé). - Invariant 2 (Solde) :
balanceOf(user) <= totalSupply. (Aucun utilisateur ne peut avoir un solde supérieur à l'offre totale). - Invariant 3 (Transfert) :
balanceOf(sender) + balanceOf(receiver) == old(balanceOf(sender)) + old(balanceOf(receiver)). (Un `transfer` ne doit ni créer ni détruire de tokens, juste les déplacer).
Outils : Echidna (Trail of Bits) & Foundry Fuzzing
Le "Fuzzer" est un outil qui va lire ces propriétés, puis bombarder votre contrat avec des **séquences de transactions aléatoires** pendant des heures, en essayant activement de **falsifier** (briser) un de vos invariants.
Exemple (Echidna)
Vous écrivez vos tests... dans un contrat Solidity !
// MyTokenTest.sol
import "./MyToken.sol";
contract MyTokenTest {
MyToken token;
// 1. Le setup (lance une nouvelle instance)
constructor() {
token = new MyToken(1000000); // 1M de supply initiale
}
// 2. La Propriété (doit toujours être vraie)
// Echidna va appeler cette fonction des millions de fois
// après avoir exécuté des séquences aléatoires de 'transfer', 'approve', etc.
function echidna_test_total_supply_is_stable() public view returns (bool) {
// L'invariant (simplifié) :
// La totalSupply doit TOUJOURS être égale à 1 million
// (car notre contrat n'a pas de fonction 'mint' ou 'burn')
return (token.totalSupply() == 1000000);
}
// Autre propriété
function echidna_test_no_one_has_too_much() public view returns (bool) {
// L'adresse 'this' (le contrat de test) est utilisée par Echidna
// pour simuler un utilisateur aléatoire
return (token.balanceOf(address(this)) <= 1000000);
}
}
$ echidna-test MyTokenTest.sol --contract MyTokenTest
Echidna va alors exécuter des millions de "campagnes" (ex: `approve(userA, 100)`, `transfer(userB, 50)`, `transferFrom(userA, userB, 20)`...) puis vérifier `echidna_test_total_supply_is_stable()`. S'il parvient à la rendre `false`, il a trouvé un bug et vous donne la séquence de transactions qui l'a causé.
Foundry Fuzzing
Le framework moderne **Foundry** (concurrent de Hardhat) intègre le fuzzing **nativement** dans ses tests unitaires. Vous pouvez écrire :
// MyToken.t.sol (Test Foundry)
function testFuzz_transfer(uint256 amount) public {
// Foundry va appeler ce test 1000x avec
// des 'amount' aléatoires (très grands, zéro, etc.)
token.transfer(userB, amount);
// ...
}
**Un audit n'est PAS un "scan de sécurité".** Exécuter Slither, Mythril et Echidna n'est pas un audit. C'est l'**étape 0** d'un audit, la "due diligence" automatisée.
Un **audit manuel** (réalisé par des firmes spécialisées comme *ConsenSys Diligence*, *Trail of Bits*, *OpenZeppelin*, *CertiK*) est un processus humain qui se concentre sur ce que les machines ne peuvent pas comprendre : la **logique business** et l'**intention**.
"Le contrat fait-il les choses correctement ?" (Pas d'overflow, pas de réentrance...).
L'audit manuel répond à la question :
"Le contrat fait-il la bonne chose ?" (Est-ce que la logique du `mint` est économiquement saine ? Est-ce que ce "staking" est juste ?).
Le Processus d'Audit
Un audit typique (ex: 2-4 semaines, coûtant 50k$ - 500k$) suit ces étapes :
- 1. Cadrage (Scope) : Les auditeurs et l'équipe définissent *exactement* quels contrats sont audités (basé sur un commit Git spécifique). Tout ce qui est "hors-scope" ne sera pas vérifié.
- 2. Revue de la Documentation : Les auditeurs lisent le "Whitepaper", la documentation technique. Ils doivent comprendre l'**intention** du protocole.
- 3. Analyse Automatisée : Les auditeurs lancent Slither, Mythril, etc. pour nettoyer les failles "faciles" et identifier les zones à risque.
- 4. Revue Manuelle (Line-by-Line) :
- C'est le cœur du travail. 2-3 auditeurs seniors lisent **chaque ligne** du code.
- Ils recherchent les failles classiques (Réentrance, etc.).
- Ils recherchent les failles de **logique business** : "Que se passe-t-il si le prix de l'Oracle devient 0 ?", "Que se passe-t-il si un utilisateur "minte" et "brûle" dans la même transaction ?".
- Ils vérifient les **incitations économiques** (ex: "Ce système de récompense peut-il être 'farmé' et vidé ?").
- 5. Rédaction du Rapport : Les auditeurs écrivent un rapport détaillé, classant les failles par sévérité :
- Critical : Perte de fonds, blocage du contrat.
- High : Comportement incorrect, perte de fonds potentielle.
- Medium : Le contrat fonctionne, mais est inefficace (gaspillage de Gas) ou trompeur.
- Low / Informational : N'adhère pas aux meilleures pratiques, typos dans les commentaires.
- 6. Remédiation : L'équipe de développement corrige les failles.
- 7. Vérification : Les auditeurs vérifient les corrections.
- 8. Publication : Le rapport final (avec les corrections) est publié.
Ce que les Outils Manquent (Exemples)
- Faille Logique (Ex: Oracle) : Un outil de Fuzzing peut vérifier qu'un "swap" fonctionne. Un auditeur humain se demandera : "Que se passe-t-il si le prix de l'oracle (ex: Chainlink) tombe en panne ? Le contrat gère-t-il un "prix à zéro" ?".
- Centralisation (
onlyOwner) : Slither verra une fonction `setFees()` protégée par `onlyOwner`. Il dira "OK". L'auditeur humain mettra un "Medium Risk" en disant : "Cette fonction est centralisée. Le 'owner' (l'équipe) peut augmenter les frais à 100% et voler les utilisateurs. Recommandation : Mettre les frais derrière une DAO ou un Timelock." - Économie (Tokenomics) : Un "fuzzer" ne verra pas qu'une fonction de "rebasing" d'un token (ex:
balanceOf(user) * X) va briser la compatibilité avec tous les protocoles DeFi (comme Uniswap) qui s'attendent à ce qu'un solde soit stable.
L'audit n'est pas une garantie à 100%. Des bugs (ou des hacks de logique économique non prévus) peuvent subsister. L'étape suivante est la **Surveillance en Temps Réel** (Runtime Security).
**Forta** est le standard de l'industrie pour cela. C'est un **réseau décentralisé de "Détection Bots"** (des scanners) qui agissent comme des caméras de sécurité pour vos contrats déployés.
Comment Forta Fonctionne
Forta est un réseau de nœuds "scanners" qui surveillent deux choses :
- Le Mempool (Transactions en attente) : Forta peut voir les transactions *avant* qu'elles ne soient minées.
- Les Blocs (Transactions confirmées) : Forta analyse chaque bloc dès sa publication.
En tant que développeur de protocole, vous écrivez (en JavaScript ou Python) un **"Detection Bot"** (Agent) qui s'abonne à des événements spécifiques. Vous le déployez sur le réseau Forta.
Exemples de "Detection Bots"
- Bot "Changement d'Admin" :
- Surveille : Votre contrat `MyContract`.
- Déclencheur : L'événement `OwnershipTransferred(oldOwner, newOwner)`.
- Action : Envoie une alerte "CRITICAL" à votre Slack/Telegram/Pager. (Un changement de propriétaire est un "red flag" majeur).
- Bot "Flash Loan" (Mempool) :
- Surveille : Le Mempool.
- Déclencheur : Détecte une transaction complexe qui (1) Emprunte sur Aave, (2) Appelle votre contrat, (3) Rembourse sur Aave, le tout atomiquement.
- Action : Alerte "HIGH" : "Attaque par Flash Loan potentielle détectée dans le Mempool ciblant votre contrat !"
- Bot "Gros Transfert" :
- Surveille : Votre contrat de Token.
- Déclencheur : Événement `Transfer(from, to, amount)` où `amount > 1,000,000`.
- Action : Alerte "INFO" : "Gros mouvement de fonds."
Temps Réel vs. Prévention
Forta ne peut pas *empêcher* l'attaque (la transaction est déjà envoyée ou minée). Son but est de **réduire le temps de réponse** (Time-to-Respond) de "jours" à "secondes".
Si une alerte "Flash Loan" est détectée, elle peut (théoriquement) :
- Alerter l'équipe de sécurité (Pager).
- Permettre à l'équipe de déclencher la fonction "Pause" (via le `Pausable.sol` d'OpenZeppelin) pour limiter les dégâts.
- (Dans le futur) Déclencher automatiquement une transaction (via un "Bot de Remédiation") pour "fronter" (front-run) l'attaque ou mettre le contrat en pause.
C'est la dernière ligne de défense lorsque la prévention et l'audit ont échoué.
Un "Secure SDLC" pour le Web3 intègre tous les outils et processus précédents dans un pipeline cohérent, de la conception à la production.
| Phase | Objectif | Outils & Actions |
|---|---|---|
| 1. Conception (Design) | Prévenir les failles de logique business. | - **"Don't roll your own crypto"** : Utiliser **OpenZeppelin** pour tout (ERC20, Ownable). - Définir les **invariants** (propriétés) du protocole. - Définir le modèle de contrôle d'accès (Simple `Ownable` ou `AccessControl` ?). |
| 2. Développement (Develop) | Écrire du code propre et testé. | - Utiliser un compilateur moderne (Solidity 0.8.x+) pour la protection native contre Over/Underflow. - Appliquer le pattern **Checks-Effects-Interactions**. - Écrire des **Tests Unitaires** (Foundry, Hardhat) couvrant 100% des lignes et des branches. |
| 3. Test (Test) | Tester les "inconnues" et les "edge cases". | - **Fuzzing / Property-Based Testing** (Echidna, Foundry Fuzz) pour tester les invariants (voir 4.1) avec des millions d'inputs aléatoires. - Tests d'intégration sur un Testnet public (Goerli, Sepolia). |
| 4. Intégration (CI/CD) | Automatiser la détection des failles faciles. | - Intégrer **Slither** dans le pipeline de CI/CD (Github Actions). - Rejeter automatiquement les commits qui introduisent une faille "High" (ex: `tx.origin`). |
| 5. Pré-Déploiement (Audit) | Vérification humaine de la logique et de la sécurité. | - **Audit Manuel** (ex: ConsenSys, Trail of Bits) par une (ou plusieurs) tierce partie(s). - Correction (Remédiation) de *toutes* les failles critiques et hautes. |
| 6. Production (Runtime) | Détecter et répondre aux menaces en temps réel. | - Déployer des **"Detection Bots" (Forta)** pour surveiller les événements critiques (changements d'admin, grosses transactions, exploits mempool). - Avoir un plan d'urgence (ex: fonction `pause()` du contrat). |
