Нажмите ESC, чтобы закрыть

Как работают криптомосты и как их взламывают: разбор 2026

Кроссчейн-мост - это не туннель между сетями, а банальный финансовый посредник, состоящий из двух изолированных смарт-контрактов и офчейн-релея (relay node). Блокчейны не умеют общаться друг с другом напрямую: Ethereum не знает, что происходит внутри Solana, а Bitcoin понятия не имеет о существовании Arbitrum.

Когда пользователь переводит 10 ETH из сети Ethereum в Arbitrum, мост не перемещает токены. Он блокирует (Lock) оригинальные 10 ETH в смарт-контракте сети-источника и одновременно минтит (Mint) 10 синтетических «обернутых» эквивалентов (wETH) в сети-назначении.

Relay node
 

Главная архитектурная уязвимость кроссчейн-механики скрыта в промежуточном слое - релеях и валидаторах, выполняющих проверку (proof verification). Если этот слой доказывает контракт на целевой сети, что токены были честно заблокированы, контракт молча печатает новые активы. Подделка этого доказательства позволяет опустошать пулы ликвидности до нуля.

Почему происходят взломы: вектор подделки доказательств и компрометация ключей

Большинство критических эксплойтов мостов делятся на два ключевых класса: криптографические/логические баги валидации и компрометация офчейн-инфраструктуры (утечка приватных ключей мультисигов).

В июне 2026 года архитектурный провал в обработке доказательств продемонстрировал Syscoin Bridge (ущерб - $10 млн). Мост использует механизм SPV (Simplified Payment Verification) для проверки сжигания/блокировки монет на UTXO-стороне Syscoin перед выпуском активов на EVM-стороне (NEVM).

Атака произошла не из-за взлома криптографии, а из-за ошибки в парсере релея. Взломщик сформировал эксплойт-пакет данных с некорректной байтовой структурой. Из-за отсутствия строгой проверки длины и формата входного массива байтов парсер обработал поддельный хэш как валидное SPV-доказательство реального сжигания. Мост авторизовал выпуск 5 миллиардов токенов SYS на UTXO-цепи без единой заблокированной монеты на NEVM.

Второй фундаментальный вектор - повторное использование подписей (Signature Reuse). В июле 2026 года на этом погорел Wanchain Bridge (мост между Cardano и BNB Chain, потери около $13 млн). Логика валидатора не помечала использованные валидные криптографические подписи как spent (завершенные) в глобальном сторедже контракта. Нападающий взял ранее проведенную легитимную транзакцию и повторно отправил её вызов, изменив вектор получателя. Контракт проверил криптографическую подпись валидатора — она оказалась математически верной, так как была сгенерирована ранее и выдал средства повторно.

Хроника взломов 2026 года

Только за несколько месяцев 2026 года уязвимости в мостах и связанной кроссчейн-инфраструктуре суммарно вывели из DeFi-экосистемы сотни миллионов долларов.

Проект / МостДатаСумма ущербаВектор атаки / Техническая причина
Aethir Bridge (OFT Adapter)Апрель 2026~$5.0MЛогическая ошибка в кроссчейн-сообщениях при переводе токенов ATH между BNB Chain и Tron
Syscoin BridgeИюнь 2026~$10.0MПарсинг-ошибка SPV-доказательства. Выпуск 5 млрд SYS без реального залога
Wanchain BridgeИюль 2026~$13.0MУязвимость повторного использования подписи (Signature-reuse flaw) на стороне валидатора
AFX Trade BridgeИюль 2026$24.15MКомпрометация приватных ключей валидаторов моста на Arbitrum
Verus Ethereum BridgeИюль 2026$7.54MОтсутствие проверки соответствия резервов (вызов вывода без валидации залога)

Ошибки разработчиков здесь носят систематический характер. Главная причина - стремительная гонка за TVL (Total Value Locked) и попытка выпустить кроссчейн-продукт раньше конкурентов. Разработчики пренебрегают созданием систем отслеживания состояний (state machine audit) и пишут сложную, громоздкую логику парсинга данных на Solidity или Go, вместо того чтобы переносить сложные проверки в верифицируемые ZK-схемы (Zero-Knowledge Proofs).

