Appuyez sur ESC pour fermer

Bridge Crypto : Fonctionnement et Hacks Majeurs en 2026

Un bridge cross-chain n'a rien d'un tunnel magique entre deux réseaux : c'est un vulgaire intermédiaire financier composé de deux smart contracts isolés et d'un relai off-chain (relay node). Les blockchains sont fondamentalement incapables de communiquer en direct : Ethereum n'a aucune idée de ce qui se passe sur Solana, et Bitcoin ignore jusqu'à l'existence d'Arbitrum.

Quand un utilisateur transfère 10 ETH du mainnet Ethereum vers Arbitrum, le bridge ne déplace absolument aucun jeton. Il se contente de verrouiller (Lock) les 10 ETH originaux dans le smart contract de la chain source et minte (Mint) en parallèle 10 équivalents synthétiques « wrapped » (wETH) sur la chain de destination.

Relay node
 

Le principal point de défaillance de la mécanique cross-chain réside dans la couche intermédiaire — ces fameux relais et validateurs chargés de la vérification de preuve (proof verification). Si cette couche certifie au contract de la chain cible que les tokens ont bien été séquestrés, le contract imprime les nouveaux assets sans poser de questions. Il suffit de forge cette preuve pour siphonner l'intégralité des pools de liquidité en un claquement de doigts.

Anatomie des hacks : falsification de preuves et compromission de clés

La quasi-totalité des exploits critiques sur les bridges se divise en deux grandes catégories : les bugs de logique/cryptographiques lors de la validation, et la compromission pure et simple de l'infra off-chain (fuite de clés privées sur des multisigs).

En juin 2026, Syscoin Bridge en a fait les frais (ardoise : 10 M$) suite à un raté monumental dans la gestion des preuves. Le bridge s'appuie sur un mécanisme SPV (Simplified Payment Verification) pour vérifier le burn ou le lock des coins côté UTXO (Syscoin) avant d'autoriser le mint côté EVM (NEVM).

L'attaque n'a pas cassé la crypto, mais a exploité un bug bête et méchant dans le parser du relai. L'attaquant a forgé un payload d'exploit avec une structure d'octets malformée. Faute de contrôle strict sur la longueur et le format du tableau d'octets en entrée, le parser a avalé un faux hash comme s'il s'agissait d'une preuve SPV valide attestant d'un vrai burn. Résultat : le bridge a validé l'émission de 5 milliards de tokens SYS sur la chain UTXO, sans qu'un seul centime ne soit verrouillé sur NEVM.

Autre vecteur dévastateur : le Signature Reuse (réutilisation de signature). En juillet 2026, c'est Wanchain Bridge (le pont reliant Cardano à BNB Chain) qui y a laissé des plumes, à hauteur de 13 M$. La logique du validateur ne marquait pas les signatures cryptographiques valides comme consommées (spent) dans el storage global du contract. L'attaquant a simplement intercepté une transaction légitime passée et l'a rejouée en modifiant le destinataire. Le contract a vérifié la signature du validateur — valide sur le plan mathématique puisque générée auparavant — et a recraché les funds une deuxième fois.

L'hécatombe de 2026 : l'année noire des bridges

En seulement quelques mois courant 2026, les vulnérabilités des bridges et des briques cross-chain ont fait partir en fumée des centaines de millions de dollars au sein de l'écosystème DeFi.

Projet / BridgeDateMontant des pertesVecteur d'attaque / Origine technique
Aethir Bridge (OFT Adapter)Avril 2026~$5,0 MBug logique dans la messagerie cross-chain lors des transferts de tokens ATH entre BNB Chain et Tron
Syscoin BridgeJuin 2026~$10,0 MErreur de parsing sur la preuve SPV. Mint abusif de 5 Md SYS sans aucun collateral réel
Wanchain BridgeJuillet 2026~$13,0 MFaille de Signature Reuse côté validateur
AFX Trade BridgeJuillet 2026$24,15 MFuite des clés privées des validateurs du bridge Arbitrum
Verus Ethereum BridgeJuillet 2026$7,54 MAbsence de verification de solvabilité des réserves (withdrawal call sans check du sous-jacent)

