Pressione ESC para fechar

Como Funcionam os Bridges Cripto e Por Que São Hackeados?

Uma bridge cross-chain não é um túnel direto entre redes, mas sim um intermediário financeiro puro e simples, composto por dois smart contracts isolados e um nó relayer off-chain. As blockchains simplesmente não conseguem se comunicar de forma nativa: o Ethereum não faz ideia do que está rolando na Solana, e o Bitcoin nem sabe que a Arbitrum existe.

Quando um usuário transfere 10 ETH da rede Ethereum para a Arbitrum, a bridge não move os tokens de verdade. Ela bloqueia (Lock) os 10 ETH originais no smart contract da rede de origem e, simultaneamente, minta (Mint) 10 equivalentes sintéticos "wrapped" (wETH) na rede de destino.

Relay node
 

A principal vulnerabilidade arquitetônica da mecânica cross-chain está escondida nessa camada intermediária — nos relayers e validadores que fazem a verificação de prova (proof verification). Se essa camada confirma para o contrato na rede de destino que os tokens foram devidamente bloqueados, o contrato simplesmente credita os novos ativos sem piscar. Forjar essa prova permite drenar pools de liquidez até o último centavo.

Por que os hacks acontecem: vetores de falsificação de provas e vazamento de chaves

A maioria dos exploits críticos em bridges se divide em duas categorias principais: bugs criptográficos/lógicos de validação e comprometimento da infraestrutura off-chain (vazamento de chaves privadas de multisigs).

Em junho de 2026, uma falha arquitetônica no processamento de provas foi demonstrada na prática pela Syscoin Bridge (prejuízo de US$ 10 milhões). A bridge utiliza o mecanismo SPV (Simplified Payment Verification) para verificar o queima/bloqueio de moedas no lado UTXO da Syscoin antes de liberar ativos no lado EVM (NEVM).

O ataque não aconteceu por quebra de criptografia, mas sim por um bug besta no parser do relayer. O hacker montou um pacote de dados malicioso com uma estrutura de bytes incorreta. Como não havia validação estrita do tamanho e do formato do array de bytes de entrada, o parser processou um hash forjado como se fosse uma prova SPV válida de uma queima real. A bridge autorizou a emissão de 5 bilhões de tokens SYS na rede UTXO sem que houvesse uma única moeda bloqueada na NEVM.

O segundo vetor fundamental é o reuso de assinaturas (Signature Reuse). Em julho de 2026, quem se deu mal por causa disso foi a Wanchain Bridge (bridge entre Cardano e BNB Chain, com perdas na casa dos US$ 13 milhões). A lógica do validador não marcava as assinaturas criptográficas válidas e já utilizadas como `spent` (gastas) no storage global do contrato. O atacante pegou uma transação legítima executada anteriormente e reenviou a chamada alterando apenas o endereço do destinatário. O contrato validou a assinatura criptográfica — que era matematicamente válida, já que tinha sido gerada anteriormente — e liberou os fundos novamente.

Histórico de Hacks em 2026

Apenas nos primeiros meses de 2026, vulnerabilidades em bridges e na infraestrutura cross-chain associada drenaram centenas de milhões de dólares do ecossistema DeFi.

Projeto / BridgeDataPrejuízoVetor de Ataque / Causa Técnica
Aethir Bridge (OFT Adapter)Abril de 2026~$5.0MErro de lógica nas mensagens cross-chain ao transferir tokens ATH entre BNB Chain e Tron
Syscoin BridgeJunho de 2026~$10.0MErro de parsing na prova SPV. Emissão de 5 bi de SYS sem colateral real
Wanchain BridgeJulho de 2026~$13.0MFalha de reuso de assinatura (Signature-reuse flaw) do lado do validador
AFX Trade BridgeJulho de 2026$24.15MComprometimento das chaves privadas dos validadores da bridge na Arbitrum
Verus Ethereum BridgeJulho de 2026$7.54MAusência de verificação de reserva (chamada de saque sem validação do colateral)

