Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3bd2c48
Multichain escrowed inputs
reednaa Jul 10, 2025
0d8fdbc
forge fmt
reednaa Jul 10, 2025
77352ae
Compact multichain claim
reednaa Jul 11, 2025
a6afeae
Standardize both escrow and compact settler with a single order
reednaa Jul 14, 2025
5b2f60e
Use chainId field to specify which chains an input belongs to
reednaa Jul 14, 2025
a5b6217
fix coverage
reednaa Jul 14, 2025
cc8cc22
Fix typos
reednaa Jul 14, 2025
ebc624a
Update escrow chain hash documentation
reednaa Jul 30, 2025
07b01d8
Merge branch 'main' into compact-multichain-1
reednaa Jul 30, 2025
3b6f051
Merge fixes
reednaa Jul 30, 2025
059e7f4
fmt
reednaa Jul 30, 2025
dd6c392
Significantly optimise the order identifier computation for multichai…
reednaa Jul 30, 2025
7c05be1
fmt
reednaa Jul 30, 2025
34a9b7e
Merge branch 'main' into compact-multichain-1
reednaa Sep 5, 2025
d2766d5
Cleanup merge issued
reednaa Sep 5, 2025
e63427d
Fix stack too deep in test
reednaa Sep 5, 2025
9d0738a
Test MultichainOrderComponentType
reednaa Sep 5, 2025
a524ee2
fmt
reednaa Sep 5, 2025
67dc7e2
Multichain compact order identifier
reednaa Sep 6, 2025
c70ce36
Test for InputSettlerMultichainCompact
reednaa Sep 8, 2025
201a0b1
Improve coverage by adding a signature finalise test to existing
reednaa Sep 8, 2025
66bb0e0
Improve CI run time by decreasing runs
reednaa Sep 8, 2025
fbe0a34
Fix compile errors
reednaa Sep 8, 2025
ff40047
Initial draft for permit2 support for multichain escrow
reednaa Sep 8, 2025
efc8287
Add permit2 support to multichain escrow
reednaa Sep 8, 2025
b12f660
Update documentation
reednaa Sep 8, 2025
b91cc0f
Merge remote-tracking branch 'origin/main' into compact-multichain-1
reednaa Oct 29, 2025
68cc406
Fix merge issues
reednaa Oct 29, 2025
3d6783c
Merge branch 'main' of https://github.com/openintentsframework/OIF in…
reednaa Oct 30, 2025
826521a
Merge branch 'main' of https://github.com/openintentsframework/OIF in…
reednaa Oct 30, 2025
d2e6096
fmt
reednaa Oct 30, 2025
d9d9768
Remote console log used for debugging
reednaa Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ evm_version = "cancun"
via_ir = true
optimizer = true
optimizer_runs = 100_000_000
fuzz.runs = 10_000
fuzz.runs = 1000

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

