LayerZeroFault
wallet security-fixes

Fix: ERC-7683 Cross-Chain Intent Replay & Settler Contract Revert (0x54656c6c)

VV

Written by

Fact-Checked on September 11, 2026

Verified Expert

Fix: ERC-7683 Cross-Chain Intent Replay & Settler Contract Revert (0x54656c6c)

The ERC-7683 Cross-Chain Intents Standard (co-developed by Across, Uniswap Labs, and Biconomy) establishes a unified interface for cross-chain liquidity and filler networks. Under ERC-7683, users sign an off-chain intent specifying desired outputs on a destination chain, and competitive filler relayers advance funds on the destination chain in exchange for reimbursement through settlement contracts.

However, production filler nodes, automated market-making relayers, and cross-chain dApps face severe transaction execution failures in settler contracts:

Transaction Reverted: Settler execution failed
Custom Error: InvalidOrderHash() (0x54656c6c)
or: NonceAlreadyFilled(bytes32 orderId)

If your relay architecture also interacts with off-chain token authorizations, consult our companion forensic analysis on Uniswap Permit2 InvalidNonce and SignatureExpired Reverts.


Technical Architecture of ERC-7683 ISettler

The core contract on both the origin chain and destination chain implements the ISettler interface:

// ERC-7683 Core Interface
struct GaslessCrossChainOrder {
    address originSettler;
    address user;
    uint256 nonce;
    uint256 originChainId;
    uint32 openDeadline;
    uint32 fillDeadline;
    bytes32 orderDataType;
    bytes orderData;
}

interface ISettler {
    function fill(
        bytes32 orderId,
        bytes calldata originData,
        bytes calldata fillerData
    ) external returns (bytes memory);
}

Placeholder: ERC-7683 Cross-Chain Intent Lifecycle Flowchart Across Origin and Destination Chains

When a user initiates an intent:

  1. The user signs an EIP-712 payload representing GaslessCrossChainOrder.
  2. The user’s tokens on the origin chain are escrowed in originSettler.
  3. A filler network observes the intent and submits fill() to destinationSettler.
  4. destinationSettler verifies the order hash, checks nonces, transfers output tokens to the user, and triggers cross-chain settlement verification back to the origin chain.

Root Cause 1: orderDataType Hash Discrepancy Between Chains

In ERC-7683, orderData is an opaque bytes field whose internal ABI decoding is governed by orderDataType:

$$\text{resolvedOrderDataType} = \text{keccak256}(\text{“MandatoryOutputOrder(bytes32 recipient,address outputToken,uint256 amount,uint256 destinationChainId)”})$$

A widespread engineering defect occurs when origin client SDKs and destination settler implementations use different field orderings or variable bitwidths (e.g., uint32 destinationChainId vs uint256 destinationChainId) when computing orderDataType:

[Origin Chain]:      Computes EIP-712 hash with orderDataType = 0x8a1b...
[Destination Node]: Expects schema with orderDataType = 0x9f4c...
[Settler.fill()]:   keccak256(orderData) mismatch -> REVERTS with InvalidOrderHash()

The Fix: Standardized Typed Data Definition

Ensure strict canonical alignment of the internal orderData struct across both origin and destination clients:

// erc7683-order-builder.ts
import { keccak256, toHex, encodeAbiParameters, parseAbiParameters, type Hex } from 'viem';

export const MANDATORY_OUTPUT_ORDER_TYPE = 
  'MandatoryOutputOrder(bytes32 recipient,address outputToken,uint256 amount,uint256 destinationChainId)';

export const MANDATORY_OUTPUT_ORDER_TYPE_HASH: Hex = keccak256(
  toHex(MANDATORY_OUTPUT_ORDER_TYPE)
);

export function encodeOrderData(
  recipient: Hex,
  outputToken: Hex,
  amount: bigint,
  destinationChainId: bigint
): Hex {
  return encodeAbiParameters(
    parseAbiParameters('bytes32 recipient, address outputToken, uint256 amount, uint256 destinationChainId'),
    [recipient, outputToken, amount, destinationChainId]
  );
}

Root Cause 2: Cross-Rollup Intent Nonce Replay

Because users sign intents that authorize fillers to take funds, what happens if an adversarial filler observes a valid intent intended for Base and attempts to fill it on Arbitrum?

If destinationChainId is not validated within the primary EIP-712 domain separator or explicitly asserted inside the destination settler’s fill() method:

// VULNERABLE SETTLER IMPLEMENTATION
function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData) external {
    GaslessCrossChainOrder memory order = abi.decode(originData, (GaslessCrossChainOrder));
    
    // MISSING: require(order.destinationChainId == block.chainid);
    require(!filledOrders[orderId], "NonceAlreadyFilled");
    filledOrders[orderId] = true;
    
    // Executes payout on the wrong chain!
}

An attacker can exploit chain-specific token price imbalances or arbitrage liquidity pools by executing the trade on an unintended chain, stranding user funds.

Placeholder: Security Diagram Showing Attacker Replaying Intent on Unintended EVM Chain

The Fix: Explicit Chain Binding & Domain Validation

The destination settler contract must strictly assert destination chain identity and verify that the origin settler address matches the authorized bridge registry:

// SECURE SETTLER VERIFICATION PATTERN
contract SecureDestinationSettler is ISettler {
    mapping(bytes32 => bool) public filledOrders;
    mapping(uint256 => address) public authorizedOriginSettlers;

    error InvalidDestinationChain(uint256 expected, uint256 actual);
    error UnauthorizedOriginSettler(address settler);
    error OrderAlreadyFilled(bytes32 orderId);

    function fill(
        bytes32 orderId,
        bytes calldata originData,
        bytes calldata fillerData
    ) external override returns (bytes memory) {
        GaslessCrossChainOrder memory order = abi.decode(originData, (GaslessCrossChainOrder));

        // 1. Verify Origin Settler Trust Anchor
        if (order.originSettler != authorizedOriginSettlers[order.originChainId]) {
            revert UnauthorizedOriginSettler(order.originSettler);
        }

        // 2. Decode and Assert Destination Chain ID
        (,,, uint256 destChainId) = abi.decode(order.orderData, (bytes32, address, uint256, uint256));
        if (destChainId != block.chainid) {
            revert InvalidDestinationChain(block.chainid, destChainId);
        }

        // 3. Mark Order as Filled
        if (filledOrders[orderId]) {
            revert OrderAlreadyFilled(orderId);
        }
        filledOrders[orderId] = true;

        // Proceed with token disbursement to recipient
    }
}

Root Cause 3: Asynchronous fillDeadline Expiration in Relayer Queues

Cross-chain message relays often encounter variable propagation latency:

  • Ethereum Mainnet to Arbitrum: $\approx 10-15$ minutes (L1 finality + sequencer ingestion)
  • Base to Optimism: $\approx 2-5$ seconds (soft settlement), but up to 20 minutes if proving gas spikes

If a user signs an intent with fillDeadline = block.timestamp + 120 (2 minutes), by the time the filler’s destination transaction gets included by the destination sequencer, block.timestamp on the destination rollup has already crossed fillDeadline.

The destination settler reverts:

require(block.timestamp <= order.fillDeadline, "OrderExpired");

The filler loses transaction gas fees, and the user’s origin deposit remains locked until openDeadline refund mechanics activate.

Placeholder: Timeline Comparison Between Origin L1 Ingestion and Destination Sequencer Inclusion

Defensive Relayer Logic: Dynamic Pre-Fill Deadline Gate

Filler bots must verify destination blocktime and discard expired orders before broadcasting transactions:

// filler-safety-guard.ts
import { type PublicClient } from 'viem';

export async function canSafelyFillOrder(
  destClient: PublicClient,
  orderFillDeadline: number,
  safetyBufferSeconds: number = 180 // 3-minute execution buffer
): Promise<{ canFill: boolean; reason?: string }> {
  const latestBlock = await destClient.getBlock({ blockTag: 'latest' });
  const destTimestamp = Number(latestBlock.timestamp);

  const remainingWindow = orderFillDeadline - destTimestamp;

  if (remainingWindow <= 0) {
    return { canFill: false, reason: `Order already expired on destination: ${remainingWindow}s` };
  }

  if (remainingWindow < safetyBufferSeconds) {
    return {
      canFill: false,
      reason: `Remaining window too narrow (${remainingWindow}s < ${safetyBufferSeconds}s buffer)`,
    };
  }

  return { canFill: true };
}

Production Diagnostic Checklist

When debugging ERC-7683 settler reverts:

[Diagnostic Routine]
├── 1. Compute Expected orderId:
│    └── keccak256(abi.encode(originChainId, originSettler, user, nonce, orderData))
├── 2. Verify Schema TypeHash:
│    └── Confirm keccak256(orderDataTypeString) matches destination settler constant
├── 3. Compare Chain IDs:
│    └── Verify destinationChainId in orderData equals block.chainid of execution network
└── 4. Inspect Block Timestamps:
     └── Assert destination latestBlock.timestamp <= fillDeadline

Adopting strict typed data verification, cryptographically binding destination chain IDs, and setting realistic cross-chain deadline margins prevents settler execution failures and preserves high fill rates across cross-chain intent networks.

Partner Spotlight: Gate.io

Trade Securely on Gate.io

Don't risk your assets on centralized silos or unverified endpoints. Trade securely on Gate.io with deep liquidity and institutional-grade security protocols.

Claim $100 Sign-up Bonus

Official Partner Referral Link

Related Inquiries

What is ERC-7683 and why do settler contracts revert during intent execution?

ERC-7683 defines a standard interface for cross-chain trade execution through intents. Settler contracts revert primarily due to three factors: orderDataType schema hash mismatches between origin and destination chains, intent order nonce replay attempts, or fillDeadline expiration caused by cross-chain message propagation latency.

How does cross-chain replay vulnerability manifest in ERC-7683 orders?

If the signed CrossChainOrder struct does not cryptographically bind the destinationChainId within its hashed orderData or uses a global nonce that does not track destination execution state, an adversarial filler can intercept the intent signature on Rollup A and attempt to execute it on Rollup B where exchange rates or collateral values are unfavorable to the user.

How should filler relayers handle fillDeadline across asynchronous rollups?

Fillers must query the block header timestamp of the destination settlement chain rather than the origin chain or local server clocks before submitting fill(). Relay bots must enforce a minimum execution buffer of at least 180 seconds before fillDeadline to account for L1 batch finality delays.