On fait face ici à une négligence quasi systématique chez les dev. La cause racine ? Une course frénétique à la TVL (Total Value Locked) pour shipper un produit cross-chain avant les concurrents. Résultat, les équipes font l'impasse sur l'audit de leurs machines à états (state machine audit) et s'obstinent à coder des parsers ultra-complexes et lourds en Solidity ou Go, au lieu d’accorder leur confiance à des circuits ZK (Zero-Knowledge Proofs) vérifiables.

Modèle de Vault sécurisé prêt pour la prod (Solidity 0.8.24)

Voici une implémentation de référence du contract SecureBridgeVault pour la chain source. Il intègre une protection stricte contre les Replay Attacks, s'appuie sur le standard EIP-712 pour la validation des signatures des relais, embarque un Reentrancy Guard et impose un suivi rigoureux des nonces.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
/**
 * @title SecureBridgeVault v3.0
 * @author EXMON Engineering Team (https://exmon.pro)
 * @dev Lead Architect & Security Audit: EXMON Core Team
 * @notice Production-grade cross-chain bridge vault contract.
 */
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/**
* @title SecureBridgeVault v3.0
* @author EXMON Engineering Team
* @notice Vault de production exploitant un depositHash unique, un contrôle strict du quorum M-of-N des relais via EnumerableSet et des garde-fous contre les erreurs de configuration.
*/
contract SecureBridgeVault is EIP712, ReentrancyGuard, AccessControl, Pausable {
   using SafeERC20 for IERC20;
   using ECDSA for bytes32;
   using EnumerableSet for EnumerableSet.AddressSet;
   bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
   bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE");
   // Typehash unique pour la signature unlock, strictement identique en structure à celle de lock
   bytes32 private constant UNLOCK_TYPEHASH = keccak256(
       "Unlock(bytes32 depositHash,address token,address sender,address recipient,uint256 amount,uint256 sourceChainId,uint256 targetChainId,uint256 depositId)"
   );
   // Registre d'adresses des relais dans le storage
   EnumerableSet.AddressSet private _relayers;
   uint256 public requiredSignatures;
   uint256 public totalDepositCount;
   mapping(uint256 => bool) public supportedChains;
   mapping(address => bool) public supportedTokens;
   mapping(bytes32 => bool) public executedHashes;
   mapping(bytes32 => bool) public knownDeposits; // Enregistrement des dépôts initiés localement
   event Locked(
       bytes32 indexed depositHash,
       uint256 indexed depositId,
       address indexed token,
       address sender,
       address recipient,
       uint256 amount,
       uint256 targetChainId
   );
   event Unlocked(
       bytes32 indexed depositHash,
       address indexed token,
       address recipient,
       uint256 amount,
       uint256 sourceChainId
   );
   event RelayerAdded(address indexed relayer);
   event RelayerRemoved(address indexed relayer);
   event ChainStatusUpdated(uint256 indexed chainId, bool supported);
   event TokenStatusUpdated(address indexed token, bool supported);
   event RequiredSignaturesUpdated(uint256 newThreshold);
   error ZeroAddress();
   error ZeroAmount();
   error UnsupportedChain();
   error UnsupportedToken();
   error TransactionAlreadyExecuted();
   error InvalidSignatureThreshold();
   error InvalidSignaturesLength();
   error DuplicateOrUnsortedSignature();
   error InvalidSigner();
   error RelayerAlreadyExists();
   error RelayerDoesNotExist();
   constructor(
       address admin,
       address emergencyAdmin,
       uint256 _requiredSignatures,
       address[] memory initialRelayers
   ) EIP712("EXMON_Bridge_Vault", "3.0.0") {
       if (admin == address(0) || emergencyAdmin == address(0)) revert ZeroAddress();
       
       _grantRole(DEFAULT_ADMIN_ROLE, admin);
       _grantRole(EMERGENCY_ADMIN_ROLE, emergencyAdmin);
       uint256 relayerLength = initialRelayers.length;
       for (uint256 i = 0; i < relayerLength; ) {
           address relayer = initialRelayers[i];
           if (relayer == address(0)) revert ZeroAddress();
           
           if (_relayers.add(relayer)) {
               _grantRole(RELAYER_ROLE, relayer);
               emit RelayerAdded(relayer);
           }
           unchecked { ++i; }
       }
       if (_requiredSignatures == 0 || _requiredSignatures > _relayers.length()) {
           revert InvalidSignatureThreshold();
       }
       requiredSignatures = _requiredSignatures;
   }
   // --- GESTION DES RELAIS ET GOUVERNANCE ---
   function addRelayer(address relayer) external onlyRole(DEFAULT_ADMIN_ROLE) {
       if (relayer == address(0)) revert ZeroAddress();
       if (!_relayers.add(relayer)) revert RelayerAlreadyExists();
       _grantRole(RELAYER_ROLE, relayer);
       emit RelayerAdded(relayer);
   }
   function removeRelayer(address relayer) external onlyRole(DEFAULT_ADMIN_ROLE) {
       if (!_relayers.remove(relayer)) revert RelayerDoesNotExist();
       if (_relayers.length() < requiredSignatures) revert InvalidSignatureThreshold();
       _revokeRole(RELAYER_ROLE, relayer);
       emit RelayerRemoved(relayer);
   }
   function setRequiredSignatures(uint256 _required) external onlyRole(DEFAULT_ADMIN_ROLE) {
       if (_required == 0 || _required > _relayers.length()) {
           revert InvalidSignatureThreshold();
       }
       requiredSignatures = _required;
       emit RequiredSignaturesUpdated(_required);
   }
   function getRelayers() external view returns (address[] memory) {
       return _relayers.values();
   }
   function getRelayerCount() external view returns (uint256) {
       return _relayers.length();
   }
   // --- CONFIGURATION ADMINISTRATIVE DES CHAINS ET TOKENS ---
   function setChainSupport(uint256 chainId, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) {
       supportedChains[chainId] = supported;
       emit ChainStatusUpdated(chainId, supported);
   }
   function setTokenSupport(address token, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) {
       if (token == address(0)) revert ZeroAddress();
       supportedTokens[token] = supported;
       emit TokenStatusUpdated(token, supported);
   }
   function pause() external onlyRole(EMERGENCY_ADMIN_ROLE) {
       _pause();
   }
   function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
       _unpause();
   }
   function emergencyWithdraw(
       address token,
       address to,
       uint256 amount
   ) external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused {
       if (to == address(0)) revert ZeroAddress();
       IERC20(token).safeTransfer(to, amount);
   }
   // --- LOCK ET UNLOCK ---
   /**
    * @notice Génération déterministe du depositHash et verrouillage des assets
    */
   function lock(
       address token,
       uint256 amount,
       address recipient,
       uint256 targetChainId
   ) external nonReentrant whenNotPaused returns (bytes32 depositHash) {
       if (amount == 0) revert ZeroAmount();
       if (recipient == address(0)) revert ZeroAddress();
       if (!supportedTokens[token]) revert UnsupportedToken();
       if (!supportedChains[targetChainId]) revert UnsupportedChain();
       uint256 depositId;
       unchecked {
           depositId = ++totalDepositCount;
       }
       // Calcul sécurisé du depositHash unique via abi.encode
       depositHash = keccak256(
           abi.encode(
               block.chainid,
               targetChainId,
               token,
               msg.sender,
               recipient,
               amount,
               depositId
           )
       );
       knownDeposits[depositHash] = true;
       IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
       emit Locked(depositHash, depositId, token, msg.sender, recipient, amount, targetChainId);
   }
   /**
    * @notice Déverrouillage des fonds via depositHash unique avec vérification du quorum M-of-N
    * @dev Les signatures DOIVENT OBLIGATOIREMENT être triées Off-chain par ordre croissant des adresses des signataires.
    */
   function unlock(
       bytes32 depositHash,
       address token,
       address sender,
       address recipient,
       uint256 amount,
       uint256 sourceChainId,
       uint256 depositId,
       bytes[] calldata signatures
   ) external nonReentrant whenNotPaused {
       if (signatures.length != requiredSignatures) revert InvalidSignaturesLength();
       if (amount == 0) revert ZeroAmount();
       if (recipient == address(0) || sender == address(0)) revert ZeroAddress();
       if (!supportedTokens[token]) revert UnsupportedToken();
       if (!supportedChains[sourceChainId]) revert UnsupportedChain();
       // Vérification : match entre le hash reconstruit et le depositHash fourni
       bytes32 expectedDepositHash = keccak256(
           abi.encode(
               sourceChainId,
               block.chainid,
               token,
               sender,
               recipient,
               amount,
               depositId
           )
       );
       if (expectedDepositHash != depositHash) revert InvalidSigner();
       bytes32 structHash = keccak256(
           abi.encode(
               UNLOCK_TYPEHASH,
               depositHash,
               token,
               sender,
               recipient,
               amount,
               sourceChainId,
               block.chainid,
               depositId
           )
       );
       bytes32 txHash = _hashTypedDataV4(structHash);
       if (executedHashes[txHash]) revert TransactionAlreadyExecuted();
       // Validation de l'ordre strict des adresses et vérification du quorum
       address lastSigner = address(0);
       for (uint256 i = 0; i < requiredSignatures; ) {
           address signer = txHash.recover(signatures[i]);
           if (!_relayers.contains(signer)) revert InvalidSigner();
           if (signer <= lastSigner) revert DuplicateOrUnsortedSignature();
           lastSigner = signer;
           unchecked { ++i; }
       }
       executedHashes[txHash] = true;
       IERC20(token).safeTransfer(recipient, amount);
       emit Unlocked(depositHash, token, recipient, amount, sourceChainId);
   }
}

