← Back to portfolio
RAAC Protocol · Blockchain Dev Process · v1.0

Smart Contract Dev Process.

Guide de référence exhaustif pour le développement, le testing, l'audit et le déploiement de smart contracts sur RAAC Protocol — la source de vérité pour tout déploiement en production.

Hardhat 2.xSolidity ^0.8.20Coverage >= 95%EVM mainnet + L2
Dev
Write + NatSpec
Lint
Slither + Solhint
Test
Unit + Fork
Coverage
>= 95%
Audit
Internal + Externe
Deploy
Testnet → Mainnet
Monitor
Tenderly + Alerts
00

Stack & Structure de Projet

Framework

Hardhat · ethers.js v6 · TypeScript · Chai / Mocha

Sécurité

Slither · Mythril · solhint · hardhat-gas-reporter

Coverage

solidity-coverage · lcov · Istanbul thresholds

Déploiement

hardhat-deploy · Tenderly · Safe Multisig

Structure de répertoires

shellarborescence

contracts/
├── core/               # Contrats principaux (VotingEscrow, GaugeController…)
├── periphery/          # StableSwap, helpers, routers
├── interfaces/         # IVotingEscrow.sol, IGauge.sol…
├── libraries/          # Logique partagée (math, epoch…)
├── mocks/              # Mocks UNIQUEMENT pour tests
└── upgradeable/        # Proxies UUPS / Beacon

test/
├── unit/               # 1 fichier de test par contrat
│   ├── VotingEscrow.test.ts
│   ├── GaugeController.test.ts
│   └── StableSwap.test.ts
├── integration/        # Scénarios multi-contrats
├── fork/               # Tests sur fork mainnet
└── fuzz/               # Propriétés invariantes

scripts/
├── deploy/             # 01_deploy_escrow.ts, 02_deploy_gauge.ts…
├── verify/
└── utils/

deployments/            # Adresses par réseau (générées par hardhat-deploy)
audits/                 # Rapports d'audit + réponses équipe
docs/                   # NatSpec exportée + architecture
Règle absolue — mocks hors production

Aucun contrat dans contracts/mocks/ ne doit jamais être référencé dans un script de déploiement production. Vérifier via grep -r "Mock" scripts/ avant chaque deploy.

01

Hardhat Configuration

typescripthardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-deploy";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@tenderly/hardhat-tenderly";

const config: HardhatUserConfig = {
  solidity: {
    compilers: [{
      version: "0.8.20",
      settings: {
        optimizer: { enabled: true, runs: 200 },
        viaIR: true,                   // active l'optimiseur via IR
        outputSelection: { "*": { "*": ["storageLayout"] } }
      }
    }]
  },
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL!,
        blockNumber: 19_500_000         // pin pour reproductibilité
      },
      allowUnlimitedContractSize: false  // simuler la limite 24KB
    },
    mainnet:  { url: process.env.MAINNET_RPC_URL!, accounts: [process.env.DEPLOYER_PK!] },
    arbitrum: { url: process.env.ARB_RPC_URL!, accounts: [process.env.DEPLOYER_PK!] },
    sepolia:  { url: process.env.SEPOLIA_RPC_URL!, accounts: [process.env.DEPLOYER_PK!] }
  },
  gasReporter: {
    enabled: process.env.REPORT_GAS === "true",
    currency: "USD", coinmarketcap: process.env.CMC_API_KEY,
    outputFile: "gas-report.txt", noColors: true
  },
  coverage: {
    skipFiles: ["mocks/", "test/"]
  }
};

export default config;

Variables d'environnement requises (.env)

shell.env.example

MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
ARB_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
DEPLOYER_PK=                          # Jamais la clé du multisig
ETHERSCAN_API_KEY=
CMC_API_KEY=                          # Pour gas reporter USD
REPORT_GAS=false
TENDERLY_ACCESS_KEY=
TENDERLY_PROJECT=raac-protocol
Sécurité des clés privées

Le DEPLOYER_PK ne doit JAMAIS être la clé propriétaire du multisig. Utiliser un hot wallet dédié avec des fonds minimaux. Les transactions importantes passent TOUJOURS par Safe Multisig.

Scripts NPM standards

jsonpackage.json — scripts

