Presiona ESC para cerrar

Puentes Cripto: Cómo Funcionan y Por Qué Los Hackean en 2026

Un bridge cross-chain no es un túnel mágico entre redes, sino un intermediario financiero puro y duro compuesto por dos smart contracts aislados y un nodo relayer off-chain. Las blockchains son incapaces de comunicarse entre sí de forma directa: Ethereum no tiene ni idea de lo que pasa dentro de Solana, y Bitcoin ni se entera de que existe Arbitrum.

Cuando un usuario mueve 10 ETH de Ethereum a Arbitrum, el bridge no está transfiriendo tokens físicamente. Lo que hace es bloquear (Lock) los 10 ETH originales en el smart contract de la red de origen y, al mismo tiempo, mintear (Mint) 10 equivalentes sintéticos "wrapped" (wETH) en la red de destino.

Relay node
 

El talón de Aquiles arquitectónico de toda la mecánica cross-chain está oculto en la capa intermedia: los relayers y validadores que ejecutan la verificación de pruebas (proof verification). Si esta capa le "demuestra" al contract de la red destino que los tokens fueron bloqueados legítimamente, el contract simplemente imprime los nuevos assets sin rechistar. Drenar los pools de liquidez a cero es tan sencillo como lograr falsificar esta prueba.

Por qué revientan los bridges: falsificación de pruebas y compromiso de llaves

La gran mayoría de los exploits críticos en bridges se dividen en dos familias principales: bugs lógicos/criptográficos en la validación y compromisos de la infraestructura off-chain (filtración de private keys en multisigs).

En junio de 2026, un fallo catastrófico en el procesamiento de pruebas dejó al descubierto a Syscoin Bridge (con un boquete de $10M). El bridge utiliza un mecanismo SPV (Simplified Payment Verification) para verificar el burn/lock de monedas en el lado UTXO de Syscoin antes de emitir los assets en el lado EVM (NEVM).

El ataque no vino por romper la criptografía, sino por un bug ridículo en el parser del relayer. El atacante armó un payload con una estructura de bytes malformada. Como no había una validación estricta sobre la longitud y el formato del array de bytes entrante, el parser tragó y procesó un hash falso como si fuera una prueba SPV válida de un burn real. ¿El resultado? El bridge autorizó el minteo de 5,000 millones de tokens SYS en la cadena UTXO sin haber dejado ni un solo token colateralizado en NEVM.

El otro vector clásico es la reutilización de firmas (Signature Reuse). En julio de 2026, Wanchain Bridge (el puente entre Cardano y BNB Chain) cayó exactamente por esto, perdiendo unos $13M. La lógica del validador no marcaba en el storage global del contract las firmas criptográficas válidas ya procesadas como consumidas (spent). El atacante simplemente agarró una transacción legítima del pasado y la volvió a enviar cambiando la dirección del receptor. El contract ejecutó la verificación, vio que la firma del validador era matemáticamente correcta (obvio, si la habían generado antes) y soltó los fondos por segunda vez.

Hackería cross-chain: el recuento de daños en 2026

En cuestión de un par de meses en 2026, las vulnerabilidades en bridges e infraestructura cross-chain drenaron cientos de millones de dólares del ecosistema DeFi.

Proyecto / BridgeFechaMonto drenadoVector de ataque / Causa técnica
Aethir Bridge (OFT Adapter)Abril 2026~$5.0MFlaw lógico en la mensajería cross-chain al transferir tokens ATH entre BNB Chain y Tron
Syscoin BridgeJunio 2026~$10.0MBug de parsing en pruebas SPV. Minteo de 5B de SYS sin colateral real
Wanchain BridgeJulio 2026~$13.0MVulnerabilidad de reutilización de firma (Signature-reuse flaw) en la lógica del validador
AFX Trade BridgeJulio 2026$24.15MPrivkeys de los validadores del bridge en Arbitrum totalmente comprometidas
Verus Ethereum BridgeJulio 2026$7.54MFalta de chequeo de consistencia en reservas (llamada de unlock sin validar colateral)

