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.

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 / Bridge | Data | Prejuízo | Vetor de Ataque / Causa Técnica |
|---|---|---|---|
| Aethir Bridge (OFT Adapter) | Abril de 2026 | ~$5.0M | Erro de lógica nas mensagens cross-chain ao transferir tokens ATH entre BNB Chain e Tron |
| Syscoin Bridge | Junho de 2026 | ~$10.0M | Erro de parsing na prova SPV. Emissão de 5 bi de SYS sem colateral real |
| Wanchain Bridge | Julho de 2026 | ~$13.0M | Falha de reuso de assinatura (Signature-reuse flaw) do lado do validador |
| AFX Trade Bridge | Julho de 2026 | $24.15M | Comprometimento das chaves privadas dos validadores da bridge na Arbitrum |
| Verus Ethereum Bridge | Julho de 2026 | $7.54M | Ausê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.