"compile":         "hardhat compile",
"test":            "hardhat test",
"test:fork":       "HARDHAT_FORK=mainnet hardhat test test/fork/**",
"coverage":        "hardhat coverage --solcoverjs .solcover.js",
"gas":             "REPORT_GAS=true hardhat test",
"lint":            "solhint 'contracts/**/*.sol'",
"slither":         "slither . --config-file slither.config.json",
"deploy:testnet":  "hardhat deploy --network sepolia --tags all",
"deploy:mainnet":  "hardhat deploy --network mainnet --tags all",
"verify":          "hardhat etherscan-verify --network mainnet",
"size":            "hardhat size-contracts"
02

Standards Solidity

Phase développement

Règles de nommage

ÉlémentConventionExemple
ContractPascalCaseVotingEscrow
InterfaceI + PascalCaseIVotingEscrow
LibraryPascalCase + LibEpochLib
EventPascalCaseGaugeAdded
Error customPascalCaseInsufficientBalance
Function publiquecamelCaselockTokens
Variable private_camelCase_totalLocked
ConstanteUPPER_SNAKEMAX_LOCK_DURATION
ImmutableUPPER_SNAKESTART_TIME
Storage slotUPPER_SNAKE_SLOTSTORAGE_SLOT

Template de contrat avec NatSpec obligatoire

solidityContractTemplate.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title  NomDuContrat
/// @author RAAC Protocol
/// @notice Description courte lisible par les utilisateurs
/// @dev    Détails techniques pour les développeurs. Mentionner
///         les dépendances critiques et les invariants du contrat.
contract NomDuContrat is Initializable, AccessControlUpgradeable {

    // ── ERRORS ────────────────────────────────────────────────
    error Unauthorized();
    error InvalidAmount(uint256 amount);
    error ZeroAddress();

    // ── EVENTS ────────────────────────────────────────────────
    /// @notice Emis lors d'un dépôt réussi
    /// @param  user    Adresse du déposant
    /// @param  amount  Montant en wei
    event Deposited(address indexed user, uint256 amount);

    // ── CONSTANTS ─────────────────────────────────────────────
    uint256 public constant MAX_LOCK = 4 * 365 days;

    // ── STATE ─────────────────────────────────────────────────
    /// @notice Total des tokens verrouillés dans le contrat
    uint256 public totalLocked;

    // ── CONSTRUCTOR / INITIALIZER ─────────────────────────────
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() { _disableInitializers(); }

    /// @notice Initialise le contrat (proxy pattern)
    /// @param  _admin  Adresse qui reçoit DEFAULT_ADMIN_ROLE
    function initialize(address _admin) external initializer {
        if (_admin == address(0)) revert ZeroAddress();
        __AccessControl_init();
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }

    // ── EXTERNAL ──────────────────────────────────────────────
    /// @notice Dépose des tokens
    /// @param  amount  Montant à déposer (>0)
    /// @return shares  Nombre de shares créditées
    function deposit(uint256 amount) external returns (uint256 shares) {
        if (amount == 0) revert InvalidAmount(amount);
        // Checks → Effects → Interactions
        shares = _calculateShares(amount);
        totalLocked += amount;
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        emit Deposited(msg.sender, amount);
    }
}
!
NatSpec est obligatoire

Toute fonction external ou public doit avoir @notice + @param + @return. Les contrats sans NatSpec complète ne passent pas la revue de code. Générer la doc via hardhat docgen.

03

Patterns & Règles de Sécurité

Checks-Effects-Interactions (CEI) — obligatoire

solidityCEI Pattern

function withdraw(uint256 amount) external nonReentrant {
    // 1. CHECKS — toutes les validations en premier
    if (amount == 0)                revert InvalidAmount(amount);
    if (balances[msg.sender] < amount) revert InsufficientBalance();

    // 2. EFFECTS — modifier l'état AVANT tout appel externe
    balances[msg.sender] -= amount;
    totalLocked -= amount;

    // 3. INTERACTIONS — appels externes EN DERNIER
    IERC20(token).transfer(msg.sender, amount);
    emit Withdrawn(msg.sender, amount);
}

Règles d'accès aux rôles (OpenZeppelin AccessControl)

solidityRôles RAAC

// Définir les rôles en bytes32 constants — jamais en string direct
bytes32 public constant OPERATOR_ROLE   = keccak256("OPERATOR_ROLE");
bytes32 public constant EMERGENCY_ROLE  = keccak256("EMERGENCY_ROLE");
bytes32 public constant GAUGE_ADMIN     = keccak256("GAUGE_ADMIN");

