Cross-Chain Swap Recovery: Retry Without Double-Spending
Retry the failed phase, never the whole swap blindly. A source transaction, a bridge message and a destination swap are separate state transitions; your integrator should identify which one failed before it submits anything again.
For a developer integrating an omnichain service, recovery is part of the execution model, not an afterthought. Persist the source transaction hash, message or deposit identifier, source and destination chain IDs, and the terminal state reported by the underlying protocol.
Resubmit only when the source transaction never landed
If the source transaction is pending, first query its hash and sender nonce on the source chain. A transaction replaced with the same nonce and higher fee supersedes the earlier pending transaction; submitting the same call with a new nonce can create a second deposit if the first later confirms.
After a receipt, inspect its status and the protocol’s source event before deciding what to do. A reverted transaction did not create the intended on-chain deposit, but an RPC timeout is not proof of a revert. Reconcile against a second provider or indexed event source before offering a retry.
This path is best for a transaction that was dropped or definitively reverted. It does not fit a confirmed source event that is merely waiting for relaying, verification or destination execution: those stages need protocol-level recovery, not another user deposit.
Retry destination execution when the message is already verified
When a message has been verified but destination execution failed, retry execution for that message instead of resending from the source. LayerZero V2 separates verification from execution: a failed lzReceive can be retried at the Endpoint, while an lzCompose failure needs a compose retry. The packet identity and verified payload remain the same.
First classify the revert. An out-of-gas failure calls for a larger execution budget; a logic revert caused by stale application state, invalid calldata or a missing precondition will fail again until that condition changes. LayerZero’s execution options carry the requested gas and native value; derive the gas limit from destination simulation and add explicit headroom rather than assuming one cross-chain default.
If you need the broader decision process for selecting a path, see how to choose an omnichain route. For recovery, keep route selection separate from message state: the same verified message can be retried without funding a fresh route.
Use protocol recovery when relaying or execution needs more gas
Some protocols expose recovery steps for different points in their pipeline. Axelar’s documentation distinguishes a call not yet approved by the network from an approved call that failed on the destination: the first may need approval resubmitted, while the second may need destination execution retried or additional gas paid.
Across intents have a different lifecycle: an origin deposit is filled by a relayer, and its fillDeadline is the Unix timestamp after which an unfilled deposit becomes eligible for refund. Track the deposit by its event or transaction hash and wait for a terminal status such as filled, expired or refunded; do not treat a slow fill as permission to submit a replacement.
These mechanisms fit protocol-specific relaying or execution failures. They do not fix an application contract that deterministically reverts. An omnichain integration should surface the failed stage and its protocol identifier so an operator can distinguish “retry execution” from “correct the payload or contract.”
Make every recovery action idempotent
Idempotency is the rule that prevents a recovery button, worker retry or webhook replay from paying twice. Key your internal operation by the protocol’s stable message or deposit identifier, and use a state machine that rejects a new source submission while the prior operation is pending or unresolved.
For example, suppose an Across deposit has a confirmed origin event but no destination fill yet. Your worker should poll that deposit’s status and compare the current time with fillDeadline; if the deadline passes, it should expose the protocol’s refund path. It should not create a second deposit merely because a client timed out waiting for a response.
Keep a short recovery record with the last observed state, block number, destination transaction hash and retry count. Before retrying, re-read protocol state on-chain: indexers and status APIs are useful for discovery, but their lag makes them unsuitable as the sole authorization to create another transfer.
When is it safe to let the user retry?
Allow a fresh source submission only after you have established that the previous source transaction reverted or never landed and cannot later be included. If the source event exists, keep the original operation open and offer the applicable protocol recovery or status path instead.
Should a failed destination swap be sent again from the source?
Usually no. If verification succeeded and only destination execution failed, retry the same message using the protocol’s execution mechanism. Resending from the source creates a new message and may duplicate the token movement while the original remains executable.
How should an integrator handle an expired intent?
Read the protocol’s terminal state and follow its refund rules. In Across, a deposit becomes eligible for refund after fillDeadline passes without a fill; expiry alone is not the same as a completed refund. Keep tracking the original deposit until the refund is confirmed.
What should the recovery interface show?
Show the last confirmed stage, the relevant transaction or message identifier, and the specific next action the protocol supports. Separate a retryable gas or execution failure from an application revert that needs a payload or contract fix. Before acting, ask: can this operation still execute somewhere if I submit it again?