Cross-chain bridge to żaden magiczny tunel łączący sieci, а po prostu zwykły pośrednik finansowy. Składa się z dwóch odizolowanych smart kontraktów oraz węzła przekaźnikowego off-chain (relay node). Blockchainy z natury nie potrafią się ze sobą bezpośrednio komunikować: Ethereum nie ma zielonego pojęcia, co dzieje się wewnątrz Solany, a Bitcoin nawet nie słyszał o istnieniu Arbitrum.
Kiedy użytkownik przerzuca 10 ETH z sieci Ethereum do Arbitrum, bridge wcale nie przesyła fizycznie tych tokenów. Zamiast tego blokuje (Lock) oryginalne 10 ETH w smart kontrakcie na sieci źródłowej i jednocześnie bije (Mint) 10 syntetycznych, „opakowanych” ekwiwalentów (wETH) na sieci docelowej.

Główna podatność architektoniczna całej tej mechaniki leży w warstwie pośredniej – czyli w relayerach i walidatorach odpowiedzialnych za weryfikację dowodów (proof verification). Jeśli ta warstwa potwierdzi kontraktowi w sieci docelowej, że środki zostały uczciwie zablokowane, kontrakt bezdyskusyjnie wydrukuje nowe aktywa. Sfałszowanie takiego dowodu pozwala wyczyścić pule płynności dosłownie do zera.
Dlaczego mosty padają: fałszowanie dowodów i wycieki kluczy
Większość krytycznych exploitów na cross-chain bridgach sprowadza się do dwóch głównych kategorii: błędów kryptograficznych/logicznych w walidacji oraz przejęcia infrastruktury off-chain (czyli wycieku kluczy prywatnych z multisigów).
W czerwcu 2026 roku architektoniczną wtopę przy obsłudze dowodów zaliczył Syscoin Bridge (straty opiewały na 10 mln USD). Most wykorzystuje mechanizm SPV (Simplified Payment Verification) do weryfikacji spalenia/zablokowania monet po stronie UTXO w Syscoinie przed wyemitowaniem aktywów po stronie EVM (NEVM).
Atak nie wynikał ze złamania kryptografii, ale ze zwykłego błędu w parserze relayera. Haker przygotował pakiet exploitujący z zaburzoną strukturą bajtów. Przez brak rygorystycznej walidacji długości i formatu wejściowej tablicy bajtów, parser potraktował sfałszowany hash jako prawidłowy dowód SPV potwierdzający realne spalenie. W efekcie bridge autoryzował wyemitowanie 5 miliardów tokenów SYS w sieci UTXO bez zablokowania choćby jednej monety na NEVM.
Drugim fundamentalnym wektorem jest ponowne użycie podpisów (Signature Reuse). W lipcu 2026 roku dokładnie na tym wyłożył się Wanchain Bridge (most łączący Cardano z BNB Chain, wyparowało około 13 mln USD). Logika walidatora nie oznaczała wykorzystanych już, poprawnych podpisów kryptograficznych jako zużyte (spent) w globalnym storage'u kontraktu. Atakujący przechwycił wcześniej przeprowadzoną, legalną transakcję i wysłał ją ponownie, zmieniając jedynie adres odbiorcy. Kontrakt sprawdził podpis kryptograficzny walidatora — matematycznie wszystko się zgadzało, bo podpis wygenerowano wcześniej — i bezmózgowo wypłacił środki po raz drugi.
Kronika haków z 2026 roku
Zaledwie w ciągu kilku miesięcy 2026 roku dziury w bridge'ach i przyległej infrastrukturze cross-chain wyciągnęły z ekosystemu DeFi setki milionów dolarów.
| Projekt / Bridge | Data | Straty | Wektor ataku / Przyczyna techniczna |
|---|---|---|---|
| Aethir Bridge (OFT Adapter) | Kwiecień 2026 | ~$5.0M | Błąd logiczny w komunikatach cross-chain podczas transferu tokenów ATH pomiędzy BNB Chain a Tron |
| Syscoin Bridge | Czerwiec 2026 | ~$10.0M | Błąd parsowania dowodu SPV. Wyemitowanie 5 mld SYS bez pokrycia w depozycie |
| Wanchain Bridge | Lipiec 2026 | ~$13.0M | Podatność typu Signature Reuse po stronie walidatora |
| AFX Trade Bridge | Lipiec 2026 | $24.15M | Przejęcie kluczy prywatnych walidatorów mostu w sieci Arbitrum |
| Verus Ethereum Bridge | Lipiec 2026 | $7.54M | Brak weryfikacji pokrycia w rezerwach (wywołanie wypłaty bez walidacji zabezpieczenia) |
Błędy deweloperów mają tutaj charakter systemowy. Główną przyczyną jest ślepy wyścig po TVL (Total Value Locked) i dowożenie produktów cross-chain na wczoraj, byle uciec konkurencji. Zamiast przenieść skomplikowaną walidację do weryfikowalnych obwodów ZK (Zero-Knowledge Proofs), devowie olewają audyty maszyn stanów (state machine audit) i piszą karkołomną, podatną na błędy logikę parsowania bezpośrednio w Solidity albo Go.
Produkcyjny kod bezpiecznego bridge'a (Solidity 0.8.24)
Poniżej znajdziesz wzorcowy kontrakt SecureBridgeVault dla sieci źródłowej. Zaimplementowano w nim ochronę przed atakami typu Replay Attack, bezpieczny mechanizm EIP-712 do weryfikacji podpisów relayera, Reentrancy Guard oraz rygorystyczne logowanie stanów nonce.
// 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 Produkcyjny vault z unikalnym identyfikatorem depositHash,
* ścisłą kontrolą relayerów M-of-N przez EnumerableSet oraz ochroną przed błędami konfiguracji.
*/
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");
// Unikalny Typehash dla podpisu unlock, strukturalnie identyczny z lock
bytes32 private constant UNLOCK_TYPEHASH = keccak256(
"Unlock(bytes32 depositHash,address token,address sender,address recipient,uint256 amount,uint256 sourceChainId,uint256 targetChainId,uint256 depositId)"
);
// Rejestr relayerów w pamięci kontraktu
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; // Rejestracja lokalnie utworzonych depozytów
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;
}
// --- ZARZĄDZANIE RELAYERAMI I GOVERNANCE ---
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();
}
// --- USTAWIENIA ADMINISTRACYJNE SIECI I TOKENÓW ---
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);
}
// --- BLOKOWANIE I ODBLOKOWYWANIE ---
/**
* @notice Generowanie deterministycznego depositHash oraz rejestracja blokady
*/
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;
}
// Bezpieczne wyliczanie unikalnego depositHash poprzez 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 Wypłata środków na podstawie spójnego depositHash z walidacją podpisów M-of-N
* @dev Podpisy OBOWIĄZKOWO muszą być posortowane off-chain rosnąco według adresów podpisujących.
*/
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();
// Weryfikacja: czy hash parametrów zgadza się ze zgłoszonym 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();
// Sprawdzenie kolejności adresów i progu podpisów
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);
}
}Cross-chain bridge'e pozostają najsłabszym ogniwem w całej infrastrukturze Web3, a wszystko dlatego, że próbują spiąć ze sobą konsensus dwóch całkowicie odmiennych systemów za pomocą kruchej warstwy kodu off-chain. Drobna obsuwa przy walidacji układu bajtów, niedociągnięcie w weryfikacji struktury EIP-712 czy zwykły wyciek klucza z serwera walidatora potrafią w ułamku sekundy zamienić smart kontrakt trzymający setki milionów dolarów w darmowy bankomat dla hakerów.