// Toujours utiliser le modifier, pas onlyOwner (Ownable déprécié)
function addGauge(address gauge) external onlyRole(GAUGE_ADMIN) {
    // ...
}

// Pattern emergency pause — le circuit breaker
function pause() external onlyRole(EMERGENCY_ROLE) { _pause(); }
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); }

Patterns obligatoires par type de contrat

Token ERC-20 (RAAC)

ERC20Permit · ERC20Votes · nonReentrant sur mint/burn · cap sur supply

veToken (VotingEscrow)

Lock / unlock avec timestamps · checkpoint obligatoire · lecture bias/slope

Gauge

Epoch-based · lazy checkpoint · overflow sur intégrale de reward protégé

Upgradeable (proxy)

UUPS ou Transparent · storage gap 50 slots · _disableInitializers() constructor

Interdictions absolues

  • Utiliser tx.origin pour les permissions
    INTERDIT
  • Utiliser block.timestamp seul pour l'aléatoire
    INTERDIT
  • Appel externe avant mise à jour d'état (reentrancy)
    INTERDIT
  • Division avant multiplication (précision perdue)
    INTERDIT
  • Déléguer à une adresse non validée delegatecall
    INTERDIT
  • Selfdestruct (déprécié EIP-6049)
    INTERDIT
  • Modifier le storage layout d'un proxy sans migration
    INTERDIT
  • Déployer sans nonReentrant sur toute fonction payable
    HIGH
04

Optimisation Gas

Règles d'optimisation de base

solidityGas patterns

// ✓ Packer les variables storage (slot de 32 bytes)
struct LockData {
    uint128 amount;       // 16 bytes  ─┐ même slot
    uint64  lockEnd;      //  8 bytes  ─┤
    uint64  lockStart;    //  8 bytes  ─┘
}

// ✓ Cache les variables storage dans des variables locales
function computeReward() external view returns (uint256) {
    uint256 _totalSupply = totalSupply; // 1 SLOAD
    uint256 _rewardRate  = rewardRate;  // 1 SLOAD
    return _totalSupply * _rewardRate / 1e18;
}

// ✓ Custom errors < revert string (économie ~200 gas)
error TooEarly(uint256 available);  // ✓
require(block.timestamp >= end, "Too early");  // ✗ plus cher

// ✓ unchecked pour les compteurs quand overflow impossible
unchecked {
    for (uint256 i; i < length; ++i) { // ++i < i++
        // ...
    }
}

// ✓ immutable > constant > storage pour les valeurs fixes
address public immutable RAAC_TOKEN;   // set dans constructor

Workflow gas reporting

shellterminal

# Générer un rapport gas avec prix USD
REPORT_GAS=true npx hardhat test --grep "GaugeController"

# Vérifier les tailles de contrats (limite: 24KB)
npx hardhat size-contracts

# Comparer avant/après une optimisation
git stash && REPORT_GAS=true npx hardhat test > gas-before.txt
git stash pop && REPORT_GAS=true npx hardhat test > gas-after.txt
diff gas-before.txt gas-after.txt
Seuil de taille de contrat

Vérifier npx hardhat size-contracts sur chaque PR. Tout contrat dépassant 20 KB doit être réarchitecturé (split en libraries, proxy pattern, Diamond EIP-2535).

05

Stratégie de Tests

Phase testing

Template de test unitaire

typescripttest/unit/VotingEscrow.test.ts

import { expect } from "chai";
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";

describe("VotingEscrow", function () {

  // ── FIXTURE ─────────────────────────────────────────────
  // Chaque describe réutilise le même fixture pour isolation
  async function deployFixture() {
    const [owner, alice, bob] = await ethers.getSigners();
    const token = await ethers.deployContract("VotingEscrow");
    await token.initialize(owner.address);
    return { token, owner, alice, bob };
  }

  describe("#lockTokens", function () {

    it("reverts with InvalidAmount on zero", async function () {
      const { token, alice } = await loadFixture(deployFixture);
      await expect(token.connect(alice).lockTokens(0, MAX_LOCK))
        .to.be.revertedWithCustomError(token, "InvalidAmount");
    });

    it("emits Locked event with correct args", async function () {
      const { token, alice } = await loadFixture(deployFixture);
      const amount = ethers.parseEther("100");
      await expect(token.connect(alice).lockTokens(amount, MAX_LOCK))
        .to.emit(token, "Locked")
        .withArgs(alice.address, amount, anyValue);
    });

    it("increases totalLocked by amount", async function () {
      const { token, alice } = await loadFixture(deployFixture);
      const amount = ethers.parseEther("100");
      const before = await token.totalLocked();
      await token.connect(alice).lockTokens(amount, MAX_LOCK);
      expect(await token.totalLocked()).to.equal(before + amount);
    });
  });
});