Expand Down
8 changes: 4 additions & 4 deletions snapshots/inputSettler.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"BasePurchaseOrder": "138330",
"CompactFinaliseFor": "137653",
"CompactFinaliseSelf": "128815",
"CompactFinaliseTo": "128815",
"EscrowFinalise": "76395",
"CompactFinaliseSelf": "128820",
"CompactFinaliseTo": "128820",
"EscrowFinalise": "76400",
"IntegrationCoinFill": "89913",
"IntegrationCompactFinaliseSelf": "125396",
"IntegrationCompactFinaliseSelf": "125401",
"IntegrationWormholeReceiveMessage": "73509",
"IntegrationWormholeSubmit": "42285",
"escrowFinaliseWithSignature": "85128",
Expand Down
14 changes: 7 additions & 7 deletions snapshots/outputSettler.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"outputSettlerCoinFill": "83343",
"outputSettlerCoinFillDutchAuction": "84573",
"outputSettlerCoinFillExclusive": "84739",
"outputSettlerCoinFillExclusiveDutchAuction": "85453",
"outputSettlerCoinFillNative": "89358",
"outputSettlerCoinFillOrderOutputsNative": "128860",
"outputSettlerCoinfillOrderOutputs": "119906"
"outputSettlerSimpleFill": "83343",
"outputSettlerSimpleFillDutchAuction": "84573",
"outputSettlerSimpleFillExclusive": "84739",
"outputSettlerSimpleFillExclusiveDutchAuction": "85453",
"outputSettlerSimpleFillNative": "89358",
"outputSettlerSimpleFillOrderOutputsNative": "128860",
"outputSettlerSimplefillOrderOutputs": "119906"
}
15 changes: 15 additions & 0 deletions src/input/InputSettlerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ abstract contract InputSettlerBase is EIP712 {
* @dev Fill deadline is after expiry deadline.
*/
error FillDeadlineAfterExpiry(uint32 fillDeadline, uint32 expires);
/**
* @dev msg.sender was expected to be `expectedCaller`.
*/
error UnexpectedCaller(bytes32 expectedCaller);

/**
* @notice Emitted when an order is finalised.
Expand Down Expand Up @@ -138,6 +142,17 @@ abstract contract InputSettlerBase is EIP712 {
if (isZero) revert NoDestination();
}

/**
* @notice Enforces a specific caller
* @dev Only reads the rightmost 20 bytes to allow providing additional destination context
* @param expectedCaller Expected caller. The leftmost 12 bytes are not read.
*/
function _validateIsCaller(
bytes32 expectedCaller
) internal view {
if (LibAddress.fromIdentifier(expectedCaller) != msg.sender) revert UnexpectedCaller(expectedCaller);
}

// --- Timestamp Helpers --- //

/**
Expand Down
13 changes: 0 additions & 13 deletions src/input/InputSettlerPurchase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,6 @@ abstract contract InputSettlerPurchase is InputSettlerBase {

// --- Order Purchase Helpers --- //

/**
* @notice Enforces that the caller is the order owner.
* @dev Only reads the rightmost 20 bytes to verify the owner/purchaser. This allows implementations to use the
* leftmost 12 bytes to encode further withdrawal logic.
* For TheCompact, 12 zero bytes indicates a withdrawals instead of a transfer.
* @param orderOwner The order owner. The leftmost 12 bytes are not read.
*/
function _orderOwnerIsCaller(
bytes32 orderOwner
) internal view {
if (orderOwner.fromIdentifier() != msg.sender) revert NotOrderOwner();
}

/**
* @notice Helper function to get the owner of order incase it may have been bought. In case an order has been
* bought, and bought in time, the owner will be set to the purchaser. Otherwise it will be set to the solver.
Expand Down
2 changes: 1 addition & 1 deletion src/input/compact/InputSettlerCompact.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ contract InputSettlerCompact is InputSettlerPurchase, IInputSettlerCompact {

bytes32 orderId = _orderIdentifier(order);
bytes32 orderOwner = _purchaseGetOrderOwner(orderId, solveParams);
_orderOwnerIsCaller(orderOwner);
_validateIsCaller(orderOwner);

_validateFills(order.fillDeadline, order.inputOracle, order.outputs, orderId, solveParams);

Expand Down
226 changes: 226 additions & 0 deletions src/input/compact/InputSettlerMultichainCompact.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { EIP712 } from "openzeppelin/utils/cryptography/EIP712.sol";

import { TheCompact } from "the-compact/src/TheCompact.sol";
import { EfficiencyLib } from "the-compact/src/lib/EfficiencyLib.sol";
import { IdLib } from "the-compact/src/lib/IdLib.sol";
import { BatchMultichainClaim, ExogenousBatchMultichainClaim } from "the-compact/src/types/BatchMultichainClaims.sol";
import { BatchClaimComponent, Component } from "the-compact/src/types/Components.sol";

import { IInputCallback } from "../../interfaces/IInputCallback.sol";
import { IInputOracle } from "../../interfaces/IInputOracle.sol";

import { BytesLib } from "../../libs/BytesLib.sol";
import { LibAddress } from "../../libs/LibAddress.sol";
import { MandateOutputEncodingLib } from "../../libs/MandateOutputEncodingLib.sol";

import { InputSettlerBase } from "../InputSettlerBase.sol";
import { MandateOutput } from "../types/MandateOutputType.sol";

import { MultichainCompactOrderType, MultichainOrderComponent } from "../types/MultichainCompactOrderType.sol";
import { OrderPurchase } from "../types/OrderPurchaseType.sol";

/**
* @title Multichain Input Settler using `The Compact` as an escrow.
* @notice This Input Settler implementation uses The Compact as the deposit scheme. It is a Output first scheme that
* allows users with a deposit inside The Compact to execute transactions that will be paid **after** the outputs have
* been proven. This has the advantage that failed orders can be quickly retried. These orders are also entirely gasless
* since neither valid nor failed transactions does not require any transactions to redeem.
*
* Users are expected to have an existing deposit inside the Compact or purposefully deposit for the intent. Then either
* register or sign a supported claim with the intent outputs as the witness.
*
* A multichain intent is an intent that collects tokens on multiple chains in exchange for single set of outputs. Using
* TheCompact allows users to issue a multichain input intent using a single Multichain Compact signature.
*
* The contract is intended to be entirely ownerless, permissionlessly deployable, and unstoppable.
*/
contract InputSettlerMultichainCompact is InputSettlerBase {
using LibAddress for bytes32;
using LibAddress for uint256;
error UserCannotBeSettler();

TheCompact public immutable COMPACT;

constructor(
address compact
) EIP712("OIFMultichainEscrow", "1") {
COMPACT = TheCompact(compact);
}

// --- Generic order identifier --- //

function _orderIdentifier(
MultichainOrderComponent calldata order
) internal view returns (bytes32) {
return MultichainCompactOrderType.orderIdentifier(order);
}

function orderIdentifier(
MultichainOrderComponent calldata order
) external view returns (bytes32) {
return _orderIdentifier(order);
}

// --- Finalise Orders --- //

/**
* @notice Finalise an order, paying the inputs to the solver.
* @param order that has been filled.
* @param signatures For the signed intent. Is packed: abi.encode(sponsorSignature, allocatorData).
* @param solver Solver of the outputs.
* @param destination Destination of the inputs funds signed for by the user.
* @return orderId Returns a unique global order identifier.
*/
function _finalise(
MultichainOrderComponent calldata order,
bytes calldata signatures,
bytes32 solver,
bytes32 destination
) internal virtual returns (bytes32 orderId) {
bytes calldata sponsorSignature = BytesLib.toBytes(signatures, 0x00);
bytes calldata allocatorData = BytesLib.toBytes(signatures, 0x20);
orderId = _resolveLock(order, sponsorSignature, allocatorData, destination);
emit Finalised(orderId, solver, destination);
}

/**
* @notice Finalises an order when called directly by the solver
* @dev The caller must be the address corresponding to the first solver in the solvers array.
* @param order MultichainOrderComponent signed in conjunction with a Compact to form an order
* @param signatures A signature for the sponsor and the allocator. abi.encode(bytes(sponsorSignature),
* bytes(allocatorData))
* @param solveParams List of solve parameters for when the outputs were filled
* @param destination Where to send the inputs. If the solver wants to send the inputs to themselves, they should
* pass their address to this parameter.
* @param call Optional callback data. If non-empty, will call orderFinalised on the destination
*/
function finalise(
MultichainOrderComponent calldata order,
bytes calldata signatures,
SolveParams[] calldata solveParams,
bytes32 destination,
bytes calldata call
) external virtual {
_validateDestination(destination);

_validateIsCaller(solveParams[0].solver);

bytes32 orderId = _finalise(order, signatures, solveParams[0].solver, destination);

_validateFills(order.fillDeadline, order.inputOracle, order.outputs, orderId, solveParams);

if (call.length > 0) {
IInputCallback(EfficiencyLib.asSanitizedAddress(uint256(destination))).orderFinalised(order.inputs, call);
}
}

/**
* @notice Finalises a cross-chain order on behalf of someone else using their signature
* @dev This function serves to finalise intents on the origin chain with proper authorization from the order owner.
* @param order MultichainOrderComponent signed in conjunction with a Compact to form an order
* @param signatures A signature for the sponsor and the allocator. abi.encode(bytes(sponsorSignature),
* bytes(allocatorData))
* @param solveParams List of solve parameters for when the outputs were filled
* @param destination Where to send the inputs
* @param call Optional callback data. If non-empty, will call orderFinalised on the destination
* @param orderOwnerSignature Signature from the order owner authorizing this external call
*/
function finaliseWithSignature(
MultichainOrderComponent calldata order,
bytes calldata signatures,
SolveParams[] calldata solveParams,
bytes32 destination,
bytes calldata call,
bytes calldata orderOwnerSignature
) external virtual {
if (destination == bytes32(0)) revert NoDestination();

bytes32 orderId = _finalise(order, signatures, solveParams[0].solver, destination);

// Validate the external claimant with signature
_allowExternalClaimant(orderId, solveParams[0].solver.fromIdentifier(), destination, call, orderOwnerSignature);

_validateFills(order.fillDeadline, order.inputOracle, order.outputs, orderId, solveParams);

if (call.length > 0) IInputCallback(destination.fromIdentifier()).orderFinalised(order.inputs, call);
}

//--- The Compact & Resource Locks ---//

/**
* @notice Resolves a Compact Claim for a Standard Order.
* @param order that should be converted into a Compact Claim.
* @param sponsorSignature The user's signature for the Compact Claim.
* @param allocatorData The allocator's signature for the Compact Claim.
* @param claimant Destination of the inputs funds signed for by the user.
* @return claimHash The compact claimhash is used as the order identifier, as it is identical for a specific order
* cross-chain.
*/
function _resolveLock(
MultichainOrderComponent calldata order,
bytes calldata sponsorSignature,
bytes calldata allocatorData,
bytes32 claimant
) internal virtual returns (bytes32 claimHash) {
BatchClaimComponent[] memory batchClaimComponents;
{
uint256 numInputs = order.inputs.length;
batchClaimComponents = new BatchClaimComponent[](numInputs);
uint256[2][] calldata maxInputs = order.inputs;
for (uint256 i; i < numInputs; ++i) {
uint256[2] calldata input = maxInputs[i];
uint256 tokenId = input[0];
uint256 allocatedAmount = input[1];

Component[] memory components = new Component[](1);
components[0] = Component({ claimant: uint256(claimant), amount: allocatedAmount });
batchClaimComponents[i] = BatchClaimComponent({
id: tokenId, // The token ID of the ERC6909 token to allocate.
allocatedAmount: allocatedAmount, // The original allocated amount of ERC6909 tokens.
portions: components
});
}
}

address user = order.user;
// The Compact skips signature checks for msg.sender. Ensure no accidental intents are issued.
if (user == address(this)) revert UserCannotBeSettler();
if (order.chainIdField == block.chainid) {
claimHash = COMPACT.batchMultichainClaim(
BatchMultichainClaim({
allocatorData: allocatorData,
sponsorSignature: sponsorSignature,
sponsor: user,
nonce: order.nonce,
expires: order.expires,
witness: MultichainCompactOrderType.witnessHash(order),
witnessTypestring: string(MultichainCompactOrderType.BATCH_COMPACT_SUB_TYPES),
claims: batchClaimComponents,
additionalChains: order.additionalChains
})
);
} else {
claimHash = COMPACT.exogenousBatchClaim(
ExogenousBatchMultichainClaim({
allocatorData: allocatorData,
sponsorSignature: sponsorSignature,
sponsor: user,
nonce: order.nonce,
expires: order.expires,
witness: MultichainCompactOrderType.witnessHash(order),
witnessTypestring: string(MultichainCompactOrderType.BATCH_COMPACT_SUB_TYPES),
claims: batchClaimComponents,
additionalChains: order.additionalChains,
chainIndex: order.chainIndex - 1, // We use chainIndex as the offset to elements array where compact
// uses it as offset to the notarized.
notarizedChainId: order.chainIdField
Comment on lines +219 to +221
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard chainIndex - 1 to avoid underflow on exogenous chains.

In the exogenous path we do chainIndex: order.chainIndex - 1. If a component for a non-origin chain is positioned at index 0 in the global elements ordering (totally valid for two components ordered [remote, origin]), this subtraction underflows and hard reverts, making those orders impossible to finalise. Please require order.chainIndex > 0 (and revert with a clear error) before subtracting, or otherwise adjust the logic so we never subtract blindly.

🤖 Prompt for AI Agents
In src/input/compact/InputSettlerMultichainCompact.sol around lines 223 to 225,
the exogenous path subtracts 1 from order.chainIndex without checking and can
underflow when chainIndex == 0; add a guard that requires order.chainIndex > 0
(revert with a clear error string) before performing chainIndex - 1, or alter
the branch so it never performs the subtraction for index 0 (e.g., handle the
origin-at-zero case separately), and update any dependent variables to use the
validated value.

})
);
}
}
}
3 changes: 2 additions & 1 deletion src/input/escrow/InputSettlerEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { OrderPurchase } from "../types/OrderPurchaseType.sol";
import { StandardOrder, StandardOrderType } from "../types/StandardOrderType.sol";

import { InputSettlerPurchase } from "../InputSettlerPurchase.sol";

import { Permit2WitnessType } from "./Permit2WitnessType.sol";

/**
Expand Down Expand Up @@ -396,7 +397,7 @@ contract InputSettlerEscrow is InputSettlerPurchase, IInputSettlerEscrow {

bytes32 orderId = order.orderIdentifier();
bytes32 orderOwner = _purchaseGetOrderOwner(orderId, solveParams);
_orderOwnerIsCaller(orderOwner);
_validateIsCaller(orderOwner);

_validateFills(order.fillDeadline, order.inputOracle, order.outputs, orderId, solveParams);

Expand Down
Loading
Loading