
One of the most difficult operational failures for a multichain asset bridge looks like this: a user locks 10,000 USDC on Ethereum mainnet to bridge to Arbitrum. Just as the source-chain transaction confirms, a surge of high-frequency inscription minting hits Arbitrum, sending its base gas fee to 80 times the usual 0.1 Gwei.
The relayer’s initial cost estimate reserved far too little gas, so its destination-chain transaction sits in the mempool without being included. The anxious user posts repeated complaints in the community. To calm the situation, support clicks “source-chain refund” in the admin console. The source vault immediately releases 10,000 USDC to the user.
But 40 minutes later, congestion on Arbitrum clears. Validators include the previously stuck destination transaction, and the destination vault also releases 10,000 USDC to the user’s receiving address.
One bridge transfer has paid out twice, leaving the project’s vault with an immediate net loss of $10,000.
This double-refund exploit, caused by a failure to align state in both directions, has cost several well-known bridge protocols millions of dollars. In cross-chain communication, a stuck transfer is a distributed consistency problem, not simply network latency. A commercial-grade asset bridge must establish a strict deterministic recovery state machine at the architecture level.
1. Four Main Causes of Stuck Cross-Chain Transfers

A cross-chain message typically passes through a multihop asynchronous pipeline on its way from source to destination. Historical stuck-transfer incidents generally trace back to four external factors:
+----------------------------------------------------------------------------------------------------+
| End-to-End Failure Points in a Stuck Cross-Chain Transfer |
+----------------------------------------------------------------------------------------------------+
Source Chain Vault Relayer Network Gas Oracle Destination Vault
│ │ │ │
(1) |── Deposit 10,000 USDC ──>│ │ │
│ │ │ │
(2) │ |── Fetch current gas ────>│ │
│ │<── Return 0.1 Gwei ─────│ │
│ │ │
(3) │ |── Broadcast destination Release transaction ──────>│
│ │ (GasPrice: 0.1 Gwei) │
│ │ │
(4) │ │ [Sudden congestion: gas rises to 15 Gwei] │
│ │ Transaction stuck in mempool, not included ────>│ [Blocked]
│ │ │
(5) │ [Critical operator error] │
|<── Operator refunds on source chain │
│ (User receives first payout) │
│ │ │
(6) │ │ [Half an hour later, gas falls; pending tx is included]
│ │──────────────────────────────────────────────────>│ [Release!]
│ (User receives second payout: double spend)
+----------------------------------------------------------------------------------------------------+
- Destination gas spikes and low-tip delays (Gas Price Spike): A sudden destination-chain traffic peak causes miners or sequencers to defer relay transactions with fixed gas parameters indefinitely.
- Temporary destination-chain liquidity exhaustion (Liquidity Exhaustion): In a pool-based bridge, large one-way transfers temporarily deplete destination-vault funds. Transactions must wait for reverse traffic or market-maker replenishment.
- Recipient contract execution failure (Execution Revert): The receiving address may be an uninitialized proxy contract, or the destination contract may throw during a token-receipt callback, repeatedly reverting the destination transaction.
- RPC node desynchronization (Node Desync): A forked or delayed RPC connected to the relayer incorrectly reports that the destination transaction was dropped, even though it remains queued on other nodes.
2. The Core Rule: Safe Two-Phase Cancellation with Destination-Chain Tombstones
To prevent double-refund spending at its source, enforce one rule: an irreversible “refuse release” state on the destination chain must be the necessary and sufficient precondition for unlocking any source-chain refund.
In Passage Bridge, we use the tombstone pattern, borrowed from distributed file systems:
+----------------------------------------------------------------------------------------------------+
| Safe Two-Phase Cancellation and Source-Chain Refund Workflow |
+----------------------------------------------------------------------------------------------------+
[ Phase 1: Record an irreversible destination-chain tombstone ]
Operator / Relayer detects a severely overdue transfer (e.g. > 2 hours)
│
▼
Submit `recordTombstone(messageId)` to the destination contract
│
├── Check whether this messageId has already been claimed on the destination (if so, stop)
│
└── If unclaimed: permanently store `tombstones[messageId] = true` on the destination
(Even if miners later include the original pending transaction, the contract must revert and refuse release)
│
▼
Destination emits `CrossChainMessageCancelled(messageId, nonce, refundRecipient)`
│
▼ Wait for destination-chain finality (e.g. 64 blocks)
[ Phase 2: Unlock the source funds using the destination tombstone proof ]
Relayer or user submits the destination cancellation signature to the source contract: `refundWithProof(...)`
│
├── Verify that the proof includes destination-validator multisig / storage proof
│
└── Verify that the original source funds have not already been refunded
│
▼
Source vault safely returns tokens to the user's original wallet address (zero double-spend risk!)
+----------------------------------------------------------------------------------------------------+
3. Passage Bridge Tombstone and Refund Contracts
The following core production-grade Solidity implementation shows how the destination records a tombstone and how the source safely authorizes a refund based on that proof:
// passage-bridge/contracts/src/core/DestinationBridgeVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
error MessageAlreadyExecuted(bytes32 messageId);
error MessageAlreadyCancelled(bytes32 messageId);
contract DestinationBridgeVault is Ownable2Step {
using SafeERC20 for IERC20;
// messageId => whether funds have been released
mapping(bytes32 => bool) public executedMessages;
// messageId => tombstone marker (whether cancelled and invalidated)
mapping(bytes32 => bool) public tombstones;
event TokensReleased(bytes32 indexed messageId, address indexed recipient, uint256 amount);
event MessageTombstoneRecorded(bytes32 indexed messageId, address indexed refundRecipient, uint256 amount);
constructor(address initialOwner) Ownable(initialOwner) {}
/**
* @notice Standard destination-chain release function
*/
function releaseTokens(
bytes32 messageId,
address token,
address recipient,
uint256 amount
) external onlyOwner {
if (executedMessages[messageId]) revert MessageAlreadyExecuted(messageId);
if (tombstones[messageId]) revert MessageAlreadyCancelled(messageId);
executedMessages[messageId] = true;
IERC20(token).safeTransfer(recipient, amount);
emit TokensReleased(messageId, recipient, amount);
}
/**
* @notice Recovery and refund core: record an irreversible destination-chain tombstone
* Triggered by the cross-chain relay committee only after confirming a stuck transfer requires a refund
*/
function recordTombstone(
bytes32 messageId,
address refundRecipient,
uint256 amount
) external onlyOwner {
if (executedMessages[messageId]) revert MessageAlreadyExecuted(messageId);
if (tombstones[messageId]) revert MessageAlreadyCancelled(messageId);
// Record a permanent tombstone: no original transaction can release funds even if included later
tombstones[messageId] = true;
emit MessageTombstoneRecorded(messageId, refundRecipient, amount);
}
}
// passage-bridge/contracts/src/core/SourceBridgeVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
error RefundAlreadyProcessed(bytes32 messageId);
error InvalidCancellationProof();
contract SourceBridgeVault is Ownable2Step {
using SafeERC20 for IERC20;
using ECDSA for bytes32;
struct LockedDeposit {
address depositor;
address token;
uint256 amount;
bool refunded;
}
mapping(bytes32 => LockedDeposit) public deposits;
address public messageReviewSigner; // Independent review committee public key
event TokensDeposited(bytes32 indexed messageId, address indexed depositor, address token, uint256 amount);
event DepositRefunded(bytes32 indexed messageId, address indexed recipient, uint256 amount);
constructor(address initialOwner, address _reviewSigner) Ownable(initialOwner) {
messageReviewSigner = _reviewSigner;
}
/**
* @notice Safely refund source-chain assets using a destination-chain tombstone proof
*/
function processRefundWithProof(
bytes32 messageId,
bytes memory destinationTombstoneProof
) external {
LockedDeposit storage deposit = deposits[messageId];
if (deposit.amount == 0 || deposit.refunded) {
revert RefundAlreadyProcessed(messageId);
}
// Verify that the refund request has definitive signed evidence of a destination tombstone
bytes32 proofHash = keccak256(abi.encodePacked(
"TOMBSTONE_VERIFIED",
block.chainid,
messageId,
deposit.depositor,
deposit.amount
));
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", proofHash));
address recoveredSigner = ethSignedMessageHash.recover(destinationTombstoneProof);
if (recoveredSigner != messageReviewSigner) {
revert InvalidCancellationProof();
}
// Mark as refunded and return tokens to the original sender
deposit.refunded = true;
IERC20(deposit.token).safeTransfer(deposit.depositor, deposit.amount);
emit DepositRefunded(messageId, deposit.depositor, deposit.amount);
}
}
4. Dynamic Tip Bumping and Adaptive Relay Scheduling
Source-chain refunds are only one option: 80% of stuck transfers do not require cancellation, only additional miner fees to speed up inclusion.
A relay engine with fixed gas logic stalls under congestion. The Passage Bridge off-chain watchdog implements an EIP-1559 retry pipeline with exponential fee increases:
// passage-bridge/src/relayer/gas-bump-watchdog.ts
import { PublicClient, WalletClient, parseGwei } from 'viem';
export class TransactionRescueWatchdog {
private client: PublicClient;
private wallet: WalletClient;
private maxAllowedBaseFee = parseGwei('200'); // Hard cost cap
constructor(client: PublicClient, wallet: WalletClient) {
this.client = client;
this.wallet = wallet;
}
public async monitorAndBumpInFlightTx(
inFlightTx: { hash: `0x${string}`; nonce: number; currentPriorityFee: bigint; submittedAt: number },
targetTxCalldata: `0x${string}`,
targetContract: `0x${string}`
) {
const elapsedSeconds = (Date.now() - inFlightTx.submittedAt) / 1000;
// If still pending after 90 seconds, increase gas fees to accelerate inclusion
if (elapsedSeconds > 90) {
const receipt = await this.client.getTransactionReceipt({ hash: inFlightTx.hash }).catch(() => null);
if (receipt && receipt.status === 'success') {
return; // Already confirmed; no intervention needed
}
const block = await this.client.getBlock();
const currentBaseFee = block.baseFeePerGas || parseGwei('1');
if (currentBaseFee > this.maxAllowedBaseFee) {
console.warn(`[Watchdog] Network gas too high (${currentBaseFee}). Pausing automatic bump.`);
return;
}
// Follow Ethereum/EVM rules: a replacement transaction's PriorityFee must rise by at least 10%~20%
const newPriorityFee = (inFlightTx.currentPriorityFee * 125n) / 100n; // Increase by 25%
const newMaxFeePerGas = currentBaseFee * 2n + newPriorityFee;
console.log(`[Watchdog] Bumping tx nonce ${inFlightTx.nonce}: PriorityFee -> ${newPriorityFee.toString()}`);
// Send a new replacement transaction with the same nonce (Speed-Up / Gas Bump)
const newTxHash = await this.wallet.sendTransaction({
to: targetContract,
data: targetTxCalldata,
nonce: inFlightTx.nonce,
maxPriorityFeePerGas: newPriorityFee,
maxFeePerGas: newMaxFeePerGas,
});
return {
hash: newTxHash,
nonce: inFlightTx.nonce,
currentPriorityFee: newPriorityFee,
submittedAt: Date.now(),
};
}
}
}
5. Comparing Passive Waiting, Unverified Manual Refunds, and Deterministic Tombstone Recovery
The following table compares safety and business impact when handling stuck bridge transfers:
| Dimension | A: Passive waiting (no intervention mechanism) | B: Unverified source refunds from the admin console | C: Destination tombstone + dynamic fee-bump recovery (this approach) |
|---|---|---|---|
| Double-spend risk | Zero (no duplicate refund) | Very high (100% loss if the stuck transaction is included later) | Absolutely zero double spending (source funds unlock only after the destination durably records tombstone evidence) |
| Complaints and wait time | Very long (hours or days with user assets completely frozen) | Short (quick payment, with all risk borne by the project) | Very short (prioritize dynamic bumps for faster inclusion; use a transparent refund flow for genuinely stuck transfers) |
| Failure-path explainability | Black box (support cannot identify the blocked hop for users) | Confused (ledger state diverges sharply from actual on-chain assets) | Fully transparent (operations sees mempool state, fee-bump history, and tombstone proofs) |
| Gas cost control | Fixed fees can leave transactions dropped for long periods, wasting relay retry costs | Source refunds incur unnecessary transaction fees | Controlled in steps (maxAllowedBaseFee acts as a circuit-breaker threshold against unbounded subsidies) |
6. Integrating Independent Review and Replay Protection
Across the cross-chain lifecycle, stuck-transfer recovery must be integrated into the infrastructure’s broader security framework:
- Coordinate with unordered bitmap replay markers: With the architecture described in Preventing Cross-Chain Replay Attacks, an earlier stuck transaction can enter a two-hour recovery investigation without blocking subsequent users’ normal small transfers. Unordered bitmap nonces keep the transaction channel moving.
- Combine with independent asset-conservation reconciliation: When a large stuck transfer enters recovery, the relay system automatically invokes the Independent Message Review and Circuit-Breaker Architecture for Multi-Relayer Bridges. It reports source and destination vault balance differences to the off-chain reconciliation service, ensuring that every release or refund is grounded in definitive on-chain facts.
Consulting and Source-Code Delivery
If you develop or operate a multichain asset bridge, cross-chain liquidity aggregator, or decentralized game-asset interoperability platform, and want to eliminate financial incidents caused by congestion, RPC packet loss, and stuck-transfer refunds:
- Visit Fone’s Client Deliveries and System Demos to explore Passage Bridge production-grade stuck-transfer monitoring, dynamic gas bumping, and tombstone refund controls;
- Read our 12-Point Engineering and Security Audit Guide for Production Web3 Launches;
- Book a one-to-one technical discussion through Fone Architecture Consulting for a deployable white-label bridge source repository and an operational emergency-response design.