Most cross-chain security writing starts and ends with bridges: lock tokens here, mint them there, and pray the multisig doesn’t get drained. General message passing is a strictly larger problem. When your protocol sends arbitrary calldata across chains — minting governance votes, rebalancing collateral, routing oracle updates, or orchestrating multi-step DeFi flows — the attack surface expands far beyond custody of locked assets. The message itself becomes the exploit vector.
This article works through that larger surface systematically: how the leading protocols model message passing, the replay and ordering traps that trip up application developers, the trust assumptions hiding inside executor and verifier design, what happens when a chain reorganizes or forks under a message in flight, and how to design protocols that degrade gracefully rather than catastrophically when cross-chain delivery fails.
1. The General Message Passing Model
Cross-chain messaging is more general than token transfers: it lets smart contracts on one chain send arbitrary data to contracts on another chain. The four dominant EVM-compatible protocols each route that data differently.
A typical cross-chain dApp uses messaging roughly like this: a user triggers a transaction on the source chain, the app’s smart contract emits a message with encoded data, a messaging protocol observes and validates that event and delivers a message to the destination chain, and a contract on the destination chain decodes the message and executes logic.
Where the protocols diverge is in who validates the event and what trust assumptions that validation imports.
LayerZero
LayerZero is modular-verifier-based, meaning its security depends on the independence and honesty of the verifiers you configure — Oracle, Relayer, or DVN sets. It does not use a global validator network; instead, applications choose their own verifier combinations. In LayerZero v2, Oracle and Relayer were replaced by a “Decentralized Validator Network” (DVN) and “Executor”, which are permissionless: any external network can become a DVN and applications can choose any combination of them to approve a message.
Wormhole
Wormhole uses a Guardian network of 19 validators to verify cross-chain events, primarily focused on token bridges and NFT transfers with strong adoption for moving liquidity between chains. When 13 out of 19 validators agree, the cross-chain message is considered valid. Wormhole signs these attestations as Verified Action Approvals (VAAs), which the destination contract must verify on-chain before executing.
Axelar
Axelar is a proof-of-stake interoperability layer with General Message Passing (GMP) and the Axelar Virtual Machine (AVM). Axelar’s trust approach relies on the economic guarantee of a network of validators to provide a higher degree of decentralization than pure multisig or off-chain oracle designs. The Axelar chain itself reaches consensus on message validity before the gateway contract on the destination chain is authorized to execute.
Chainlink CCIP
Chainlink CCIP leverages Chainlink’s existing oracle network to transmit messages between blockchains. CCIP adds a Risk Management Network — a secondary, independent set of nodes that monitors all lanes and can halt message delivery if anomalous patterns are detected. This makes it distinctive among the four: it ships an out-of-box circuit breaker rather than leaving defense entirely to the application.
The Common Abstraction
Across all four, the canonical lifecycle is:
- Source contract calls
send(dstChainId, dstAddress, payload, options). - An off-chain process (guardian quorum, DVN set, Axelar validator set, CCIP DON) observes and attests.
- An executor submits the attestation + payload to the destination endpoint contract.
- The endpoint validates the attestation and calls the destination application contract.
The application developer controls steps 1 and 4. Everything in between is the protocol’s trust boundary — and that boundary is where most vulnerabilities originate.
2. Message Replay and the Nonce + Chain ID Fix
Inadequate nonce validation treats signed payloads as universally valid across chains. Attackers intercept a legitimate message from Chain A, replay it on Chain B, and trigger duplicate actions like minting or transfers. Without chain-specific checks, the destination contract executes the payload again, draining funds.
In cross-chain messaging this is subtler than in ordinary transaction replay. The destination chain never issued the nonce — only the source chain did. The receiving contract must therefore maintain its own nonce ledger and validate the message’s embedded nonce against it.
EIP-712 binds every signature to a specific contract name, version, chainId, and verifying contract address — a single mechanism that prevents both cross-chain and cross-contract replay.
A minimal, audit-ready receive handler should look like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Minimal cross-chain message receiver with replay protection.
abstract contract SecureMessageReceiver {
// srcChainId => srcSender => nonce => consumed
mapping(uint32 => mapping(bytes32 => mapping(uint64 => bool))) private _usedNonces;
event MessageExecuted(
uint32 indexed srcChainId,
bytes32 indexed srcSender,
uint64 nonce,
bytes payload
);
error AlreadyExecuted(uint32 srcChainId, bytes32 srcSender, uint64 nonce);
error WrongDestChain(uint32 expected, uint32 actual);
/// @dev Must be called after the messaging layer has authenticated the sender.
function _execute(
uint32 srcChainId,
bytes32 srcSender,
uint64 nonce,
uint32 dstChainId, // embedded in the payload by the sender
bytes calldata payload
) internal {
// 1. Verify this message was actually intended for THIS chain.
if (dstChainId != uint32(block.chainid))
revert WrongDestChain(uint32(block.chainid), dstChainId);
// 2. Verify the nonce has not been consumed before.
if (_usedNonces[srcChainId][srcSender][nonce])
revert AlreadyExecuted(srcChainId, srcSender, nonce);
// 3. Mark consumed BEFORE any external call (reentrancy guard).
_usedNonces[srcChainId][srcSender][nonce] = true;
emit MessageExecuted(srcChainId, srcSender, nonce, payload);
_handleMessage(srcChainId, srcSender, payload);
}
function _handleMessage(
uint32 srcChainId,
bytes32 srcSender,
bytes calldata payload
) internal virtual;
}
Two independent fields protect against two independent attacks:
| Field | Prevents |
|---|---|
dstChainId embedded in payload | Replaying the same message on a sibling chain |
| Per-(srcChain, srcSender) nonce | Replaying the same message twice on the same destination |
Note that nonces are consumed before the downstream call. A common implementation flaw is only incrementing a user’s nonce after a transaction succeeds. If a transaction fails due to a temporary condition, the nonce remains unchanged — allowing the attacker to replay the message repeatedly until it succeeds.
3. Message Ordering Assumptions and Out-of-Order Delivery
Most general message passing layers offer optional, not guaranteed, ordering. LayerZero’s default channel is ordered; an application can opt into unordered delivery for throughput. Wormhole VAAs carry no inherent sequencing: they are emitted and consumed independently. Axelar GMP has explicit sequence numbers per (srcChain, srcContract, dstChain, dstContract) tuple.
Even when authentication is solid, ordering assumptions silently leak into app logic. A cross-chain “deposit then borrow” flow that assumes ordered delivery can become a money printer if messages arrive out of order, arrive twice, or arrive after a state change you did not bind into the message domain. This is the exact place where IBC’s explicit ordered vs. unordered channels, sequence tracking, and timeouts are useful as a design reference.
In the absence of mechanisms to enforce cross-chain transaction ordering, monopolizing relayers can also reorder transactions to perform front-running and sandwich attacks.
Consider a cross-chain lending protocol. Message A increases a user’s collateral; Message B borrows against it. If B arrives first, the borrow check fails, or worse — if the receiving contract doesn’t revert atomically — state becomes inconsistent in a way no single-chain invariant would ever produce.
The correct fix is to encode state dependencies explicitly in the message payload:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Message with explicit precondition binding.
struct CrossChainBorrow {
address user;
uint256 borrowAmount;
// Snapshot of collateral value AT the time the message was sent.
// The receiver rejects if current collateral < snapshotCollateral.
uint256 snapshotCollateral;
uint64 nonce;
uint32 dstChainId;
}
contract LendingReceiver {
mapping(address => uint256) public collateral;
error StaleMessage(uint256 required, uint256 current);
error InsufficientCollateral();
function receiveBorrow(CrossChainBorrow calldata msg_) external {
// Ordered-delivery guard: reject if protocol state has
// diverged from what the source chain saw.
if (collateral[msg_.user] < msg_.snapshotCollateral)
revert StaleMessage(msg_.snapshotCollateral, collateral[msg_.user]);
uint256 maxBorrow = collateral[msg_.user] * 75 / 100; // 75% LTV
if (msg_.borrowAmount > maxBorrow)
revert InsufficientCollateral();
// proceed with borrow ...
}
}
Embedding a snapshot of the state that justified the action on the source chain turns ordering into an application-level invariant rather than a protocol-layer guarantee.
4. Executor Trust Models
An executor is the off-chain entity that submits a transaction to the destination chain. In LayerZero v2 the DVN verifies but a separate Executor pays for and delivers gas. In Wormhole, relayers are permissionless but trustless with respect to content — they cannot forge a VAA, only censor or delay one. In CCIP, the off-chain reporting (OCR) committee is also the deliverer.
This separation of verification from delivery creates a subtle trust surface:
- Censorship: An executor can simply decline to submit a message. The message remains valid; it just never lands. Protocols must expose a permissionless re-execution path so any party can manually submit a verified message.
- Gas griefing: A gas griefing attack takes place when a user provides just enough gas to run the specified smart contract but not enough to run its sub-calls. This flaw can be found in smart contract methods that either fail to check the amount of gas needed to perform a sub-call or fail to check the call’s actual returned value. In cross-chain context, the executor chooses how much gas to forward to the destination call. When a user sends a request to the relayer, the user specifies the amount of gas to include in the transaction and digitally signs their request. However, the relayer might not respect the gas limit requested by the user, and send a lower amount.
The safe pattern is to cryptographically bind the gas limit in the message itself and verify it on-chain:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Endpoint that enforces the gas limit committed on the source chain.
contract GasEnforcingEndpoint {
error InsufficientGasForwarded(uint256 required, uint256 available);
/// @param payload Decoded application payload.
/// @param gasLimit Gas limit committed by the sender on the source chain
/// and verified as part of the attestation by the DVN/Guardian.
/// @param target Destination application contract.
function deliver(
bytes calldata payload,
uint256 gasLimit,
address target
) external {
// Ensure the executor has actually forwarded the committed gas.
// gasleft() here must be >> gasLimit to cover overhead.
if (gasleft() < gasLimit + 10_000)
revert InsufficientGasForwarded(gasLimit, gasleft());
// Low-level call with explicit gas forwarding.
(bool success, bytes memory returnData) =
target.call{gas: gasLimit}(payload);
if (!success) {
// Store for permissionless retry rather than silently dropping.
_storeFailedMessage(payload, gasLimit, target, returnData);
}
}
// --- retry storage ---
struct FailedMessage {
bytes payload;
uint256 gasLimit;
address target;
bytes revertData;
}
mapping(bytes32 => FailedMessage) public failedMessages;
function _storeFailedMessage(
bytes calldata payload,
uint256 gasLimit,
address target,
bytes memory revertData
) internal {
bytes32 key = keccak256(abi.encode(payload, gasLimit, target));
failedMessages[key] = FailedMessage(payload, gasLimit, target, revertData);
}
function retryFailedMessage(bytes32 key) external {
FailedMessage memory m = failedMessages[key];
require(m.target != address(0), "no such message");
delete failedMessages[key];
(bool success,) = m.target.call{gas: m.gasLimit}(m.payload);
require(success, "retry failed");
}
}
If the relayer’s call to the forwarding contract succeeds, but the sub-call the user wants fails, then the relayer can “blame” the user for sending a transaction that reverts when the real reason was the subcall ran out of gas due to the relayer not sending enough. The _storeFailedMessage pattern breaks that ambiguity: delivery is acknowledged but execution is separated, and anyone can retry.
5. Optimistic vs. Attested Message Verification
Every cross-chain messaging protocol answers the same question: how does the destination chain know that an event on the source chain actually happened? The answer is always some form of attestation, and the security of the entire system reduces to the trustworthiness of whoever or whatever provides that attestation.
Attested Verification
Externally verified bridges rely on a third-party set of actors to attest the correctness of data transported between chains, typically represented as a multisig, MPC system, PoS validator set, or oracle/relay. External verification is easy to extend to any chain and can support generalized messages at low latency. However, the security of a message passed between chains fully relies on the verifier set, meaning security scales with the size of the set. In most cases, externally verified bridges have far less security than the underlying chains, implying that they are not trust-minimized.
An oracle watches the source chain, detects an outbound message event, and posts a signed attestation. A separate relayer picks up that attestation and submits the message to a receiving contract on the destination chain. The receiving contract checks that the oracle’s signature is valid before executing. Security here is a function of the oracle’s honesty. If the oracle is compromised or colluding with the relayer, arbitrary messages can be injected.
Optimistic Verification
Optimistic bridges assume messages are valid and provide a challenge window for fraud proofs. If no proof is submitted (typically 30 minutes to a few hours), the message finalizes. The advantage is low verification cost. The disadvantage is latency and dependency on at least one honest watcher monitoring every message.
The Nomad exploit is the canonical failure case. A routine upgrade set the trusted root of the message verification Merkle tree to zero, making any message with a zero-initialized proof automatically valid. Once the first attacker’s technique was visible on-chain, hundreds of copycats replicated the transaction. The exploit proved that optimistic bridge designs can fail catastrophically if the fraud proof mechanism itself is compromised.
Zero-Knowledge Verification
The most rigorous approach is a light client or zero-knowledge proof. The destination chain runs a cryptographic verifier that reads the source chain’s block headers and verifies that the claimed event is included in a finalized block. This approach inherits the source chain’s own security guarantees. No external party is trusted; the math does the work.
Unlike optimistic bridges with 7-day challenge windows or multi-sig validators, ZK-based attestations provide cryptographic finality upon proof verification. This eliminates the reorg risk and capital inefficiency that plagues guardian models for high-value transactions.
The Hybrid Spectrum
Legacy messaging layers like LayerZero and Axelar are integrating light clients and ZK proofs to remain competitive, creating hybrid trust models: security stacking that combines optimistic verification with fallback ZK proofs, and cost optimization that uses proofs for high-value messages and committees for low-value.
6. What Happens When the Receiving Chain Is Forked
A chain fork — whether an unintentional deep reorg or an intentional hard fork — is one of the most dangerous states for an in-flight cross-chain message. The message was committed on the source chain. The destination chain now exists in two incompatible histories.
Two failure modes emerge:
Fork-induced double execution. The message lands on both the canonical fork and the minority fork before either is declared final. State changes (e.g., minting tokens) are executed twice. Without proper nonce tracking this is invisible to the receiving contract. The nonce fix from Section 2 is the primary defense.
Stale attestation after reorg. The source chain reorgs below the block that emitted the message event. Attestors already signed the VAA / DVN attestation. The message may still arrive on the destination chain even though the source chain no longer contains the event. Protocols that value finality over latency should wait for sufficient source chain confirmations before the executor is permitted to deliver. This is configurable in LayerZero (block confirmations per DVN) and in CCIP (finality tag per lane).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Message envelope that includes source block number and
/// required confirmation depth so the receiver can enforce
/// finality guarantees independently of the messaging layer.
struct FinalityBoundMessage {
uint32 srcChainId;
uint64 srcBlockNumber;
uint16 requiredConfirmations; // agreed upon at protocol deployment
uint64 nonce;
uint32 dstChainId;
bytes payload;
}
interface IFinalityOracle {
/// @dev Returns the latest finalized block number on `chainId`
/// as reported by the protocol's trusted oracle/DVN.
function finalizedBlockNumber(uint32 chainId) external view returns (uint64);
}
contract FinalityAwareReceiver {
IFinalityOracle public immutable finalityOracle;
error NotYetFinalized(uint64 src, uint64 finalized, uint16 required);
constructor(address oracle) {
finalityOracle = IFinalityOracle(oracle);
}
function receiveMessage(FinalityBoundMessage calldata m) external {
uint64 finalized = finalityOracle.finalizedBlockNumber(m.srcChainId);
// The source block must be old enough to have accumulated
// the required confirmation depth.
if (finalized < m.srcBlockNumber + m.requiredConfirmations)
revert NotYetFinalized(
m.srcBlockNumber,
finalized,
m.requiredConfirmations
);
// Continue with nonce and chain ID checks...
}
}
For chains with probabilistic finality (Bitcoin-style PoW or chains with known reorg history), confirmation depth requirements should be set conservatively in the protocol configuration, not left as an executor-supplied parameter.
7. Composability of Cross-Chain Messages
Single-hop cross-chain messages are hard enough. Composing them — where the result of a message on Chain B triggers a second message to Chain C — multiplies every failure mode by the number of hops.
Composability risks deserve particular attention: bridges often plug into DeFi protocols. An attacker could exploit one component to cause cascading failures. Risk-aware design means auditing not just the bridge, but its interactions with other systems.
Fast bridging and composability have elevated economic attacks (MEV, timing manipulation) and systemic risk to the same threat level as traditional forgery. A bridge or messaging layer rarely “moves tokens” — it moves a claim that some event happened elsewhere, and it asks the destination chain to treat that claim as real.
A cross-chain composed flow must handle partial failure explicitly. If hop B→C fails, the state written on Chain B may already be committed. Without a compensation mechanism, the protocol is permanently inconsistent.
The Saga pattern — a well-known distributed systems primitive — applies directly:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Two-phase cross-chain action with an explicit rollback path.
/// Phase 1: Lock on source chain. Phase 2: Confirm OR rollback.
contract SagaCoordinator {
enum Phase { None, Locked, Confirmed, RolledBack }
struct Saga {
address user;
uint256 amount;
Phase phase;
uint256 timeout; // block.timestamp deadline for Phase 2
}
mapping(bytes32 => Saga) public sagas;
uint256 public constant TIMEOUT = 30 minutes;
event SagaLocked(bytes32 indexed id, address user, uint256 amount);
event SagaConfirmed(bytes32 indexed id);
event SagaRolledBack(bytes32 indexed id);
error WrongPhase(Phase current, Phase required);
error NotExpired();
function lock(bytes32 sagaId, address user, uint256 amount) external {
sagas[sagaId] = Saga(user, amount, Phase.Locked, block.timestamp + TIMEOUT);
emit SagaLocked(sagaId, user, amount);
// Send cross-chain message to destination chain...
}
/// @notice Called by the messaging layer on successful destination execution.
function confirm(bytes32 sagaId) external {
Saga storage s = sagas[sagaId];
if (s.phase != Phase.Locked) revert WrongPhase(s.phase, Phase.Locked);
s.phase = Phase.Confirmed;
emit SagaConfirmed(sagaId);
}
/// @notice Anyone can trigger rollback after timeout — no executor liveness dependency.
function rollback(bytes32 sagaId) external {
Saga storage s = sagas[sagaId];
if (s.phase != Phase.Locked) revert WrongPhase(s.phase, Phase.Locked);
if (block.timestamp < s.timeout) revert NotExpired();
s.phase = Phase.RolledBack;
// Refund logic here...
emit SagaRolledBack(sagaId);
}
}
Key design rules for composed cross-chain flows:
- Idempotency. Every step must be safe to execute multiple times. Nonces enforce this.
- Timeout-gated rollback. Rollback must be triggerable by anyone after a deadline, not just the executor, so executor liveness is never a liveness requirement for fund recovery.
- Atomic state within each hop. Each chain’s local state transition must be fully atomic; cross-chain atomicity is approximated through compensating transactions, not native ACID guarantees.
- Bounded depth. Limit hop count. Each additional hop multiplies reorg risk, ordering assumptions, and gas exposure.
8. Gas Limit Attacks on the Destination Chain
This attack class is underappreciated precisely because it doesn’t steal funds directly. Instead, it uses the gas budget of a cross-chain message to cause the destination execution to fail silently, leaving the source-chain side committed and the destination-chain side unexecuted.
Gas griefing occurs when a user sends the amount of gas required to execute the target smart contract but not enough to execute subcalls — calls it makes to other contracts.
In a cross-chain context the attack surface is asymmetric: the executor controls the gas forwarded to the destination, not the original sender. Because gas price and block gas limits differ across chains, a message that was correctly estimated on the source chain may be under-resourced on the destination chain — whether accidentally or maliciously.
Attack vector 1: Variable-cost destination logic. An attacker inflates the gas cost of the destination call by front-running a state change that makes the operation more expensive (e.g., expanding a mapping before the message lands). The message arrives, the fixed gas budget is consumed, and execution reverts.
Attack vector 2: Executor under-forwarding. When a user sends a request to the relayer, the user specifies the amount of gas to include in the transaction and digitally signs their request. However, the relayer might not respect the gas limit requested by the user, and send a lower amount.
Mitigations:
- Commit the gas limit in the signed payload. The DVN/Guardian signs over
gasLimitas part of the message hash. The destination endpoint verifiesgasleft() >= signedGasLimitbefore calling the application. - Separate verification from execution. The endpoint marks the message as verified in one call, and the application claims it in a second call. This way, under-gas never loses the message — it just delays execution until the application retries.
- Rate limiting. It may be prudent to sandbox bridge contracts — minimize their privileges, use rate limiting on contract calls, and avoid complex logic that could hide vulnerabilities.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Two-step receive: verify first, execute separately.
/// Execution failure never loses message provenance.
contract TwoStepReceiver {
enum Status { Unknown, Verified, Executed, Failed }
struct MessageRecord {
Status status;
bytes32 payloadHash;
uint256 verifiedAt;
}
mapping(bytes32 => MessageRecord) public messages;
event MessageVerified(bytes32 indexed msgId, bytes32 payloadHash);
event MessageExecuted(bytes32 indexed msgId);
event MessageFailed(bytes32 indexed msgId, bytes reason);
/// @dev Called by the messaging layer endpoint. Minimal gas required.
function verifyMessage(
bytes32 msgId,
bytes calldata payload
) external /* onlyEndpoint */ {
messages[msgId] = MessageRecord({
status: Status.Verified,
payloadHash: keccak256(payload),
verifiedAt: block.timestamp
});
emit MessageVerified(msgId, keccak256(payload));
}
/// @dev Can be called by anyone, with any gas amount.
/// Retryable — failed execution keeps Status.Failed so it can be replayed.
function executeMessage(
bytes32 msgId,
bytes calldata payload
) external {
MessageRecord storage r = messages[msgId];
require(
r.status == Status.Verified || r.status == Status.Failed,
"not executable"
);
require(keccak256(payload) == r.payloadHash, "payload mismatch");
r.status = Status.Executed; // optimistic; reset on failure
try this._doExecute(payload) {
emit MessageExecuted(msgId);
} catch (bytes memory reason) {
r.status = Status.Failed;
emit MessageFailed(msgId, reason);
}
}
function _doExecute(bytes calldata payload) external {
require(msg.sender == address(this), "internal only");
// Application-specific logic here.
(bool ok,) = address(this).call(payload);
require(ok, "execution failed");
}
}
9. Designing Protocols Resilient to Cross-Chain Message Failures
If you build cross-chain code, you are importing authority across domains. The job is to make that authority explicit, narrow, and survivable when something goes wrong.
The following design principles form a hierarchy from weakest to strongest guarantee:
9.1 Never Assume Delivery
Design every source-chain state transition to be reversible by a timeout. Commit-reveal patterns, escrow-with-expiry, and Saga coordinators (Section 7) all provide this. If the message never arrives, the user should be able to reclaim their position without operator intervention.
9.2 Separate Authority Planes
The messaging layer delivers a claim. Your contract decides what authority that claim grants. These are two independent concerns. Do not conflate them:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract AuthoritySeparated {
// The messaging layer tells us WHAT was claimed.
// The ACL tells us WHETHER that claim grants authority.
mapping(uint32 => mapping(bytes32 => bool)) public trustedSenders;
address public immutable messagingEndpoint;
error UntrustedSender(uint32 chainId, bytes32 sender);
error UntrustedEndpoint();
constructor(address endpoint) {
messagingEndpoint = endpoint;
}
modifier onlyEndpoint() {
if (msg.sender != messagingEndpoint) revert UntrustedEndpoint();
_;
}
modifier onlyTrustedSender(uint32 srcChainId, bytes32 srcSender) {
if (!trustedSenders[srcChainId][srcSender])
revert UntrustedSender(srcChainId, srcSender);
_;
_;
}
function receiveMessage(
uint32 srcChainId,
bytes32 srcSender,
bytes calldata payload
)
external
onlyEndpoint
onlyTrustedSender(srcChainId, srcSender)
{
// Decode and process payload
(address recipient, uint256 amount) = abi.decode(payload, (address, uint256));
_processTransfer(recipient, amount);
}
}
Cross-Chain Message Security Checklist
Message authentication
- Every incoming message is validated against a registry of trusted source addresses per chain
- The messaging endpoint (
msg.sender) is validated before checking message content - Trusted sender registry is updatable only by governance with a timelock
Replay prevention
- Message nonces are tracked per (srcChainId, srcSender) pair
- Chain ID is part of every signed message hash
- Messages are marked as processed before executing their effects
Ordering and delivery
- Protocol behavior is explicitly defined for out-of-order message delivery
- Messages that arrive out of order either revert cleanly or are queued safely
- There is no assumption that delivery is ordered unless the messaging layer guarantees it
Finality
- The minimum block confirmation requirement is documented and enforced at the relayer layer
- The protocol does not execute irreversible actions (mints, withdrawals) on messages from chains with short finality windows without additional delay
- Reorg risk is documented for each supported chain
Executor trust
- The executor model (permissioned vs open) is explicitly chosen and documented
- If open execution: the protocol is robust to any actor submitting a valid message
- If permissioned: executor key rotation procedure is defined
Gas limits
- The destination gas limit is set to bound the worst-case cost of message execution
- Message execution failure (out-of-gas on destination) has a defined recovery path
- Gas limit is not set so high that a malicious message can grief the destination by consuming excessive gas
Composability
- Cross-chain messages that trigger additional cross-chain messages are explicitly tracked
- Circular message paths are impossible by construction or explicitly guarded
- The maximum message chain depth is bounded