Règles de test à respecter

  • Utiliser loadFixture pour l'isolation des tests (pas de beforeEach avec déploiement)
    CRITICAL
  • 1 describe par fonction publique, 1 it par scénario distinct
    HIGH
  • Tester TOUS les chemins d'erreur avec revertedWithCustomError
    HIGH
  • Tester les événements avec emit + withArgs
    MED
  • Tester les limites: 0, max, max-1, overflow potentiel
    HIGH
  • Tester les transitions d'état avant et après chaque action
    MED
  • Tester le contrôle d'accès pour TOUS les rôles sur chaque fonction protégée
    HIGH
  • Tester les scénarios de reentrancy si contrat payable
    HIGH
06

Coverage Thresholds

Seuils minimums par type de métrique

Lines
95%
Functions
100%
Branches
90%
Statements
95%
javascript.solcover.js

module.exports = {
  skipFiles: ['mocks/', 'test/', 'interfaces/'],
  configureYulOptimizer: true,
  // Seuils minimum — le CI échoue si non atteints
  istanbulReporter: ['html', 'lcov', 'text'],
  mocha: { timeout: 120_000 }
};
jsonpackage.json — coverage thresholds (nyc)

"nyc": {
  "check-coverage": true,
  "lines": 95,
  "functions": 100,
  "branches": 90,
  "statements": 95
}
Coverage insuffisant = PR bloquée

Aucune PR ne peut être mergée si la coverage tombe en dessous des seuils. Exceptions uniquement pour les fichiers explicitement exclus dans .solcover.js.

07

Fork Tests & Fuzz Testing

Fork test — intégration contre le mainnet réel

typescripttest/fork/StableSwap.fork.test.ts

import { ethers } from "hardhat";
import { impersonateAccount, setBalance } from
  "@nomicfoundation/hardhat-network-helpers";

describe("StableSwap — Mainnet Fork", function () {
  this.timeout(120_000); // Fork tests sont lents

  it("swaps USDC → DAI via Curve 3pool", async function () {
    // Impersonate un whale USDC réel
    const whale = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
    await impersonateAccount(whale);
    await setBalance(whale, ethers.parseEther("10"));
    const signer = await ethers.getSigner(whale);

    const usdc = await ethers.getContractAt("IERC20", USDC_ADDRESS);
    const pool = await ethers.getContractAt("ICurvePool", CURVE_3POOL);

    const amountIn = ethers.parseUnits("1000", 6); // 1000 USDC
    await usdc.connect(signer).approve(pool.target, amountIn);

    // Curve exchange(i, j, dx, min_dy) — 3pool: 0=DAI, 1=USDC, 2=USDT
    const out = await pool.connect(signer).exchange(1, 0, amountIn, 0);
    // Vérifier le peg: 1 USDC ~ 1 DAI (± 0.1%)
    expect(out).to.be.closeTo(
      ethers.parseEther("1000"),
      ethers.parseEther("1")  // 0.1% de tolérance
    );
  });
});

Fuzz / Propriétés invariantes

typescripttest/fuzz/VotingEscrow.invariants.ts

import fc from "fast-check"; // npm i -D fast-check

it("totalVotingPower >= 0 for any lock config", async function () {
  await fc.assert(
    fc.asyncProperty(
      fc.bigInt({ min: 1n, max: ethers.parseEther("1000000") }),
      fc.integer({ min: 1, max: 4 * 365 }), // durée en jours
      async (amount, days) => {
        const power = await escrow.computeVotingPower(amount, days);
        expect(power).to.be.gte(0);
        expect(power).to.be.lte(amount); // toujours <= amount
      }
    ),
    { numRuns: 500 }
  );
});
08

Checklist Pré-Audit

Phase audit