Os erros dos desenvolvedores aqui são sistemáticos. O motivo principal é a corrida insana por TVL (Total Value Locked) e a pressão para lançar produtos cross-chain antes dos concorrentes. Os devs negligenciam a criação de auditorias de máquina de estados (state machine audit) e escrevem lógicas de parsing complexas e inchadas em Solidity ou Go, em vez de migrar verificações pesadas para circuitos ZK (Zero-Knowledge Proofs) verificáveis.

Código de Produção para uma Bridge Segura (Solidity 0.8.24)

Abaixo está um exemplo de contrato `SecureBridgeVault` para a rede de origem. Ele inclui proteção contra ataques de reuso de transação (Replay Attack), mecanismo seguro via EIP-712 para validação das assinaturas dos relayers, Reentrancy Guard e controle rigoroso do estado de 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 Cofre pronto para produção com identificador único depositHash,
* controle rigoroso M-of-N de relayers via EnumerableSet e proteção contra erros de configuração.
*/
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 único para assinatura de unlock, estruturalmente idêntico ao lock
   bytes32 private constant UNLOCK_TYPEHASH = keccak256(
       "Unlock(bytes32 depositHash,address token,address sender,address recipient,uint256 amount,uint256 sourceChainId,uint256 targetChainId,uint256 depositId)"
   );
   // Registro de relayers no storage do contrato
   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; // Registro de depósitos criados localmente
   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;
   }
   // --- GESTÃO DE RELAYERS E GOVERNANÇA ---
   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();
   }
   // --- CONFIGURAÇÕES ADMINISTRATIVAS DE REDES E 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);
   }
   // --- LÓGICA DE LOCK E UNLOCK ---
   /**
    * @notice Geração determinística de depositHash e confirmação do bloqueio
    */
   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;
       }
       // Cálculo seguro do depositHash único 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 Liberação de fundos por depositHash único com validação de assinaturas M-of-N
    * @dev As assinaturas OBRIGATORIAMENTE precisam ser ordenadas Off-chain em ordem crescente pelos endereços dos signatários.
    */
   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();
       // Validação: o hash dos parâmetros bate com o depositHash informado?
       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();
       // Verificação da ordenação dos endereços e do limite mínimo de assinaturas
       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);
   }
}

As bridges continuam sendo o elo mais fraco da infraestrutura Web3 porque tentam conectar o consenso de dois sistemas completamente distintos através de uma camada frágil de código off-chain. Qualquer errinho besta na validação de bytes, na checagem da estrutura EIP-712 ou um simples vazamento de chave nos servidores dos validadores transforma num piscar de olhos um smart contract com centenas de milhões de dólares em caixa aberta para hackers.

Resumir este post do blog com:

FAQ

As pontes cross-chain verificam mudanças de estado por meio de provas criptográficas (como raízes de Merkle SPV ou provas Zero-Knowledge) validadas na blockchain de destino ou através de uma rede distribuída de relayers que utilizam assinaturas de limiar (Threshold Signatures como ECDSA ou BLS). Quando os tokens são bloqueados na cadeia de origem, a rede de relayers confirma a finalidade da transação e envia um payload assinado ao contrato de destino, que valida a conformidade das assinaturas sob a estrutura EIP-712 antes de emitir os ativos sintéticos.

Os ataques decorrem principalmente de falhas de lógica no parsing de provas criptográficas (ausência de validação de tamanho e estrutura de arrays de bytes), vulnerabilidades de reuso de assinatura (signature-reuse) e vazamento de chaves privadas em nós validadores multisig. Atacantes exploram brechas nos parsers para injetar hashes falsos como eventos válidos de bloqueio ou utilizam chaves comprometidas para autorizar saques sem o devido colateral em reserva.

A prevenção contra replay attacks exige a implementação de hashing de dados estruturados via EIP-712 incluindo o separador de domínio (block.chainid e endereço do contrato) e o registro dos identificadores únicos de transação no armazenamento permanente (executedHashes). A lógica do contrato deve verificar o mapeamento booleano antes da transferência de ativos, marcar o hash como executado antes da liberação dos fundos e exigir a ordenação ascendente das assinaturas para impedir a verificação duplicada no controle de limiar M-of-N.
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.

...

Deixe seu parecer

O seu endereço de e-mail não será publicado. Campos obrigatórios estão marcados *