To fix ERC-7683 cross-chain intent settler reverts and replay exploits, enforce unique intent nonces with destination settlement hashes and verify filler authorizations before executing fill orders. Reverts happen when filler relayers submit settlement transactions with expired intent deadlines, mismatched order hashes, or duplicate nonces in settler contracts. Binding order execution to deterministic settlement receipts prevents double-fulfillment and relayer capital lockups.
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.
[!TIP] Diagnostic Sandbox: Unpack cross-chain settlement calldata and verify intent execution payloads in our air-gapped /tools/erc4337-calldata-decoder/. Compare with our analysis on Uniswap Permit2 Nonce Expiration Collisions.
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);
}
When a user initiates an intent:
- The user signs an EIP-712 payload representing
GaslessCrossChainOrder. - The user’s tokens on the origin chain are escrowed in
originSettler. - A filler network observes the intent and submits
fill()todestinationSettler. destinationSettlerverifies 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.
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.
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.