Naciśnij ESC, aby zamknąć

Jak działają mosty krypto i dlaczego są atakowane? Анализа 2026

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.

Relay node
 

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 / BridgeDataStratyWektor ataku / Przyczyna techniczna
Aethir Bridge (OFT Adapter)Kwiecień 2026~$5.0MBłąd logiczny w komunikatach cross-chain podczas transferu tokenów ATH pomiędzy BNB Chain a Tron
Syscoin BridgeCzerwiec 2026~$10.0MBłąd parsowania dowodu SPV. Wyemitowanie 5 mld SYS bez pokrycia w depozycie
Wanchain BridgeLipiec 2026~$13.0MPodatność typu Signature Reuse po stronie walidatora
AFX Trade BridgeLipiec 2026$24.15MPrzejęcie kluczy prywatnych walidatorów mostu w sieci Arbitrum
Verus Ethereum BridgeLipiec 2026$7.54MBrak 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.

Podsumuj ten wpis na blogu za pomocą:

FAQ

Bridge cross-chain weryfikuje zmiany stanu poprzez dowody kryptograficzne (takie jak korzenie drzew Merkle SPV lub dowody ZK) zatwierdzane na łańcuchu docelowym bądź przez rozproszoną sieć relayerów stosującą podpisy progowe BLS lub ECDSA. Po zablokowaniu aktywów w smart kontrakcie sieci źródłowej, sieć walidatorów generuje podpisany payload i przekazuje go do kontraktu docelowego, który po sprawdzeniu poprawności podpisów względem wymaganego progu M-of-N wybiija syntetyczne tokeny.

Najważniejszymi przyczynami incydentów są błędy parsowania dowodów (brak weryfikacji struktury bajtów i długości tablic), podatności typu signature-reuse umożliwiające ponowne przetworzenie tych samych podpisów walidatorów oraz wycieki kluczy prywatnych w węzłach multisig. Napastnicy wykorzystują usterki logiki smart kontraktów do wstrzykiwania fałszywych haszy zdarzeń blokady lub przejmują próg podpisów wymaganych do autoryzacji wypłat bez pokrycia w depozycie.

Ochrona przed atakami typu replay wymaga wdrożenia standardu EIP-712 zawierającego identyfikator sieci (block.chainid) i adres kontraktu w separatorze domeny oraz zapisywania przetworzonych unikalnych identyfikatorów depozytów w pamięci trwałej (executedHashes). Kod musi weryfikować stan mapowania przed wywołaniem ecrecover, natychmiast oznaczać dany hash jako wykupiony przed transferem środków i wymagać sortowania podpisów w celu wyeliminowania duplikatów.
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.

...

Dodaj opinię

Twój adres e-mail nie zostanie opublikowany. Obowiązkowe pola są oznaczone*