Los errores de los dev teams en este sector son sistemáticos. La causa raíz suele ser la carrera desmedida por inflar el TVL (Total Value Locked) y salir a producción antes que la competencia. Se saltan la implementación de auditorías de máquinas de estado (state machine audits) y prefieren meter con calzador lógicas de parsing supercomplejas en Solidity o Go, en lugar de delegar esas verificaciones pesadas a esquemas ZK (Zero-Knowledge Proofs) formalmente probados.

Código de producción: Bridge Vault seguro (Solidity 0.8.24)

Aquí hay un ejemplo del contract SecureBridgeVault diseñado para la red de origen. Incluye protección contra Replay Attacks, implementación segura de EIP-712 para validar las firmas del relayer, Reentrancy Guard y un control estricto del estado de los 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 listo para producción con identificador único depositHash, 
* control estricto M-of-N de relayers vía EnumerableSet y blindaje contra errores de configuración.
*/
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 la firma de unlock, idéntico en estructura al 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)"
   );
   // Registro de relayers en el storage del contract
   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 creados 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;
   }
   // --- GESTIÓN DE RELAYERS Y GOBERNANZA ---
   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();
   }
   // --- CONFIGURACIÓN ADMINISTRATIVA DE CHAINS Y 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);
   }
   // --- BLOQUEO Y DESBLOQUEO ---
   /**
    * @notice Generación determinista de depositHash y registro del bloqueo
    */
   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 del depositHash único mediante 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 Retiro de fondos mediante depositHash único con validación de firmas M-of-N
    * @dev Las firmas DEBEN venir ordenadas Off-chain de forma ascendente según las direcciones de los firmantes.
    */
   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();
       // Verificación: comprueba si el hash de los parámetros coincide con el depositHash declarado
       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();
       // Validación de orden de direcciones y umbral de firmas
       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);
   }
}

Los bridges siguen siendo el eslabón más débil de toda la infraestructura Web3. El problema de fondo es que intentan conectar el consenso de dos entornos totalmente distintos usando una capa de código off-chain sumamente frágil. Cualquier fallo tonto en la validación de bytes, un descuido en la estructura EIP-712 o una simple filtración de keys en los servidores de los validadores convierte un smart contract con cientos de millones de dólares en el cajero automático de cualquier hacker.

Resumir esta publicación de blog con:

FAQ

Los bridges cross-chain verifican los cambios de estado mediante pruebas criptográficas (como raíces de Merkle SPV o pruebas Zero-Knowledge) validadas en la cadena de destino o mediante una red distribuida de relayers que ejecutan firmas de umbral (Threshold Signatures como ECDSA o BLS). Al bloquear los activos en el smart contract de origen, la red de relayers confirma la finalización del evento y envía un payload firmado bajo la estructura EIP-712 al contrato de destino, el cual valida las firmas frente al umbral M-of-N antes de mintear los tokens sintéticos.

Las vulnerabilidades se originan principalmente por errores de lógica en el parsing de pruebas criptográficas (falta de validación en la longitud y estructura de arrays de bytes), fallos de reutilización de firmas (signature-reuse) y la filtración de private keys en nodos validadores multisig. Los atacantes explotan los fallos de parsing para procesar hashes falsos como eventos válidos de bloqueo o utilizan claves comprometidas para autorizar retiros sin el debido colateral en reserva.

La prevención de replay attacks exige implementar el hashing de datos estructurados EIP-712 incluyendo el separador de dominio (block.chainid y la dirección del contrato) y registrar los identificadores únicos de transacción en el almacenamiento permanente (executedHashes). La lógica del contrato debe verificar el estado del mapping antes de transferir los fondos, marcar el hash como ejecutado antes de la liberación de activos y requerir la ordenación ascendente de las firmas de entrada para evitar la verificación duplicada de claves en el control de umbral 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.

...

Escribe una opinión

Tu correo electrónico no será publicado. Los campos obligatorios están marcados *