Vulnérabilités à vérifier manuellement

  • Reentrancy — toutes les fonctions avec appels externes ont nonReentrant ou suivent CEI strict
    CRITICAL
  • Access Control — chaque fonction sensible a le bon modifier de rôle, testé
    CRITICAL
  • Integer overflow — opérations arithmétiques protégées (Solidity 0.8+ ou unchecked justifié)
    CRITICAL
  • Price oracle manipulation — pas d'utilisation de spot price sans TWAP ou Chainlink
    CRITICAL
  • Front-running — les fonctions sensibles (swap, liquidate) utilisent des slippage params
    HIGH
  • Flash loan attack — les fonctions qui lisent des balances vérifient l'état du même block
    HIGH
  • Donation attack (ERC4626) — protection contre la manipulation du exchange rate au premier dépôt
    HIGH
  • Storage collision — les proxies utilisent des storage slots isolés via ERC7201
    HIGH
  • Initialization — tous les contrats upgradeables ont _disableInitializers()
    HIGH
  • Precision loss — les divisions sont en dernier, pas de truncation prématurée
    HIGH
  • Timestamp dependency — aucune logique critique dépend de block.timestamp seul
    MED
  • Gas griefing — les boucles ont des limites ou sont unbounded sur des arrays contrôlés
    MED
  • Events — chaque modification d'état émet un event avec les données indexées
    MED

Checklist documentation audit

  • NatSpec complète sur tous les contrats, fonctions, events et erreurs
  • Diagramme d'architecture à jour (draw.io ou Mermaid)
  • Liste des adresses de dépendances externes (oracles, tokens, multisigs)
  • Rapport de coverage lcov disponible et > seuils
  • Gas report généré et joint
  • CHANGELOG des modifications depuis le dernier audit
  • Inventaire complet des trusted roles et leurs permissions
09

Analyse Statique

Slither
Trail of Bits — détecteur de patterns dangereux
Solhint
Linter de style + règles de sécurité
Mythril
Analyse symbolique approfondie
solc-select
Gestion des versions Solidity pour Slither
Semgrep
Règles custom sur patterns RAAC
eth-security-toolbox
Image Docker Trail of Bits complète
jsonslither.config.json

{
  "filter_paths": "node_modules,contracts/mocks,contracts/test",
  "exclude_dependencies": true,
  "checklist": true,
  "sarif": "slither-report.sarif",
  "detectors_to_exclude": "tautology,boolean-equality",
  "printers_to_run": "human-summary,inheritance-graph,contract-summary"
}
shell.solhint.json

{
  "extends": "solhint:recommended",
  "rules": {
    "compiler-version": ["error", "^0.8.20"],
    "func-visibility": ["error", { "ignoreConstructors": true }],
    "no-unused-vars": "error",
    "no-empty-blocks": "warn",
    "custom-errors": "warn",
    "named-parameters-mapping": "warn",
    "avoid-call-value": "error"
  }
}
!
Zéro warning High/Critical de Slither

Le CI est configuré pour échouer si Slither détecte des issues de sévérité high ou critical. Les warnings Medium doivent être documentés et triés avant chaque déploiement.

10

Scripts de Déploiement

Phase déploiement

Ordre de déploiement RAAC Protocol

ÉtapeContratDépendancesTag hardhat-deploy
01RAACTokentoken
02VotingEscrowRAACTokenescrow
03GaugeControllerVotingEscrowgauge-ctrl
04GaugeRewardsDistributorGaugeControllerdistributor
05StableSwapRAACToken + Oraclepool
06Setup RolesTous ci-dessussetup
typescriptscripts/deploy/02_deploy_escrow.ts

import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";

const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
  const { deployments, getNamedAccounts, network } = hre;
  const { deploy, get } = deployments;
  const { deployer, multisig } = await getNamedAccounts();

  // Récupérer les dépendances déployées précédemment
  const raacToken = await get("RAACToken");

  const result = await deploy("VotingEscrow", {
    from: deployer,
    proxy: {
      proxyContract: "UUPS",
      execute: {
        methodName: "initialize",
        args: [raacToken.address, multisig]  // admin = multisig
      }
    },
    log: true,
    autoMine: true,
    waitConfirmations: network.name === "mainnet" ? 5 : 1
  });

  // Vérifier sur Etherscan automatiquement
  if (result.newlyDeployed && network.name !== "hardhat") {
    await hre.run("verify:verify", {
      address: result.address,
      constructorArguments: []
    });
  }
};

deploy.tags = ["escrow", "all"];
deploy.dependencies = ["token"];  // Hardhat-deploy gère l'ordre
export default deploy;
11

Checklist de Déploiement

