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

Главная архитектурная уязвимость кроссчейн-механики скрыта в промежуточном слое - релеях и валидаторах, выполняющих проверку (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 или банальная утечка ключа из серверов валидатора мгновенно превращает смарт-контракт с сотнями миллионов долларов в открытую кормушку для хакеров.