Intent-Based Architecture
Overview
Intent-based architecture represents a fundamental paradigm shift in how users interact with blockchain systems. Rather than specifying exact execution paths (imperative transactions), users declare desired outcomes (intents) and delegate execution to competitive third parties called solvers or fillers. The system selects the solver that best satisfies the intent, improving execution quality, reducing user complexity, and enabling MEV protection.
1. Intents vs Transactions — The Paradigm Shift
Imperative (Transaction) Model
Traditional blockchain interaction requires users to specify every step:
User → signs tx → specifies: exact route, slippage, gas price, nonce, exact calldata
→ submits → mempool → miner/validator executes exactly as specified
Problems:
- Users bear execution risk (price changes between sign and execution)
- Suboptimal routing — user must know best path
- MEV exposure — front-running, sandwich attacks on public mempool
- Failed transactions still cost gas
- Cross-chain operations require multiple coordinated transactions
Declarative (Intent) Model
User → signs intent → specifies: input token, output token, minimum output, deadline
→ propagates to solver network → competitive off-chain solving
→ best solver executes on-chain → settlement contract verifies output constraint
Benefits:
- Users specify what, not how
- Solvers compete on execution quality → better prices
- MEV protection through solver competition and surplus sharing
- Cross-chain atomicity becomes feasible
- Failed solving has no gas cost for users (in most designs)
Declarative vs Imperative Comparison
| Dimension | Imperative (Tx) | Declarative (Intent) |
|---|---|---|
| User burden | High — must specify route | Low — specify outcome |
| MEV exposure | High — public mempool | Low — private solving |
| Execution quality | Fixed at signing | Competitive, improving |
| Cross-chain | Sequential, fragile | Atomic possible |
| Gas on failure | Yes | No (typically) |
| Composability | High (on-chain) | Evolving (ERC-7683) |
2. Intent Lifecycle
Phase 1: Creation
User constructs a typed intent object specifying constraints:
- Input asset and amount
- Output asset and minimum amount
- Deadline (timestamp or block)
- Exclusivity conditions (optional)
- Additional data (hooks, callbacks)
Phase 2: Signing (EIP-712)
Intents are signed off-chain using EIP-712 structured data. The signature commits the user to the constraints without broadcasting a transaction.
import { ethers } from "ethers";
const DUTCH_ORDER_TYPEHASH = ethers.utils.id(
"DutchOrder(OrderInfo info,uint256 decayStartTime,uint256 decayEndTime," +
"address exclusiveFiller,uint256 exclusivityOverrideBps,DutchInput input," +
"DutchOutput[] outputs)" +
"DutchInput(address token,uint256 startAmount,uint256 endAmount)" +
"DutchOutput(address token,uint256 startAmount,uint256 endAmount,address recipient)" +
"OrderInfo(address reactor,address swapper,uint256 nonce,uint256 deadline," +
"address additionalValidationContract,bytes additionalValidationData)"
);
const domain = {
name: "UniswapX",
chainId: 1,
verifyingContract: REACTOR_ADDRESS,
};
const types = {
DutchOrder: [
{ name: "info", type: "OrderInfo" },
{ name: "decayStartTime", type: "uint256" },
{ name: "decayEndTime", type: "uint256" },
{ name: "exclusiveFiller", type: "address" },
{ name: "exclusivityOverrideBps", type: "uint256" },
{ name: "input", type: "DutchInput" },
{ name: "outputs", type: "DutchOutput[]" },
],
OrderInfo: [
{ name: "reactor", type: "address" },
{ name: "swapper", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "additionalValidationContract", type: "address" },
{ name: "additionalValidationData", type: "bytes" },
],
DutchInput: [
{ name: "token", type: "address" },
{ name: "startAmount", type: "uint256" },
{ name: "endAmount", type: "uint256" },
],
DutchOutput: [
{ name: "token", type: "address" },
{ name: "startAmount", type: "uint256" },
{ name: "endAmount", type: "uint256" },
{ name: "recipient", type: "address" },
],
};
async function signDutchOrder(signer: ethers.Signer, order: DutchOrder) {
// ethers v5 _signTypedData
const signature = await (signer as any)._signTypedData(domain, types, order);
return signature;
}
// Reconstruct signer from signature for verification
function recoverSigner(order: DutchOrder, signature: string): string {
const digest = ethers.utils._TypedDataEncoder.hash(domain, types, order);
return ethers.utils.recoverAddress(digest, signature);
}
Phase 3: Propagation
Signed intents are broadcast to:
- Protocol-specific APIs (UniswapX order endpoint, CoW Protocol API)
- Peer-to-peer solver networks
- Public order books (permissionless)
Phase 4: Solving (Off-Chain)
Solvers receive intents and compute execution strategies:
- Check validity (deadline, nonce not spent, sufficient user balance/allowance)
- Simulate execution paths (on-chain simulation, off-chain price modeling)
- Determine if profitable to fill
- Submit fill transaction (competing with other solvers)
Phase 5: Settlement (On-Chain)
Reactor/settlement contract:
- Validates signature
- Checks nonce not spent (marks nonce as used)
- Pulls input tokens from user (via permit2 or direct allowance)
- Calls solver's fill logic
- Verifies output constraints are satisfied
- If output insufficient, reverts entire transaction
Phase 6: Verification
Post-settlement verification:
- Event emission for off-chain indexers
- Nonce invalidation prevents replay
- Output token balance check is the atomic guarantee
3. UniswapX
Architecture
UniswapX separates concerns across three layers:
Reactor Layer — on-chain settlement contracts that enforce constraints Solver Layer — off-chain competitive actors that find execution Order Book — off-chain signed order propagation (UniswapX API)
User → signs order → UniswapX API → solver network
↓
solver picks best order
↓
solver executes fill tx
↓
Reactor validates & settles
Core Reactor Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IReactor} from "./interfaces/IReactor.sol";
import {IReactorCallback} from "./interfaces/IReactorCallback.sol";
import {SignedOrder, ResolvedOrder, OutputToken, InputToken} from "./base/ReactorStructs.sol";
import {Permit2Lib} from "./lib/Permit2Lib.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
abstract contract BaseReactor is IReactor {
using Permit2Lib for ResolvedOrder;
IPermit2 public immutable permit2;
error InvalidSigner();
error DeadlinePassed();
error InsufficientOutput(
uint256 actualAmount,
uint256 expectedAmount
);
constructor(IPermit2 _permit2) {
permit2 = _permit2;
}
/// @notice Execute a single order
function execute(SignedOrder calldata order) external payable override {
ResolvedOrder[] memory resolvedOrders = new ResolvedOrder[](1);
resolvedOrders[0] = resolve(order);
_fill(resolvedOrders, msg.sender, bytes(""));
}
/// @notice Execute with callback for complex filling strategies
function executeWithCallback(
SignedOrder calldata order,
bytes calldata callbackData
) external payable override {
ResolvedOrder[] memory resolvedOrders = new ResolvedOrder[](1);
resolvedOrders[0] = resolve(order);
_fill(resolvedOrders, msg.sender, callbackData);
}
/// @notice Execute batch of orders atomically
function executeBatch(SignedOrder[] calldata orders) external payable override {
ResolvedOrder[] memory resolvedOrders = new ResolvedOrder[](orders.length);
for (uint256 i = 0; i < orders.length; i++) {
resolvedOrders[i] = resolve(orders[i]);
}
_fill(resolvedOrders, msg.sender, bytes(""));
}
function _fill(
ResolvedOrder[] memory orders,
address filler,
bytes memory callbackData
) internal {
// Pull input tokens from swappers via permit2
for (uint256 i = 0; i < orders.length; i++) {
orders[i].transferInputTokens(permit2);
}
// Callback to filler to provide output tokens
if (callbackData.length > 0) {
IReactorCallback(filler).reactorCallback(orders, callbackData);
}
// Verify all outputs are satisfied
for (uint256 i = 0; i < orders.length; i++) {
_verify(orders[i]);
emit Fill(
keccak256(orders[i].sig),
orders[i].info.swapper,
orders[i].info.nonce,
filler
);
}
}
function _verify(ResolvedOrder memory order) internal view {
for (uint256 i = 0; i < order.outputs.length; i++) {
OutputToken memory output = order.outputs[i];
uint256 balance = output.token.balanceOf(output.recipient);
// Check balance satisfies minimum output
// (simplified — real impl uses pre/post balance tracking)
if (balance < output.amount) {
revert InsufficientOutput(balance, output.amount);
}
}
}
/// @dev Subclasses implement order-specific resolution (Dutch decay, etc.)
function resolve(SignedOrder calldata order)
internal
virtual
returns (ResolvedOrder memory);
}
Dutch Auction Orders
Price decays from a start (favorable to filler) to an end (minimum acceptable to user). Fillers are incentivized to fill early (better margin), but competition drives them to fill at the best price for the user.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
struct OrderInfo {
address reactor;
address swapper;
uint256 nonce;
uint256 deadline;
address additionalValidationContract;
bytes additionalValidationData;
}
struct DutchInput {
address token;
uint256 startAmount; // amount at decayStartTime (favorable to filler)
uint256 endAmount; // amount at decayEndTime (minimum user provides)
}
struct DutchOutput {
address token;
uint256 startAmount; // amount at decayStartTime (user receives most)
uint256 endAmount; // amount at decayEndTime (minimum user receives)
address recipient;
}
struct DutchOrder {
OrderInfo info;
uint256 decayStartTime;
uint256 decayEndTime;
address exclusiveFiller; // address(0) if no exclusivity
uint256 exclusivityOverrideBps; // basis points penalty if non-exclusive filler fills early
DutchInput input;
DutchOutput[] outputs;
}
library DutchDecayLib {
/// @notice Calculates decayed amount at current block.timestamp
/// @param startAmount Amount at decayStartTime
/// @param endAmount Amount at decayEndTime
/// @param decayStartTime Start of decay period
/// @param decayEndTime End of decay period
function decay(
uint256 startAmount,
uint256 endAmount,
uint256 decayStartTime,
uint256 decayEndTime
) internal view returns (uint256) {
if (decayEndTime <= decayStartTime) revert("InvalidDecay");
if (block.timestamp >= decayEndTime) return endAmount;
if (block.timestamp <= decayStartTime) return startAmount;
uint256 elapsed = block.timestamp - decayStartTime;
uint256 duration = decayEndTime - decayStartTime;
if (endAmount < startAmount) {
// Decaying downward (output token: user gets less over time)
uint256 decayAmount = startAmount - endAmount;
return startAmount - (decayAmount * elapsed) / duration;
} else {
// Decaying upward (input token: user provides more over time)
uint256 decayAmount = endAmount - startAmount;
return startAmount + (decayAmount * elapsed) / duration;
}
}
}
contract DutchOrderReactor is BaseReactor {
using DutchDecayLib for uint256;
bytes32 constant DUTCH_ORDER_TYPEHASH = keccak256(
"DutchOrder(OrderInfo info,uint256 decayStartTime,uint256 decayEndTime,"
"address exclusiveFiller,uint256 exclusivityOverrideBps,DutchInput input,"
"DutchOutput[] outputs)"
"DutchInput(address token,uint256 startAmount,uint256 endAmount)"
"DutchOutput(address token,uint256 startAmount,uint256 endAmount,address recipient)"
"OrderInfo(address reactor,address swapper,uint256 nonce,uint256 deadline,"
"address additionalValidationContract,bytes additionalValidationData)"
);
error NotExclusiveFiller(address filler, address exclusiveFiller);
error OrderExpired();
constructor(IPermit2 _permit2) BaseReactor(_permit2) {}
function resolve(SignedOrder calldata signedOrder)
internal
override
returns (ResolvedOrder memory)
{
DutchOrder memory order = abi.decode(signedOrder.order, (DutchOrder));
if (block.timestamp > order.info.deadline) revert OrderExpired();
// Verify signature
bytes32 orderHash = _hashOrder(order);
address signer = ECDSA.recover(orderHash, signedOrder.sig);
if (signer != order.info.swapper) revert InvalidSigner();
// Check exclusivity
if (
order.exclusiveFiller != address(0) &&
block.timestamp < order.decayStartTime &&
msg.sender != order.exclusiveFiller
) {
// Non-exclusive filler can still fill but must provide override bonus
revert NotExclusiveFiller(msg.sender, order.exclusiveFiller);
}
// Resolve decayed amounts
uint256 inputAmount = DutchDecayLib.decay(
order.input.startAmount,
order.input.endAmount,
order.decayStartTime,
order.decayEndTime
);
OutputToken[] memory outputs = new OutputToken[](order.outputs.length);
for (uint256 i = 0; i < order.outputs.length; i++) {
outputs[i] = OutputToken({
token: order.outputs[i].token,
amount: DutchDecayLib.decay(
order.outputs[i].startAmount,
order.outputs[i].endAmount,
order.decayStartTime,
order.decayEndTime
),
recipient: order.outputs[i].recipient
});
}
return ResolvedOrder({
info: order.info,
input: InputToken({
token: order.input.token,
amount: inputAmount,
maxAmount: order.input.endAmount
}),
outputs: outputs,
sig: signedOrder.sig,
hash: orderHash
});
}
function _hashOrder(DutchOrder memory order) internal pure returns (bytes32) {
return keccak256(abi.encode(
DUTCH_ORDER_TYPEHASH,
// ... encode all fields
order.decayStartTime,
order.decayEndTime,
order.exclusiveFiller,
order.exclusivityOverrideBps,
keccak256(abi.encode(order.input)),
keccak256(_encodeOutputs(order.outputs))
));
}
}
Exclusive Fillers
Exclusive fillers have a time window where only they can fill the order (e.g., market makers who committed off-chain). After exclusivity expires, any solver can fill with a penalty bonus to the user.
Cross-Chain UniswapX
Extends the model to cross-chain swaps:
- User signs intent specifying input on chain A, output on chain B
- Solver fills on destination chain, proving via cross-chain messaging
- Reactor on origin chain releases funds to solver after proof
4. CoW Protocol
Architecture
CoW Protocol (formerly Gnosis Protocol) uses batch auctions with coincidence of wants (CoW) detection.
Users → submit limit orders to CoW API
→ batch auction runs every ~30 seconds
→ solver competition: find optimal settlement
→ best solver submits settlement tx
→ GPv2Settlement executes all trades atomically
Coincidence of Wants
When two users' orders can be matched directly (A sells ETH for USDC, B sells USDC for ETH), no AMM liquidity is needed — surplus is shared between both parties.
Without CoW:
Alice: 1 ETH → 2000 USDC (uses Uniswap, pays 0.3% fee + MEV)
Bob: 2100 USDC → 1 ETH (uses Uniswap, pays 0.3% fee + MEV)
With CoW:
Alice gets 2050 USDC (better than market)
Bob gets 1 ETH for 2050 USDC (better than market)
Solver gets 50 USDC surplus for finding the CoW
GPv2Settlement Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// Simplified GPv2Settlement structure
contract GPv2Settlement {
// Domain separator for EIP-712 signing
bytes32 public immutable domainSeparator;
// Reentrancy guard
uint256 private reentrancyStatus = 1;
struct GPv2Trade {
uint256 sellTokenIndex; // index in tokens array
uint256 buyTokenIndex; // index in tokens array
address receiver;
uint256 sellAmount;
uint256 buyAmount; // limit price denominator
uint32 validTo; // order expiry
bytes32 appData; // arbitrary metadata hash
uint256 feeAmount; // protocol fee
uint256 flags; // isPartiallyFillable, isSell, etc.
uint256 executedAmount; // actual fill amount
bytes signature;
}
struct GPv2Interaction {
address target;
uint256 value;
bytes callData;
}
event Trade(
address indexed owner,
address sellToken,
address buyToken,
uint256 sellAmount,
uint256 buyAmount,
uint256 feeAmount,
bytes32 orderUid
);
event Settlement(address indexed solver);
/// @notice Settle a batch of trades
/// @param tokens Unique token list referenced by trade indices
/// @param clearingPrices Price vector: clearingPrices[i] = price of tokens[i] in batch unit
/// @param trades All trades in this batch
/// @param interactions Pre/intra/post settlement interactions (AMM calls, etc.)
function settle(
address[] calldata tokens,
uint256[] calldata clearingPrices,
GPv2Trade[] calldata trades,
GPv2Interaction[][3] calldata interactions
) external nonReentrant {
// Pre-settlement interactions (e.g., flashloans)
_executeInteractions(interactions[0]);
// Transfer sell tokens from traders to settlement contract
for (uint256 i = 0; i < trades.length; i++) {
_transferSellTokenIn(tokens, clearingPrices, trades[i]);
}
// Intra-settlement interactions (e.g., AMM swaps to fill gaps)
_executeInteractions(interactions[1]);
// Transfer buy tokens to traders
for (uint256 i = 0; i < trades.length; i++) {
_transferBuyTokenOut(tokens, clearingPrices, trades[i]);
}
// Post-settlement interactions
_executeInteractions(interactions[2]);
emit Settlement(msg.sender);
}
function _transferSellTokenIn(
address[] calldata tokens,
uint256[] calldata prices,
GPv2Trade calldata trade
) internal {
address sellToken = tokens[trade.sellTokenIndex];
// Verify order signature, check limit price, transfer
// Actual amount transferred = executedAmount
IERC20(sellToken).transferFrom(
recoverOrderSigner(trade),
address(this),
trade.executedAmount
);
}
function _executeInteractions(GPv2Interaction[] calldata interactions) internal {
for (uint256 i = 0; i < interactions.length; i++) {
(bool success,) = interactions[i].target.call{value: interactions[i].value}(
interactions[i].callData
);
require(success, "Interaction failed");
}
}
}
Solver Competition
Solvers submit solutions off-chain. The CoW Protocol backend selects the solution that maximizes surplus for traders. Solvers are bonded — slashable if they submit invalid settlements on-chain.
// Solver solution submission (off-chain API)
interface SolverSolution {
prices: Record<string, string>; // token address -> clearing price
trades: Array<{
order: SignedOrder;
executedSellAmount: string;
executedBuyAmount: string;
executedFeeAmount: string;
}>;
interactions: {
pre: Interaction[];
intra: Interaction[];
post: Interaction[];
};
// Objective value: surplus extracted for traders
objectiveValue: string;
}
async function submitSolution(solution: SolverSolution): Promise<void> {
const response = await fetch(`${COW_API}/api/v1/solver/solve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(solution),
});
if (!response.ok) throw new Error(`Solver submission failed: ${response.statusText}`);
}
5. ERC-7683 — Cross-Chain Intent Standard
ERC-7683 defines a standardized interface for cross-chain intents, enabling interoperability between protocols.
CrossChainOrder Struct
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title ERC-7683: Cross-Chain Intents Standard
/// @notice Standardized order types and settlement interfaces for cross-chain intents
/// @notice Tokens that are interacted with during a cross-chain order
struct Output {
bytes32 token; // ERC-7683 uses bytes32 to support non-EVM chains
uint256 amount;
bytes32 recipient;
uint32 chainId;
}
struct Input {
address token;
uint256 amount;
}
/// @notice The top-level cross-chain order type
struct CrossChainOrder {
address settlementContract; // The settlement contract on origin chain
address swapper; // The user placing the order
uint256 nonce; // Unique nonce per swapper
uint32 originChainId; // Chain where order is placed
uint32 initiateDeadline; // Order must be initiated by this time
uint32 fillDeadline; // Order must be filled by this time on destination
bytes orderData; // Arbitrary data for the settlement contract
}
/// @notice Data returned after resolving a cross-chain order
struct ResolvedCrossChainOrder {
address settlementContract;
address swapper;
uint256 nonce;
uint32 originChainId;
uint32 initiateDeadline;
uint32 fillDeadline;
Input[] maxSpent; // Max tokens spent from swapper on origin
Output[] minReceived; // Min tokens received by swapper on destination
FillInstruction[] fillInstructions;
}
/// @notice Instructions for filling on destination chain
struct FillInstruction {
uint64 destinationChainId;
bytes32 destinationSettler; // Settlement contract on destination
bytes originData; // Data to pass to destination settler
}
/// @notice ERC-7683 Origin Chain Interface
interface IOriginSettler {
event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder);
/// @notice Opens a cross-chain order
function open(CrossChainOrder calldata order, bytes calldata signature) external payable;
/// @notice Resolves a cross-chain order into standard types
function resolve(CrossChainOrder calldata order, bytes calldata fillerData)
external
view
returns (ResolvedCrossChainOrder memory);
}
/// @notice ERC-7683 Destination Chain Interface
interface IDestinationSettler {
/// @notice Fills a cross-chain order on destination chain
/// @param orderId Unique identifier for the order
/// @param originData Data emitted from origin chain Open event
/// @param fillerData Additional data provided by filler
function fill(
bytes32 orderId,
bytes calldata originData,
bytes calldata fillerData
) external payable;
}
ERC-7683 Settlement Implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract CrossChainSettler is IOriginSettler, IDestinationSettler {
mapping(bytes32 => OrderStatus) public orderStatus;
mapping(bytes32 => address) public orderFiller;
enum OrderStatus { None, Opened, Filled, Refunded }
error OrderAlreadyOpened();
error OrderExpired();
error InvalidSignature();
error OrderNotFound();
/// @notice Open order on origin chain — lock user funds
function open(
CrossChainOrder calldata order,
bytes calldata signature
) external payable override {
bytes32 orderId = _computeOrderId(order);
if (orderStatus[orderId] != OrderStatus.None) revert OrderAlreadyOpened();
if (block.timestamp > order.initiateDeadline) revert OrderExpired();
// Verify EIP-712 signature
_verifySignature(order, signature);
// Pull input tokens
ResolvedCrossChainOrder memory resolved = _resolve(order, bytes(""));
for (uint256 i = 0; i < resolved.maxSpent.length; i++) {
IERC20(resolved.maxSpent[i].token).transferFrom(
order.swapper,
address(this),
resolved.maxSpent[i].amount
);
}
orderStatus[orderId] = OrderStatus.Opened;
emit Open(orderId, resolved);
}
/// @notice Fill order on destination chain — send output tokens to user
function fill(
bytes32 orderId,
bytes calldata originData,
bytes calldata fillerData
) external payable override {
FillInstruction memory instruction = abi.decode(originData, (FillInstruction));
Output[] memory outputs = abi.decode(fillerData, (Output[]));
for (uint256 i = 0; i < outputs.length; i++) {
if (outputs[i].chainId == block.chainid) {
address token = address(uint160(uint256(outputs[i].token)));
address recipient = address(uint160(uint256(outputs[i].recipient)));
if (token == address(0)) {
payable(recipient).transfer(outputs[i].amount);
} else {
IERC20(token).transferFrom(msg.sender, recipient, outputs[i].amount);
}
}
}
orderFiller[orderId] = msg.sender;
// Emit proof event for origin chain relay
emit Filled(orderId, msg.sender, outputs);
}
function _computeOrderId(CrossChainOrder calldata order) internal pure returns (bytes32) {
return keccak256(abi.encode(order));
}
}
6. 1inch Fusion
Architecture
1inch Fusion uses a Dutch auction with resolver (solver) competition. Key innovation: resolvers must hold 1INCH stake to participate.
Dutch Auction Decay
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
struct FusionOrder {
address makerAsset;
address takerAsset;
uint256 makingAmount; // Input from maker (user)
uint256 takingAmount; // Minimum output to maker
address maker;
uint256 salt; // Encodes auction params
bytes makerTraits; // Encoded deadline, nonce, flags
bytes extension; // Auction timing, resolver whitelist, fees
}
library AuctionLib {
struct AuctionDetails {
uint32 startTime;
uint24 duration;
uint32 initialRateBump; // Initial bonus rate above market (in 1e5 units)
bytes pointsAndTimeDeltas; // Auction decay curve points
}
/// @notice Calculate current rate bump based on auction progress
function getCurrentRateBump(
AuctionDetails memory auction
) internal view returns (uint256 rateBump) {
uint256 currentTime = block.timestamp;
if (currentTime <= auction.startTime) {
return auction.initialRateBump;
}
uint256 endTime = auction.startTime + auction.duration;
if (currentTime >= endTime) {
return 0; // No bonus — at minimum acceptable rate
}
// Linear interpolation (simplified — real implementation uses points curve)
uint256 elapsed = currentTime - auction.startTime;
rateBump = auction.initialRateBump * (auction.duration - elapsed) / auction.duration;
}
/// @notice Calculate taking amount with rate bump applied
function calcTakingAmount(
uint256 orderTakingAmount,
uint256 makingAmount,
uint256 requestedMakingAmount,
AuctionDetails memory auction
) internal view returns (uint256) {
uint256 rateBump = getCurrentRateBump(auction);
// takingAmount = orderTakingAmount * requestedMakingAmount / makingAmount * (1 + rateBump)
uint256 base = orderTakingAmount * requestedMakingAmount / makingAmount;
return base * (1e5 + rateBump) / 1e5;
}
}
Resolver Whitelist & Fee Structure
contract FusionSettlement {
mapping(address => uint256) public resolverStake;
uint256 public constant MIN_STAKE = 100_000e18; // 100k 1INCH
struct ResolverFee {
uint256 protocolFeeRate; // basis points
uint256 integratorFeeRate;
address integratorFeeRecipient;
}
error InsufficientStake(uint256 actual, uint256 required);
error NotWhitelistedResolver();
modifier onlyResolver() {
if (resolverStake[msg.sender] < MIN_STAKE) {
revert InsufficientStake(resolverStake[msg.sender], MIN_STAKE);
}
_;
}
function fillOrder(
FusionOrder calldata order,
bytes calldata signature,
uint256 makingAmount,
uint256 takingAmount
) external onlyResolver {
// Verify order signature (EIP-712)
_verifyOrderSignature(order, signature);
// Transfer maker asset from user to resolver
IERC20(order.makerAsset).transferFrom(order.maker, msg.sender, makingAmount);
// Resolver provides taker asset to user
IERC20(order.takerAsset).transferFrom(msg.sender, order.maker, takingAmount);
// Collect protocol fee
uint256 fee = takingAmount * 2 / 10000; // 2 bps
IERC20(order.takerAsset).transferFrom(msg.sender, FEE_RECIPIENT, fee);
}
}
7. Across Protocol — Intent-Based Bridging
Architecture
Across uses an intent model where relayers (solvers) pre-fund destination chain transfers and are repaid on origin chain via optimistic verification.
User on Chain A:
1. Signs cross-chain intent (input: ETH on Ethereum, output: ETH on Arbitrum)
2. Calls SpokePool.depositV3() — locks funds, emits DepositV3 event
Relayer:
3. Detects DepositV3 event
4. Immediately fills on destination (SpokePool.fillRelay())
5. User receives funds on destination within ~2-10 seconds
Optimistic Verification:
6. Relayer submits repayment claim to HubPool on Ethereum
7. 2-hour challenge window (anyone can dispute invalid fills)
8. If unchallenged, relayer receives repayment + LP fees
SpokePool Intent Deposit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SpokePool {
struct DepositData {
address depositor;
address recipient; // Recipient on destination chain
address inputToken;
address outputToken;
uint256 inputAmount;
uint256 outputAmount; // Minimum output (intent constraint)
uint256 destinationChainId;
address exclusiveRelayer; // Optional: exclusive relayer for period
uint32 quoteTimestamp; // Timestamp used to price LP fee
uint32 fillDeadline;
uint32 exclusivityDeadline;
bytes message; // Arbitrary execution data on destination
}
event V3FundsDeposited(
address inputToken,
address outputToken,
uint256 inputAmount,
uint256 outputAmount,
uint256 indexed destinationChainId,
uint32 indexed depositId,
uint32 quoteTimestamp,
uint32 fillDeadline,
uint32 exclusivityDeadline,
address indexed depositor,
address recipient,
address exclusiveRelayer,
bytes message
);
event FilledV3Relay(
address inputToken,
address outputToken,
uint256 inputAmount,
uint256 outputAmount,
uint256 repaymentChainId,
uint256 indexed originChainId,
uint32 indexed depositId,
uint32 fillDeadline,
uint32 exclusivityDeadline,
address exclusiveRelayer,
address indexed relayer,
address depositor,
address recipient,
bytes message,
V3RelayExecutionEventInfo relayExecutionInfo
);
uint32 public numberOfDeposits;
/// @notice User deposits and expresses intent
function depositV3(
address depositor,
address recipient,
address inputToken,
address outputToken,
uint256 inputAmount,
uint256 outputAmount,
uint256 destinationChainId,
address exclusiveRelayer,
uint32 quoteTimestamp,
uint32 fillDeadline,
uint32 exclusivityDeadline,
bytes calldata message
) external payable {
uint32 depositId = numberOfDeposits++;
// Pull input tokens
if (inputToken == WETH && msg.value > 0) {
WETH.deposit{value: msg.value}();
} else {
IERC20(inputToken).safeTransferFrom(depositor, address(this), inputAmount);
}
emit V3FundsDeposited(
inputToken, outputToken, inputAmount, outputAmount,
destinationChainId, depositId, quoteTimestamp,
fillDeadline, exclusivityDeadline, depositor,
recipient, exclusiveRelayer, message
);
}
/// @notice Relayer fills intent on destination chain
function fillV3Relay(
DepositData calldata depositData,
uint256 repaymentChainId
) external {
bytes32 relayHash = _getRelayHash(depositData);
require(!fillStatus[relayHash], "Already filled");
require(block.timestamp <= depositData.fillDeadline, "Fill deadline passed");
// Check exclusivity
if (
depositData.exclusiveRelayer != address(0) &&
block.timestamp <= depositData.exclusivityDeadline &&
msg.sender != depositData.exclusiveRelayer
) {
revert("Exclusive relayer period active");
}
fillStatus[relayHash] = true;
// Relayer pays output tokens to recipient
IERC20(depositData.outputToken).safeTransferFrom(
msg.sender,
depositData.recipient,
depositData.outputAmount
);
// Execute arbitrary message if present
if (depositData.message.length > 0) {
AcrossMessageHandler(depositData.recipient).handleAcrossMessage(
depositData.outputToken,
depositData.outputAmount,
msg.sender,
depositData.message
);
}
emit FilledV3Relay(/*...*/);
}
}
Relayer Implementation (Off-Chain)
import { ethers } from "ethers";
class AcrossRelayer {
private originProvider: ethers.providers.Provider;
private destProvider: ethers.providers.Provider;
private signer: ethers.Signer;
constructor(config: RelayerConfig) {
this.originProvider = new ethers.providers.JsonRpcProvider(config.originRpc);
this.destProvider = new ethers.providers.JsonRpcProvider(config.destRpc);
this.signer = new ethers.Wallet(config.privateKey, this.destProvider);
}
async listen(): Promise<void> {
const originSpokePool = new ethers.Contract(
ORIGIN_SPOKE_POOL,
SpokePoolABI,
this.originProvider
);
originSpokePool.on("V3FundsDeposited", async (
inputToken, outputToken, inputAmount, outputAmount,
destinationChainId, depositId, quoteTimestamp,
fillDeadline, exclusivityDeadline, depositor,
recipient, exclusiveRelayer, message, event
) => {
if (destinationChainId.toNumber() !== DEST_CHAIN_ID) return;
const profitable = await this.isProfitable({
inputToken, outputToken, inputAmount, outputAmount,
fillDeadline: fillDeadline.toNumber(),
});
if (!profitable) return;
await this.fill({
depositor, recipient, inputToken, outputToken,
inputAmount, outputAmount, destinationChainId,
exclusiveRelayer, quoteTimestamp, fillDeadline,
exclusivityDeadline, message,
depositId: depositId.toNumber(),
});
});
}
async isProfitable(params: FillParams): Promise<boolean> {
// Calculate LP fee from HubPool
const lpFeeRate = await this.getLpFee(params.inputToken, params.quoteTimestamp);
// Output = inputAmount * (1 - lpFeeRate) - relayerFee
const expectedRepayment = params.inputAmount
.mul(1e18 - lpFeeRate).div(1e18);
// Profitable if repayment > outputAmount (what we pay)
return expectedRepayment.gt(params.outputAmount);
}
async fill(depositData: DepositData): Promise<void> {
const destSpokePool = new ethers.Contract(
DEST_SPOKE_POOL,
SpokePoolABI,
this.signer
);
// Approve output token
const outputToken = new ethers.Contract(depositData.outputToken, ERC20ABI, this.signer);
await outputToken.approve(DEST_SPOKE_POOL, depositData.outputAmount);
// Fill the relay
const tx = await destSpokePool.fillV3Relay(
depositData,
ORIGIN_CHAIN_ID // repayment chain
);
console.log(`Filled deposit ${depositData.depositId} in tx ${tx.hash}`);
}
}
8. Solver/Filler Architecture
Solver Components
┌─────────────────────────────────────────────────────────┐
│ SOLVER ARCHITECTURE │
├─────────────────┬───────────────────┬───────────────────┤
│ Order Intake │ Solving Engine │ Execution Layer │
│ │ │ │
│ - API polling │ - Price feeds │ - Tx construction │
│ - WebSocket │ - Route finding │ - Gas estimation │
│ - P2P orders │ - Simulation │ - MEV protection │
│ - Mempool watch │ - Profitability │ - Flashloans │
└─────────────────┴───────────────────┴───────────────────┘
Solver Implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @notice Solver contract — implements IReactorCallback for UniswapX
contract ExampleSolver is IReactorCallback {
ISwapRouter public immutable uniswapRouter;
address public immutable owner;
error OnlyOwner();
error OnlyReactor();
constructor(ISwapRouter _router) {
uniswapRouter = _router;
owner = msg.sender;
}
/// @notice Fill via direct token transfer (solver holds inventory)
function fillDirect(
IReactor reactor,
SignedOrder calldata order,
address outputToken,
uint256 outputAmount
) external {
if (msg.sender != owner) revert OnlyOwner();
IERC20(outputToken).approve(address(reactor), outputAmount);
reactor.execute(order);
}
/// @notice Fill via callback — solver gets input tokens, must provide output
function fillWithCallback(
IReactor reactor,
SignedOrder calldata order,
bytes calldata callbackData
) external {
if (msg.sender != owner) revert OnlyOwner();
…(truncated)