How to prevent duplicate cross-chain actions

A duplicate cross-chain action happens when the same request changes destination state more than once. The key condition is that retries must reuse the original request identity, and the destination must record that identity before it applies the effect.

  • Assume a message may be delivered more than once; make its effect apply at most once.
  • Build a stable idempotency key from the source and request, then store it on the destination.
  • Retry the same request after a failure; never create a new one just because delivery is slow.

Why can one request arrive more than once?

Cross-chain delivery has separate source, verification, and destination execution stages. A sender may see the source transaction succeed while destination execution is still pending, or a relayer may submit execution again after an unclear response. Retrying transport is useful for recovery, but the application must prevent that retry from repeating the action.

This matters in an omnichain app because a message can coordinate shared state, token supply, or liquidity across networks. For the architecture and build sequence behind those applications, see how to build apps with omnichain; this guide focuses on making one destination action safe to retry.

What identifies the request reliably?

Use a stable key that names the logical action, not the transaction that happened to deliver it. A destination transaction hash changes when execution is retried, so it cannot identify the original request.

  1. Choose a source nonce. Increment a counter in the source application for each request. Include the source chain identifier and source application address so that two apps or networks cannot accidentally produce the same key.
  2. Derive the key at both ends. A practical key is a hash of source chain ID, source app, destination app, and nonce. Include an action type or version if the same nonce could otherwise be interpreted under different logic. If the messaging protocol provides a unique message ID, store that too for tracing, while keeping the app-level key as the identity of the business action.
  3. Put the required data in the message. Include the recipient, amount or action parameters, and the key. The destination should derive or validate the key from authenticated message fields, rather than trusting a caller-supplied key on its own.

For example, a source app sends nonce 42 to release 100 units to a recipient. If destination execution times out and is resubmitted, both deliveries still refer to that same source, destination, and nonce. The key remains unchanged even though the destination transaction hash does not.

How should the destination apply an action once?

Store a processed flag or status for each key on the destination contract, and make checking and changing it part of the same transaction as the action. On EVM chains, a mapping from key to status is a common implementation. If the action reverts, the status update reverts too, so a later retry can try again.

  1. Authenticate the message first. Check the messaging protocol’s verification and source-app rules before using its payload. A key prevents duplicate effects; it does not prove who sent the request.
  2. Reject an already completed key. If the key is marked complete, return safely or revert according to the protocol’s delivery model. Do not repeat a token mint, withdrawal, permission change, or other effect.
  3. Mark and execute atomically. Set the key to processing or complete, then perform the effect in the same transaction. For a synchronous EVM call, a revert rolls both back. If execution involves an external system or a later transaction, use explicit states such as pending, completed, and failed instead of treating “message received” as “action completed.”

A common mistake is to mark a key processed when the message is first observed, then try the actual action in a separate transaction. If that second transaction fails, a retry looks like a duplicate and the request is stuck. Tie the status transition to the action’s real completion, or add a recoverable failure state.

When is it safe to retry?

Retry the same authenticated request when destination execution has failed or remains pending; do not issue a fresh source request to solve a delivery delay. The precise finality rule depends on the source chain and messaging system, so use the protocol’s accepted verification state rather than guessing a fixed number of blocks.

  1. Track the lifecycle. Record the source transaction, message ID, app-level key, and destination status. This lets an operator distinguish “not yet delivered” from “delivered but action reverted.”
  2. Retry only unresolved work. A pending message can be relayed again under the protocol’s recovery path. A completed key must remain a no-op, even if a duplicate delivery reaches the contract.
  3. Test the edge cases. Simulate duplicate delivery, a revert during execution, and two relayers submitting the same request close together. Confirm that only one effect succeeds and that a failed attempt remains retryable.

Expect a small storage cost for each recorded key, and decide how long records must remain to cover the protocol’s retry and replay window. For token actions, check the accounting invariant as well: one source debit should correspond to no more than one destination credit, including after a failed execution or delayed retry.

Comments

Popular posts from this blog

Syncswap Aqua Pool Fees: Why Imbalance Matters

4 Checks for Comparing Multi-Pool Token Activity

Spendable Monero outputs before a bridge deposit