Avant le déploiement (pre-flight)

  • Tests complets passent sur le réseau cible (fork au bon block)
    CRITICAL
  • Coverage >= seuils sur tous les contrats à déployer
    CRITICAL
  • Slither 0 warning high/critical sur le scope de déploiement
    CRITICAL
  • Taille des contrats vérifiée (< 24KB via hardhat size-contracts)
    HIGH
  • Déploiement testé sur Sepolia/testnet avec le même script
    HIGH
  • Adresses multisig Safe vérifiées (getNamedAccounts pointe vers le bon Safe)
    CRITICAL
  • Gas limit et gas price estimés — solde deployer suffisant
    HIGH
  • Variables d'environnement production définies et vérifiées
    HIGH

Après le déploiement (post-deploy)

  • Vérification Etherscan réussie sur tous les contrats et implémentations proxy
    HIGH
  • Adresses enregistrées dans deployments/mainnet/ via hardhat-deploy
  • Transfert admin vers Safe Multisig exécuté et confirmé on-chain
    CRITICAL
  • Smoke tests post-déploiement (appels en lecture, 1 transaction test)
    HIGH
  • Alertes Tenderly configurées sur les contrats déployés
    HIGH
  • Mise à jour du fichier README avec les nouvelles adresses
  • Tag git créé: git tag deploy/mainnet/v1.x.x -a
12

CI/CD Pipeline (GitHub Actions)

yaml.github/workflows/ci.yml

name: CI — Smart Contracts

on:
  push: { branches: [main, develop] }
  pull_request: { branches: [main, develop] }

jobs:
  lint-and-compile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run lint
      - run: npm run compile

  test-and-coverage:
    needs: lint-and-compile
    runs-on: ubuntu-latest
    env:
      MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run coverage
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with: { token: ${{ secrets.CODECOV_TOKEN }} }
      - name: Fail if coverage below threshold
        run: npx istanbul check-coverage --lines 95 --functions 100

  slither:
    needs: lint-and-compile
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Slither
        uses: crytic/slither-action@v0.4.0
        with:
          sarif: results.sarif
          fail-on: high
      - uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: results.sarif }

  gas-report:
    needs: test-and-coverage
    runs-on: ubuntu-latest
    env:
      MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
      REPORT_GAS: "true"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run gas
      - uses: actions/upload-artifact@v4
        with: { name: gas-report, path: gas-report.txt }

Branch protection rules

Toutes les branches main et develop requièrent: lint ✓ tests ✓ coverage ✓ slither ✓ avant merge.

Secrets GitHub requis

MAINNET_RPC_URL · CODECOV_TOKEN · ETHERSCAN_API_KEY · CMC_API_KEY

13

Monitoring Post-Déploiement

Alertes Tenderly à configurer

TriggerSévéritéAction
Transfer admin roleCRITICALPagerDuty immédiat
Pause / UnpauseHIGHSlack + équipe
Gauge added/killedHIGHSlack
Large pool swap (> $100k)HIGHSlack
Transaction failed on contractMEDSlack
Gas usage anomaly (>50% baseline)MEDSlack
Escrow totalLocked change > 5%INFODashboard

Outils de monitoring

Tenderly
Alertes + simulation de tx
OpenZeppelin Defender
Autotasks + Sentinels
Dune Analytics
Dashboards metrics protocol
14

Incident Response

Circuit Breaker — procédure d'urgence

  • Pause immédiate — Appeler pause() via Safe Multisig si exploitation détectée
    STEP 1
  • Notifier — Annoncer le problème sur Discord/Twitter + contact avec auditeur
    STEP 2
  • Analyser — Reproduire sur fork local, quantifier l'impact, identifier la root cause
    STEP 3
  • Patch — Développer le fix, tests, audit rapide (si possible) sur le correctif
    STEP 4
  • Upgrade ou Migration — Si proxy: upgrade via Safe. Sinon: migration vers nouveau contrat
    STEP 5
  • Post-mortem — Rapport post-incident public dans 48h (root cause, impact, mesures prises)
    STEP 6
Bug Bounty

Maintenir un programme de bug bounty actif sur Immunefi ou HackerOne. Tout report de sévérité Critical ou High doit être traité en < 24h. Les fonds de récompense sont pré-approuvés par le multisig.


RAAC Protocol · Blockchain Dev Process · v1.0
Hardhat · Solidity ^0.8.20 · Ce document est la source de vérité pour tout déploiement en production.