ERC-7683 Cross-Chain Intents: How Solvers Can Be Gamed and Funds Lost
Cross-chain intent protocols represent a structural shift in how users move assets and execute actions across disparate blockchain networks. By shifting the complexity of routing, bridging, and gas management from the user to professional market makers—known as solvers—protocols like Uniswap and Across (via the ERC-7683 specification) aim to improve the user experience.
However, this design introduces unique security assumptions. In the intent-centric model, solvers use their own capital to fulfill a user's request on the destination chain before receiving reimbursement on the source chain. This asynchronous flow opens up dangerous attack vectors. If a settlement contract or a solver's execution infrastructure is poorly designed, attackers can game the system to steal solver liquidity, replay signatures across chains, or hijack transaction proofs.
To build a resilient cross-chain ecosystem, understanding erc-7683 intent solver security is paramount. This technical analysis explores the core vulnerabilities in ERC-7683 solver implementations, focusing on signature replay attacks, proof hijacking, and malicious callbacks. We provide concrete, compilable Solidity examples of vulnerable and secure architectures.
The Architecture of ERC-7683
ERC-7683 standardizes the interface for cross-chain orders. The lifecycle of a cross-chain intent typically involves three phases:
- Order Creation and Escrow (Source Chain): The swapper signs a cross-chain order detailing their desired input assets, output assets, destination chain, and recipient. The swapper deposits the input assets into a source-chain settlement contract (the Escrow).
- Order Execution (Destination Chain): A solver discovers the signed order off-chain. The solver executes the order by calling a settlement contract on the destination chain, transferring the required output assets directly to the user's recipient address.
- Settlement and Reimbursement (Cross-Chain Verification): The destination-chain settlement contract records that the order was successfully filled. A verification mechanism (such as an optimistic oracle, a storage proof, or a zero-knowledge bridge) verifies this execution and triggers the release of the escrowed input assets on the source chain to the solver.
Solvers rely on the absolute guarantee that if they fulfill the user's order on the destination chain, they will receive the corresponding assets on the source chain. If this guarantee is compromised, the solver suffers a direct capital loss.
Attack Vector 1: Cross-Chain Signature Replay
The most severe vulnerability in cross-chain intent protocols is signature replay. If the order schema signed by the swapper does not explicitly bind the intent to a specific chain ID and settlement contract, the signature can be replayed on other networks.
The Vulnerability Mechanics
Suppose a swapper wants to swap 10 ETH on Ethereum for USDC on Arbitrum. They sign an intent specifying:
- Swapper: 0xSwapper...
- Recipient: 0xRecipient...
- Output Token: USDC
- Output Amount: 30,000 USDC
- Nonce: 42
If the hashing function used by the settlement contract does not incorporate block.chainid or the address(this) of the destination settler, this exact signature is valid on any chain where a compatible settlement contract is deployed.
An attacker can intercept this signature and broadcast it on Optimism, where the solver also maintains a liquidity pool. The solver's automated execution bot detects the valid signature on Optimism and automatically fulfills the order, sending 30,000 USDC to 0xRecipient.
However, because the swapper only deposited 10 ETH escrow on Ethereum for the Arbitrum destination, no escrow exists for the Optimism execution. When the solver attempts to claim their reimbursement on Ethereum, the settlement contract rejects the claim because the source-chain escrow was bound to the Arbitrum execution, or has already been claimed. The solver has successfully filled the order twice but can only get reimbursed once, resulting in a loss of 30,000 USDC.
Vulnerable Code Example
The following Solidity contract shows a simplified, compilable implementation of a destination settler vulnerable to cross-chain signature replay. The hashing function fails to bind the order to a specific chain ID and contract address.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title VulnerableDestinationSettler
* @notice A demonstration contract vulnerable to cross-chain signature replay.
*/
contract VulnerableDestinationSettler {
using ECDSA for bytes32;
struct CrossChainOrder {
address swapper;
address recipient;
address tokenOut;
uint256 amountOut;
uint256 nonce;
uint256 expiry;
}
mapping(bytes32 => bool) public executedOrders;
event OrderFilled(bytes32 indexed orderHash, address indexed solver, address indexed recipient);
/**
* @notice Generates the hash of the order parameters.
* @dev VULNERABILITY: This hash does not include the chainId or the settler address.
*/
function hashOrder(CrossChainOrder calldata order) public pure returns (bytes32) {
return keccak256(abi.encode(
order.swapper,
order.recipient,
order.tokenOut,
order.amountOut,
order.nonce,
order.expiry
));
}
/**
* @notice Fulfills a cross-chain order on the destination chain.
* @param order The struct containing order details.
* @param signature The signature generated by the swapper.
*/
function fillOrder(
CrossChainOrder calldata order,
bytes calldata signature
) external {
require(block.timestamp <= order.expiry, "Order expired");
bytes32 orderHash = hashOrder(order);
require(!executedOrders[orderHash], "Order already executed");
// Recover the signer from the signature
bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(orderHash);
address signer = ethHash.recover(signature);
require(signer == order.swapper, "Invalid signature");
executedOrders[orderHash] = true;
// VULNERABLE LINE: The transfer is executed without validating the destination chain or settlement instance
/* @audit-issue VULNERABLE: Lack of chainId and settlement contract validation allows cross-chain replay */
IERC20(order.tokenOut).transferFrom(msg.sender, order.recipient, order.amountOut);
emit OrderFilled(orderHash, msg.sender, order.recipient);
}
}
The Secure Remediation
To secure the protocol against signature replay, the order hash must conform to EIP-712, incorporating a proper domain separator. The domain separator binds the signature to a specific contract name, version, chain ID, and verifying contract address—making the signature invalid on any other chain or contract instance.
Note on ERC-7683 struct design: The actual ERC-7683 standard (
GaslessCrossChainOrder) encodes destination chain and token details inside an opaqueorderDatabytes field with abytes32 orderDataTypetypehash. The struct used below is a simplified, self-contained illustration that embeds fields directly for clarity. Production implementations should follow the ERC-7683 spec and decodeorderDataper their registered sub-type.
The correct approach uses OpenZeppelin's EIP712 base contract, which automatically constructs a proper domain separator from the contract name, version, block.chainid, and address(this). This eliminates the need to manually validate chain ID and contract address in every function call.
Here is the secure implementation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title SecureDestinationSettler
* @notice A secure destination settler using EIP-712 typed data signatures to prevent
* cross-chain signature replay. The EIP712 base contract encodes chainId and
* address(this) into the domain separator automatically.
*/
contract SecureDestinationSettler is EIP712 {
using ECDSA for bytes32;
// EIP-712 typehash for the order struct.
bytes32 public constant ORDER_TYPEHASH = keccak256(
"CrossChainOrder(address swapper,address recipient,address tokenOut,uint256 amountOut,uint256 nonce,uint256 expiry)"
);
struct CrossChainOrder {
address swapper;
address recipient;
address tokenOut;
uint256 amountOut;
uint256 nonce;
uint256 expiry;
}
mapping(bytes32 => bool) public executedOrders;
event OrderFilled(bytes32 indexed orderHash, address indexed solver, address indexed recipient);
constructor() EIP712("DestinationSettler", "1") {}
/**
* @notice Returns the EIP-712 struct hash for the given order.
* @dev The domain separator (containing chainId and address(this)) is applied
* separately via _hashTypedDataV4, binding the final digest to this chain
* and contract instance.
*/
function hashOrder(CrossChainOrder calldata order) public pure returns (bytes32) {
return keccak256(abi.encode(
ORDER_TYPEHASH,
order.swapper,
order.recipient,
order.tokenOut,
order.amountOut,
order.nonce,
order.expiry
));
}
/**
* @notice Fulfills a cross-chain order on the destination chain securely.
* @param order The struct containing order details.
* @param signature The EIP-712 signature generated by the swapper.
*/
function fillOrder(
CrossChainOrder calldata order,
bytes calldata signature
) external {
require(block.timestamp <= order.expiry, "Order expired");
// _hashTypedDataV4 applies the domain separator (chainId + address(this)),
// making this digest invalid on any other chain or contract.
bytes32 digest = _hashTypedDataV4(hashOrder(order));
require(!executedOrders[digest], "Order already executed");
address signer = ECDSA.recover(digest, signature);
require(signer == order.swapper, "Invalid signature");
// CEI: mark executed before external calls
executedOrders[digest] = true;
IERC20(order.tokenOut).transferFrom(msg.sender, order.recipient, order.amountOut);
emit OrderFilled(digest, msg.sender, order.recipient);
}
}
Attack Vector 2: Proof Hijacking (Front-Running the Settler)
In standard bridging protocols, execution and settlement are atomic; the contract executing the fill also logs the solver's address to ensure they receive the refund. However, in some asynchronous intent designs, the execution is decoupled from the proof submission.
The Attack Vector
Consider this scenario:
1. The solver sends the output token directly to the user's address via standard transfer functions.
2. The solver generates a transaction receipt or storage proof showing that the transfer occurred.
3. The solver submits this proof to the source chain settlement contract to release the escrowed funds.
If the source chain settlement contract only checks that a transaction occurred from any address to the recipient, it is vulnerable to proof hijacking.
An attacker monitors the destination chain for direct transfers matching active intent requests. The moment they detect a transfer, they extract the transaction details and generate the corresponding proof. The attacker then submits the proof to the source chain settlement contract, specifying their own address as the reward recipient.
Because the settlement contract does not verify that the source-chain claimant matches the sender of the destination-chain transaction, the attacker receives the reimbursement. The solver who actually deployed the capital is left empty-handed.
Mitigation
To prevent proof hijacking, the destination-chain execution state must bind the identity of the executing solver to the execution event.
- If a custom settlement contract is used to fill the order, the event logged on the destination chain must include msg.sender as the verified solver:
solidity
emit OrderFilled(orderHash, msg.sender, recipient);
- The verification system (e.g., storage proofs or optimistic relays) must verify that the claimant on the source chain matches the solver logged in the destination chain's state.
Attack Vector 3: Malicious Callbacks (The Recipient Trap)
Another critical threat to solvers involves malicious interactions with recipient contracts. An attacker can construct an intent where the recipient is a smart contract designed to disrupt the solver's transaction execution.
Gas-Exhaustion DoS
If the settlement contract or the solver's execution script transfers native tokens (ETH) to the recipient using call, the recipient contract's receive or fallback function executes code:
(bool success, ) = order.recipient.call{value: order.amountOut}("");
require(success, "Transfer failed");
A malicious recipient can execute an infinite loop or consume a massive amount of gas inside its fallback function.
- If the solver does not limit the gas passed to the external call, the recipient can drain the solver's transaction gas limit, causing the transaction to revert.
- Even if the transaction reverts and the solver does not lose the transfer amount, the solver still pays the gas fee for the failed transaction. The attacker can exploit this to perform a DoS attack, causing the solver's automated execution bot to repeatedly attempt execution and burn gas fees.
Reentrancy During Token Hooks
If the output token is an ERC-777 token or has transfer hooks (like ERC-1363 or specific custom tokens), transferring the token triggers a callback in the recipient contract before the transfer completes.
If the destination-chain settlement contract does not employ strict reentrancy protection (like OpenZeppelin's ReentrancyGuard) and does not follow the Checks-Effects-Interactions pattern, the malicious recipient can re-enter the fillOrder function and trigger a double-execution. If the execution tracking state is updated after the token transfer, the contract will allow the re-entrant call to proceed, draining the solver's contract inventory.
Mitigation
- Use Gas Limits on External Calls: When transferring native gas tokens to external addresses, limit the gas forwarded to prevent gas-exhaustion attacks.
solidity (bool success, ) = order.recipient.call{value: order.amountOut, gas: 5000}(""); - Implement Reentrancy Protection: Always apply the
nonReentrantmodifier to the execution functions and update all internal states (e.g.,executedOrders[orderHash] = true) before executing external calls or token transfers. - Avoid Token Hooks on Native Execution: Ensure that destination settlers do not support callbacks or hooks unless strictly necessary, and enforce strict gas ceilings on those operations.
Pre-Deployment Solver Security Checklist
Before deploying an ERC-7683 solver or settlement system, ensure the following checks are met:
- [ ] EIP-712 Compliance: Ensure all order hashes incorporate a robust domain separator containing
chainIdand the target contract address to prevent cross-chain signature replays. - [ ] State Update Order: Enforce the Checks-Effects-Interactions pattern. Mark orders as executed in the state registry before initiating token transfers or external calls.
- [ ] Solver Identity Binding: Verify that the settlement contract on the source chain only reimburses the address logged as the caller (
msg.sender) of the execution transaction on the destination chain. - [ ] Gas Limits on Native Transfers: Restrict the gas forwarded to recipient contracts during native ETH transfers to prevent gas-exhaustion denial-of-service attacks.
- [ ] Reentrancy Guard: Equip all execution entry points on the settlement contracts with
nonReentrantmodifiers to protect against malicious token callbacks. - [ ] Deadline Validation: Verify that order expirations (
expiryordeadlinefields) are strictly validated on-chain to prevent solvers from executing stale orders that can no longer be settled on the source chain.
FAQ
How does ERC-7683 handle signature verification across different chains?
ERC-7683 relies on EIP-712 typed data signing. The signed order structure contains parameters detailing the origin and destination chain IDs. Solvers and settlement contracts verify the signature against this structured data. If the signature is presented on a chain that does not match the destinationChainId in the signed structure, the validation checks fail, preventing unauthorized execution.
What is the difference between signature replay and proof hijacking in cross-chain intents?
Signature replay occurs when an attacker takes a valid swapper signature and submits it to another chain to trick a solver into executing a fill for which no escrow exists. Proof hijacking occurs when a solver has already executed a valid fill, but an attacker submits the transaction proof to the source chain first, claiming the refund instead of the solver.
Can a solver lose funds if the source chain bridge is exploited?
Yes. If the underlying bridge or messaging protocol used to relay the proof of execution from the destination chain back to the source chain is compromised, the settlement system may fail. In such scenarios, the solver's proof of execution will not be recognized on the source chain, and the escrowed funds will remain locked or be returned to the swapper, resulting in a loss of capital for the solver.
Secure Your Protocol
Fulfilling cross-chain intents requires absolute cryptographic precision. A single missing field in a domain separator or a loose proof-validation check can lead to severe capital drain.
Ensure your code is thoroughly vetted. Audit your settlement contracts and automated execution systems with ContractScan to identify security vulnerabilities before they are exploited.