DEV Community

Anna Botsford
Anna Botsford

Posted on

Modeling Cross-Chain Completion States

Ethereum finality takes about 15 minutes under normal conditions, but a finalized source transaction still does not prove that the destination asset arrived. If you are wiring cross-chain execution into an application, your completion state must reflect the destination chain’s evidence, not just the wallet’s successful source receipt. A user who deposits on Ethereum and expects an asset on Optimism needs a clear answer when the first transaction is confirmed but the second is still pending.

Define completion as a destination-side condition

A transfer is complete when the expected destination-side effect has occurred and meets your application’s acceptance policy. That condition might be a token balance increase, a successful destination transaction emitting the expected event, or a protocol message marked executed. Persist the source transaction hash as the initial correlation key, then attach the route or message identifier and destination transaction hash as they become available.

Model execution as distinct states such as source_submitted, source_confirmed, in_flight, destination_executed, and failed. Keep “confirmed” separate from “finalized”: confirmation is your chosen inclusion threshold, while finality is a chain-specific assurance about reversion. Store the source and destination chain IDs, token addresses, raw token amounts, block numbers, and block hashes so you can reconcile events and detect reorgs.

The distinction matters when your product aggregates routes. A bungee bridge integration can surface routes with different execution paths, so don’t assume that every route shares one completion signal or timeout. Treat route metadata as input to an adapter that reports normalized states; retain the underlying protocol identifiers for support and recovery.

Track source finality and destination execution separately

Source confirmation means the source transaction has been included and meets your configured threshold; it does not mean the bridge has accepted or delivered the transfer. Ethereum’s proof-of-stake documentation describes finality through checkpoint votes, with about 15 minutes as the usual time to finality. That is a useful reference for Ethereum, not a universal timeout for every chain or route.

For an Ethereum-to-Optimism withdrawal through the canonical bridge, the source-side withdrawal is only the start: the withdrawal must be proven and later finalized after the fault-proof dispute period. Optimism’s documentation describes this multi-stage flow, including a seven-day dispute window in the standard path. By comparison, a Celer cBridge liquidity transfer follows a different mechanism: destination liquidity can be released through the bridge’s transfer process without waiting for the same canonical withdrawal window. These two cases have different expected latency and proof conditions; a single global “bridge complete after N minutes” rule will misreport one of them.

Use protocol events and identifiers as evidence, rather than inferring success from elapsed time. A destination transaction can revert, a relayer can be delayed, or an event query can temporarily miss an indexed log. When the user searches for a bungee bridge route, the aggregator helps identify a way to move or swap assets; your integration still needs to verify that route’s destination-side result before crediting the user.

Reconcile events without double-crediting

Make event handling idempotent. A practical key is the protocol transfer or message ID plus the destination chain ID; if the route exposes no stable transfer ID, derive a key from the source chain ID, source transaction hash, and log index. Enforce uniqueness in storage so duplicate webhook delivery, RPC replay, or a worker retry cannot credit the same transfer twice.

For each state transition, persist the block number and block hash. If a source or destination block is reorganized before your acceptance threshold, roll back the affected observation and resume polling; do not erase the transfer record. Treat an RPC timeout or an absent indexed event as unknown, not as failure. Mark failure only when the route reports a terminal failure or your recovery policy determines that an explicit intervention is required.

Polling can start at a few seconds while a source transaction is newly submitted, then back off exponentially—for example, doubling from 2 seconds to a 60-second cap. Those are illustrative worker settings, not protocol guarantees. Use jitter across workers, respect RPC rate limits, and keep the timeout policy route-specific: a short timeout suitable for a liquidity transfer would incorrectly label a canonical withdrawal as stuck.

Implement the lifecycle as observable steps

Build the state machine around explicit evidence and actions, so both users and operators can see what remains before funds are usable.

  1. Record the intent. Save the source and destination chain IDs, token contracts, amount in base units, recipient, route identifier, and source transaction hash before starting asynchronous tracking.
  2. Verify source inclusion. Read the transaction receipt and confirm the expected contract interaction succeeded. Apply a chain-appropriate confirmation or finality policy, then save the block hash used for that decision.
  3. Resolve the route’s pending condition. For a canonical withdrawal, track proof and finalization eligibility; for a liquidity route, track the bridge transfer and destination release. Keep route-specific states inside the adapter.
  4. Verify destination delivery. Confirm the destination receipt succeeded and validate the expected event, recipient, token, and amount. Only then mark the transfer complete or make the balance spendable.
  5. Recover on uncertainty. On timeout, re-query by message ID or transaction hash, check whether the recorded block remains canonical, and expose a pending or action-required state rather than silently retrying a value-moving call.

In practice, I would choose the acceptance threshold from the cost of a false credit and the guarantees of the route, then document it per route. That is the difference between displaying “source transaction confirmed” and safely telling a user their destination balance is ready.

Top comments (0)