Les bridges demeurent le maillon faible absolu des infrastructures Web3. La raison est simple : ils tentent de faire le grand écart entre les mécanismes de consensus de deux systèmes totalement hétérogènes, reliés par une couche de code off-chain bien trop fragile. La moindre coquille dans le parsing d'un tableau d'octets, un oubli dans la vérification d'une structure EIP-712 ou la moindre fuite de clé sur un serveur de validateur transforme instantanément un smart contract blindé à plusieurs centaines de millions de dollars en un buffet à volonté pour hackers.

Résumer cet article de blog avec :

FAQ

Un bridge cross-chain vérifie les changements d'état via des preuves cryptographiques (telles que les racines de Merkle SPV ou les preuves Zero-Knowledge) validées sur la chaîne de destination ou par un réseau de relais utilisant des signatures de seuil M-of-N. Lors du verrouillage des actifs sur le smart contract source, le réseau de relayeurs valide la finalité de l'événement et transmet une charge utile signée au contrat cible, qui vérifie la conformité des signatures EIP-712 avant d'émettre les jetons synthétiques.

Les failles découlent principalement d'erreurs de parsing des preuves cryptographiques (absence de vérification de la structure et de la taille des tableaux de octets), de la réutilisation de signatures valides (signature-reuse) et de la compromission des clés privées au sein des ensembles de nœuds multi-signatures. Les attaquants exploitent ces faiblesses logiques pour injecter de faux événements de verrouillage ou détourner le seuil de validation afin d'autoriser des retraits non collatéralisés.

La protection contre les attaques par rejeu nécessite la mise en œuvre du hachage de données structurées EIP-712 intégrant un séparateur de domaine (block.chainid et adresse du contrat) ainsi que le suivi des identifiants d'exécution uniques (executedHashes) dans le stockage permanent. Le contrat doit vérifier la valeur booléenne avant tout transfert de fonds, marquer le hachage comme consommé avant l'exécution et imposer un tri ascendant des signatures pour éliminer les doublons lors de la validation du seuil.
Astra EXMON

Astra is the official voice of EXMON and the editorial collective dedicated to bringing you the most timely and accurate information from the crypto market. Astra represents the combined expertise of our internal analysts, product managers, and blockchain engineers.

...

Partager votre avis

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués *