- name
- cross-chain
- description
- Auto-loaded by cross-chain-auditor agent during Phase 2. Provides detection patterns for: missing source chain/address validation, replay attacks, message ordering, chain-specific code, finality assumptions. Core artifact: Cross-Chain Message Flow diagram.
- user-invocable
- false
# Cross-Chain Vulnerability Patterns
This skill provides comprehensive knowledge for identifying cross-chain and bridge vulnerabilities in smart contracts.
## Overview
Cross-chain communication introduces trust assumptions at chain boundaries. Message verification is the entire security model - if messages can be forged or replayed, billions can be stolen.
## Historical Context
| Hack | Loss | Root Cause |
|------|------|------------|
| Ronin Bridge | $624M | Compromised validators |
| Wormhole | $326M | Signature verification bypass |
| Nomad | $190M | Initialization bug allowed fake proofs |
| Harmony Horizon | $100M | Compromised validators |
---
## Vulnerability Categories
### 1. Missing Source Chain Validation
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Accepts from any chain
function receiveMessage(bytes calldata payload) external {
// No srcChainId check!
(address recipient, uint256 amount) = abi.decode(payload, (address, uint256));
token.mint(recipient, amount);
}
```
**Secure Pattern:**
```solidity
function receiveMessage(uint16 srcChainId, bytes calldata payload) external {
require(msg.sender == trustedEndpoint, "Invalid endpoint");
require(srcChainId == ALLOWED_SOURCE_CHAIN, "Invalid source chain");
// Process...
}
```
**Detection:**
```
Grep("receiveMessage|onMessage|handleMessage", glob="**/*.sol")
```
Check if srcChainId is validated.
---
### 2. Missing Source Address Validation
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Accepts from any address on source chain
function lzReceive(
uint16 _srcChainId,
bytes calldata _srcAddress,
uint64 _nonce,
bytes calldata _payload
) external {
require(msg.sender == lzEndpoint);
require(_srcChainId == trustedChain);
// Missing: _srcAddress validation!
_processPayload(_payload);
}
```
**Secure Pattern:**
```solidity
function lzReceive(
uint16 _srcChainId,
bytes calldata _srcAddress,
uint64 _nonce,
bytes calldata _payload
) external {
require(msg.sender == lzEndpoint, "Invalid endpoint");
require(_srcChainId == trustedChain, "Invalid chain");
require(
keccak256(_srcAddress) == keccak256(abi.encodePacked(trustedRemote)),
"Invalid source"
);
_processPayload(_payload);
}
```
---
### 3. Replay Attacks
**Vulnerable Pattern:**
```solidity
// DANGEROUS: No nonce tracking
function processMessage(bytes32 messageId, bytes calldata data) external {
// Can be replayed!
_execute(data);
}
```
**Secure Pattern:**
```solidity
mapping(bytes32 => bool) public processedMessages;
function processMessage(bytes32 messageId, bytes calldata data) external {
require(!processedMessages[messageId], "Already processed");
processedMessages[messageId] = true;
_execute(data);
}
```
**Detection:**
```
Grep("messageId|nonce|processed", glob="**/*.sol")
```
Check if there's uniqueness tracking.
---
### 4. Message Ordering Issues
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Assumes sequential processing
function receiveDeposit(uint256 depositId, uint256 amount) external {
require(depositId == lastDepositId + 1, "Out of order"); // Can block!
lastDepositId = depositId;
}
```
**Risk:** One failed message blocks all subsequent messages.
**Secure Pattern:**
```solidity
// Allow out-of-order processing with idempotency
function receiveDeposit(bytes32 depositHash, uint256 amount) external {
require(!processed[depositHash], "Already processed");
processed[depositHash] = true;
_processDeposit(amount);
}
```
---
### 5. Chain-Specific Code Assumptions
#### block.number Differences
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Different meaning on L2
uint256 blockAge = block.number - startBlock;
require(blockAge >= REQUIRED_BLOCKS, "Too early");
```
**Chain Differences:**
| Chain | block.number Behavior |
|-------|----------------------|
| Ethereum | L1 block number |
| Arbitrum | L1 block number |
| Optimism | L2 block number |
| Polygon | L2 block number |
#### PUSH0 Opcode (Solidity 0.8.20+)
**Risk:** Contracts compiled with Solidity >=0.8.20 use PUSH0, not supported on:
- zkSync Era
- Arbitrum (until Dencun)
- Some other L2s
**Detection:**
```
Grep("pragma solidity", glob="**/*.sol")
```
Check if version >= 0.8.20 and deploying to chains without PUSH0.
---
### 6. Bridge Lock/Mint Attacks
**Vulnerable Pattern:**
```solidity
// Source chain: Lock
function bridgeOut(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
emit BridgeOut(msg.sender, amount);
}
// Destination chain: Mint
function bridgeIn(address user, uint256 amount) external onlyRelayer {
// If relayer is compromised or message forged...
wrappedToken.mint(user, amount);
}
```
**Attack Vectors:**
1. Forge message without locking
2. Replay lock message
3. Lock on chain A, mint on chains B and C
**Mitigations:**
- Multi-sig relayers
- Optimistic verification with fraud proofs
- Zero-knowledge proofs
- Message hash verification
---
### 7. Finality Assumptions
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Assumes instant finality
function onMessageReceived(bytes calldata proof) external {
// Process immediately after 1 confirmation
_execute(proof);
}
```
**Risk:** Chain reorganizations can invalidate processed messages.
**Chain Finality:**
| Chain | Finality Time | Confirmations |
|-------|---------------|---------------|
| Ethereum | ~15 min | 32 slots |
| Polygon | ~30 min | 256 blocks |
| BSC | ~3 min | 15 blocks |
| Arbitrum | 7 days (challenged) | Instant (sequencer) |
---
## LayerZero Specific Checks
```solidity
// Required validations for LayerZero
function lzReceive(
uint16 _srcChainId,
bytes calldata _srcAddress,
uint64 _nonce,
bytes calldata _payload
) external override {
// 1. Endpoint check
require(msg.sender == address(lzEndpoint), "Invalid endpoint");
// 2. Chain check
require(_srcChainId == trustedRemoteChain, "Invalid chain");
// 3. Source address check
bytes memory trustedRemote = trustedRemoteLookup[_srcChainId];
require(
_srcAddress.length == trustedRemote.length &&
keccak256(_srcAddress) == keccak256(trustedRemote),
"Invalid source"
);
// 4. Process payload
_nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload);
}
```
---
## Cross-Chain Audit Checklist
- [ ] Source chain ID validated
- [ ] Source address validated
- [ ] Message nonce/ID tracked for replay protection
- [ ] Endpoint address verified (msg.sender check)
- [ ] Chain-specific code identified (block.number, PUSH0)
- [ ] Finality assumptions appropriate
- [ ] Out-of-order messages handled
- [ ] Failed message recovery possible
## Severity Classification
### Critical
- Missing source chain validation
- Missing source address validation
- No replay protection
- Signature verification bypass
### High
- Insufficient finality wait
- Message ordering DoS
- Cross-chain reentrancy
### Medium
- Chain-specific code assumptions
- Incomplete trusted remote setup
- Missing event emission
---
## Modern Bridge Protocols (2025-2026)
### Chainlink CCIP
**Key Security Checks:**
```solidity
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
contract MyCCIPReceiver is CCIPReceiver {
mapping(uint64 => bool) public allowedSourceChains;
mapping(address => bool) public allowedSenders;
function _ccipReceive(Client.Any2EVMMessage memory message) internal override {
// 1. Chain validation
require(allowedSourceChains[message.sourceChainSelector], "Invalid chain");
// 2. Sender validation
address sender = abi.decode(message.sender, (address));
require(allowedSenders[sender], "Invalid sender");
// 3. Process message
_processMessage(message.data);
}
}
```
**CCIP-Specific Risks:**
- `sourceChainSelector` not validated
- `message.sender` not decoded/validated
- Rate limits not enforced
- Token transfer vs message confusion
**Search Queries:**
```
Grep("CCIP|ccip|CCIPReceiver|sourceChainSelector", glob="**/*.sol")
```
---
### Hyperlane
**Key Security Checks:**
```solidity
import {IMessageRecipient} from "@hyperlane-xyz/core/contracts/interfaces/IMessageRecipient.sol";
contract MyHyperlaneReceiver is IMessageRecipient {
address public mailbox;
mapping(uint32 => bytes32) public trustedSenders;
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) external payable override {
// 1. Mailbox validation
require(msg.sender == mailbox, "Invalid mailbox");
// 2. Origin chain validation
require(trustedSenders[_origin] != bytes32(0), "Unknown origin");
// 3. Sender validation
require(_sender == trustedSenders[_origin], "Invalid sender");
// 4. Process
_processMessage(_message);
}
}
```
**Hyperlane-Specific Risks:**
- Mailbox address not validated
- `_sender` is bytes32 (address left-padded)
- Interchain Security Module (ISM) misconfiguration
- Missing origin validation
**Search Queries:**
```
Grep("Hyperlane|hyperlane|IMessageRecipient|mailbox", glob="**/*.sol")
```
---
### Wormhole
**Key Security Checks:**
```solidity
import {IWormhole} from "./interfaces/IWormhole.sol";
contract MyWormholeReceiver {
IWormhole public wormhole;
mapping(uint16 => bytes32) public registeredEmitters;
mapping(bytes32 => bool) public processedMessages;
function receiveMessage(bytes memory encodedVM) external {
// 1. Parse and verify VAA
(IWormhole.VM memory vm, bool valid, string memory reason) =
wormhole.parseAndVerifyVM(encodedVM);
require(valid, reason);
// 2. Replay protection
require(!processedMessages[vm.hash], "Already processed");
processedMessages[vm.hash] = true;
// 3. Emitter validation
require(
registeredEmitters[vm.emitterChainId] == vm.emitterAddress,
"Invalid emitter"
);
// 4. Process payload
_processPayload(vm.payload);
}
}
```
**Wormhole-Specific Risks:**
- VAA not verified via `parseAndVerifyVM`
- Emitter chain/address not validated (Wormhole hack root cause)
- VAA hash not tracked for replay
- Guardian set changes
**Search Queries:**
```
Grep("Wormhole|wormhole|parseAndVerifyVM|emitterChainId", glob="**/*.sol")
```
---
### LayerZero v2
**Key Changes from v1:**
```solidity
// v2 uses OApp pattern
import { OApp } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol";
contract MyOApp is OApp {
constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {}
function _lzReceive(
Origin calldata _origin, // New struct format
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) internal override {
// Origin contains srcEid (endpoint ID), sender, nonce
require(_origin.srcEid == trustedEndpointId, "Invalid source");
require(_origin.sender == trustedSender, "Invalid sender");
_processMessage(_message);
}
}
```
**v2-Specific Considerations:**
- New `Origin` struct format
- `_guid` for message tracking
- Executor and extraData parameters
- DVN (Decentralized Verifier Network) configuration
**Search Queries:**
```
Grep("OApp|lz-evm-oapp|srcEid|_lzReceive", glob="**/*.sol")
```
---
### Axelar General Message Passing (GMP)
Voir sur GitHub