Полноценный продакшн-код безопасного моста (Solidity 0.8.24)

Пример контракта SecureBridgeVault для сети-источника. В нем реализована защита от атаки повторного использования подписей (Replay Attack), защищенный механизм EIP-712 для проверки подписей релея, защита от повторного входа (Reentrancy Guard) и строгое логирование состояний 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 Продакшн-хранилище с единым идентификатором depositHash, 
* жестким контролем M-of-N релеев через EnumerableSet и защитой от ошибок конфигурации.
*/
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 для подписи unlock, идентичный по структуре с lock
   bytes32 private constant UNLOCK_TYPEHASH = keccak256(
       "Unlock(bytes32 depositHash,address token,address sender,address recipient,uint256 amount,uint256 sourceChainId,uint256 targetChainId,uint256 depositId)"
   );
   // Реестр релеев в хранилище контракта
   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; // Фиксация локально созданных депозитов
   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;
   }
   // --- УПРАВЛЕНИЕ РЕЛЕЯМИ И ГОВЕРНАНС ---
   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();
   }
   // --- АДМИНИСТРАТИВНЫЕ НАСТРОЙКИ СЕТЕЙ И ТОКЕНОВ ---
   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);
   }
   // --- БЛОКИРОВКА И РАЗБЛОКИРОВКА ---
   /**
    * @notice Генерация детерминированного depositHash и фиксация блокировки
    */
   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;
       }
       // Безопасное вычисление единого depositHash через 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 Вывод средств по единому depositHash с валидацией M-of-N подписей
    * @dev Подписи В ОБЯЗАТЕЛЬНОМ ПОРЯДКЕ должны быть отсортированы Off-chain по возрастанию адресов подписывантов.
    */
   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();
       // Проверка: совпадает ли хэш параметров с заявленным depositHash
       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();
       // Проверка сортировки адресов и порога подписей
       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);
   }
}

Мосты остаются самым слабым звеном в Web3-инфраструктуре из-за того, что они пытаются связать консенсусы двух совершенно разных систем с помощью хрупкой прослойки кода вне блокчейна. Любая незначительная ошибка в валидации формата байтов, проверке структуры EIP-712 или банальная утечка ключа из серверов валидатора мгновенно превращает смарт-контракт с сотнями миллионов долларов в открытую кормушку для хакеров.

Сделать краткую выжимку этой статьи с помощью:

FAQ

Кроссчейн-мосты валидируют изменения состояния с помощью криптографических доказательств (таких как SPV Merkle roots или Zero-Knowledge proofs), проверяемых на стороне целевой сети или через распределенную сеть релейеров с использованием пороговых подписей (Threshold Signatures, например ECDSA или BLS). При блокировке токенов в смарт-контракте исходной сети релейеры подтверждают финализацию события и передают подписанный payload в контракт назначения, который проверяет соответствие подписей структуре EIP-712 и пороговому значению M-of-N перед минтингом обернутых активов.

Взломы мостов происходят преимущественно из-за ошибок парсинга криптографических доказательств (отсутствие проверок длины и структуры байтовых массивов), уязвимостей повторного использования подписей (signature-reuse) и компрометации приватных ключей узлов валидаторов в multisig-схемах. Злоумышленники эксплуатируют логические баги парсеров для внедрения поддельных хэшей событий блокировки или используют скомпрометированные ключи для авторизации вывода средств без фактического обеспечения в депозите.

Защита от replay-атак реализуется через хэширование структурированных данных EIP-712 с использованием доменного сепаратора (block.chainid и адрес контракта), а также сохранение обработанных хэшей транзакций в постоянном хранилище (executedHashes). Логика контракта должна проверять статус хэша в маппинге до вызова функции перевода средств, помечать хэш как исполненный перед отправкой токенов и требовать строгой сортировки массива подписей по возрастанию адресов для исключения дублирования ключей при валидации порога 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.

...

Поделитесь своим мнением

Ваш e-mail не будет опубликован. Обязательные поля отмечены *