Intents Smart Contract¶
Use the SDK
Most integrations should use the SDK, which builds and submits orders to this contract for you. Use this page if you can't use the SDK, for example because you aren't using TypeScript, or if you want to control every step yourself.
Intents are submitted by calling open on the Intents contract (FullSettler) on the origin chain. The contract follows the ERC-7683 open(OnchainCrossChainOrder) interface.
open pulls the user's USDC into escrow and emits an Open event with the order ID. It does not call any TAO API, and it needs no API credentials. The Quoter API gives you the output amount to put in the order.
Addresses¶
| Chain | Chain ID | Intents contract | USDC |
|---|---|---|---|
| Ethereum | 1 |
0x2831Ea5524f171735aF8FEB2e331821bef176C8A |
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Subtensor EVM | 964 |
0x24834012973b6e70997a23C7aAB01481d46CB8E1 |
0x9001dbe4D68d36ab87923A2a9Dfb0c745fd25001 |
Tokens¶
| Chain | Token | Address | Decimals | Use |
|---|---|---|---|---|
| Ethereum | USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
6 | Input and output |
| Ethereum | ETH (native) | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE |
18 | Output |
| Subtensor EVM | USDC (on-chain symbol USDC.e) |
0x9001dbe4D68d36ab87923A2a9Dfb0c745fd25001 |
6 | Input and output |
| Subtensor EVM | wTAO | 0x134f59E8B8637FD70ae12f263492B1dc73A25D1e |
18 | Output |
| Subtensor EVM | TAO (native) | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE |
18 | Output |
The USDC on Subtensor EVM is a bridged token whose on-chain symbol is USDC.e, so wallets label it USDC.e. These docs call it USDC. The native-token address is a placeholder that the contract and the Quoter API use for ETH and TAO.
Use these addresses for inputToken and outputToken.
Both chains expose the same interface. Each contract is the origin contract for orders opened on its chain and the destination contract for orders filled on its chain.
The open function¶
function open(OnchainCrossChainOrder calldata order) external payable;
struct OnchainCrossChainOrder {
uint32 fillDeadline; // Unix time (seconds) by which the order must be filled
bytes32 orderDataType; // Must equal ONCHAIN_ORDER_DATA_TYPEHASH()
bytes orderData; // abi.encode(InputOrderData)
}
fillDeadlinemust be strictly after the current block time and at most 86,400 seconds (1 day) later. Use the value you sent to the Quoter API.orderDataTypemust equal the contract'sONCHAIN_ORDER_DATA_TYPEHASH(). Read it from the contract instead of hardcoding it.orderDatais the ABI-encodedInputOrderDatastruct described below.- Send no native value (
msg.valuemust be0). The input is USDC.
InputOrderData¶
struct InputOrderData {
address user;
address inputTokenToSwapFrom;
uint24 poolFee;
uint256 inputAmountToSwapFrom;
uint32 swapDeadline;
address inputToken;
uint256 inputAmount;
address outputToken;
uint256 outputAmount;
uint256 destinationChainId;
bytes32 recipient;
uint16 maxProtocolFeeBps;
bytes subtensorData;
}
For a USDC input, set the fields like this:
| Field | Value |
|---|---|
user |
The address calling open. It must equal msg.sender. Refunds go to this address. |
inputTokenToSwapFrom |
0x0000000000000000000000000000000000000000 |
poolFee |
0 |
inputAmountToSwapFrom |
0 |
swapDeadline |
0 |
inputToken |
USDC on the origin chain. |
inputAmount |
The USDC amount the user pays, in base units. The same value you sent to the Quoter API. |
outputToken |
Token to deliver on the destination chain. 0xEeee...EEeE for the native token. |
outputAmount |
The output amount from the quote, unchanged. |
destinationChainId |
The destination chain ID. It must be a supported destination for this contract. |
recipient |
The recipient as a bytes32: the 20-byte address left-padded with zeros. |
maxProtocolFeeBps |
The most the user accepts as a protocol fee, in basis points. Read protocolFeeBps() before quoting and use that value. 65535 means no limit. |
subtensorData |
0x for a transfer to an EVM address. Otherwise see subtensorData. |
The inputTokenToSwapFrom, poolFee, inputAmountToSwapFrom, and swapDeadline fields are for inputs that are swapped to USDC on open. They must be zero for a USDC input. Swapped inputs are not covered in this documentation. subtensorData is for deliveries to Substrate accounts and subnet tokens, and it must be empty for a transfer to an EVM address. See subtensorData.
Set inputTokenToSwapFrom to the zero address
Do not set inputTokenToSwapFrom to the USDC address. Any non-zero value selects the swap path and the call reverts with FullSettler__InvalidCall.
Encoding orderData¶
orderData is the whole InputOrderData struct encoded as one tuple, the same as Solidity's abi.encode(inputOrderData). Do not encode the 13 fields as separate values. That produces different bytes, and open reverts without any error data.
import { encodeAbiParameters, parseAbiParameters } from "viem";
const orderData = encodeAbiParameters(
parseAbiParameters(
"(address user, address inputTokenToSwapFrom, uint24 poolFee, uint256 inputAmountToSwapFrom, uint32 swapDeadline, address inputToken, uint256 inputAmount, address outputToken, uint256 outputAmount, uint256 destinationChainId, bytes32 recipient, uint16 maxProtocolFeeBps, bytes subtensorData) order",
),
[order], // order is an object with the fields above
);
import { ethers } from "ethers";
const orderData = ethers.AbiCoder.defaultAbiCoder().encode(
[
"tuple(address user, address inputTokenToSwapFrom, uint24 poolFee, uint256 inputAmountToSwapFrom, uint32 swapDeadline, address inputToken, uint256 inputAmount, address outputToken, uint256 outputAmount, uint256 destinationChainId, bytes32 recipient, uint16 maxProtocolFeeBps, bytes subtensorData)",
],
[[user, ethers.ZeroAddress, 0, 0n, 0, usdc, inputAmount, outputToken, outputAmount, destinationChainId, recipient, maxProtocolFeeBps, "0x"]],
);
Encoding the recipient¶
recipient must be a canonical left-padded address. A value with non-zero bytes in the upper 12 bytes reverts with FullSettler__NonCanonicalRecipient.
import { pad } from "viem";
const recipient = pad("0x1111111111111111111111111111111111111111", { size: 32 });
// 0x0000000000000000000000001111111111111111111111111111111111111111
subtensorData¶
subtensorData tells the contract that the output is not an ordinary transfer to an EVM address. It applies only when the destination is Subtensor EVM (964). On other destinations the contract does not decode it.
Empty bytes (0x) mean a normal payout to the EVM recipient. Otherwise subtensorData is one ABI-encoded SubtensorData struct:
enum SubtensorOperation { None, AlphaStake, TransferTao }
struct SubtensorData {
SubtensorOperation operationType; // encoded as uint8
uint32 netuid;
bytes32 delegatorHotkey;
bytes32 recipientColdkey;
uint256 amount;
}
The ABI tuple is tuple(uint8,uint32,bytes32,bytes32,uint256). All 32-byte keys are Substrate AccountId32 values: an SS58 address decoded to its 32 raw bytes.
operationType |
Kind | What the solver delivers |
|---|---|---|
0 |
None |
Nothing special. Treat as a normal EVM payout. |
1 |
AlphaStake |
Subnet alpha, staked for a Substrate coldkey. |
2 |
TransferTao |
Native TAO, credited to the free balance of a Substrate coldkey. |
Which fields each kind uses:
| Field | AlphaStake (1) |
TransferTao (2) |
|---|---|---|
netuid |
The subnet to stake into. | 0 |
delegatorHotkey |
The validator hotkey the stake goes to. | 0x00…00 |
recipientColdkey |
The coldkey that owns the stake. | The coldkey that receives the TAO. Required. |
amount |
The minimum alpha the stake must yield, in rao (9 decimals). | 0. outputAmount is the amount paid. |
For both kinds, set outputToken to the native-token address (0xEeee…EEeE) and outputAmount to the native TAO, in wei, that the solver delivers or stakes. Do not use wTAO. The payout destination comes from recipientColdkey, not from the order's recipient. The SDK sets recipient to the user's own EVM address, and you should do the same.
TransferTao¶
The solver pays native TAO, and the settler credits it to recipientColdkey's free balance on the Substrate side. amount is 0: the TAO paid is outputAmount.
import { encodeAbiParameters, zeroHash } from "viem";
const subtensorData = encodeAbiParameters(
[{ type: "tuple", components: [
{ name: "operationType", type: "uint8" },
{ name: "netuid", type: "uint32" },
{ name: "delegatorHotkey", type: "bytes32" },
{ name: "recipientColdkey", type: "bytes32" },
{ name: "amount", type: "uint256" },
] }],
[{ operationType: 2, netuid: 0, delegatorHotkey: zeroHash, recipientColdkey, amount: 0n }],
);
AlphaStake¶
Alpha is not an ERC-20 that a solver can transfer. The solver provides native TAO, and the settler stakes it into subnet netuid through the Subtensor staking precompile, to delegatorHotkey, and moves the stake to recipientColdkey. The fill reverts if the stake would yield less than amount alpha, so amount is the user's guaranteed minimum. It is not derived from outputAmount.
Encode it the same way with operationType: 1, the subnet, the hotkey, the coldkey, and the minimum alpha as amount:
[{ operationType: 1, netuid, delegatorHotkey, recipientColdkey, amount: minAlpha }]
netuid 0 is root stake.
Get the data from the Quoter API¶
Don't compute amount yourself. For these outputs the quote carries a ready-made subtensorData, with the alpha minimum already filled in. Request the quote with the matching Substrate output, and place the returned value in the order unchanged. Before signing, decode it and check that the operation, subnet, validator, and coldkey match what the user asked for. The SDK does this check for you.
Checks at open¶
For a Subtensor destination, open validates subtensorData:
| Check | AlphaStake |
TransferTao |
|---|---|---|
outputToken is the native sentinel or wTAO (the wrapped native token on Subtensor EVM) |
Yes | Yes |
recipientColdkey is non-zero |
Not checked | Required |
| Encoded length is at least 160 bytes | Not checked | Required |
operationType is at most 2 |
Yes | Yes |
An invalid value reverts with FullSettler__InvalidCall. open accepts some orders that can never be filled, and they are refunded after fillDeadline:
- Don't use wTAO (
0x134f59E8B8637FD70ae12f263492B1dc73A25D1e) asoutputTokenfor these kinds.openaccepts it, but a fill requires the native sentinel.
Always set recipientColdkey for AlphaStake
open does not check recipientColdkey for AlphaStake. A zero coldkey is accepted, and the fill then fails or the stake goes to the wrong owner. Set the coldkey before signing.
Attribution¶
Partners can have orders attributed to them with an ERC-8021 (schema 0) suffix on the open transaction's calldata. Append these bytes after the ABI-encoded open call. The contract ignores the extra trailing bytes, so the order is unaffected. The SDK does this for you when you pass partnerId.
calldata = abi.encodeCall(open, (order)) ‖ codes ‖ codesLength ‖ 0x00 ‖ 0x80218021802180218021802180218021
| Part | Size | Value |
|---|---|---|
codes |
1 to 100 bytes | Your partner ID as ASCII. Printable characters only, no spaces or commas. |
codesLength |
1 byte | The length of codes in bytes. |
| schema ID | 1 byte | 0x00 |
| marker | 16 bytes | 0x80218021802180218021802180218021 |
For example, my-app becomes 0x6d792d617070 06 00 80218021802180218021802180218021.
Use your partner ID, and add the suffix to the transaction you send and to any simulation of it.
Before you call open¶
The user must approve the Intents contract to spend their USDC:
IERC20(usdc).approve(intentsContract, inputAmount);
Approve exactly inputAmount rather than an unlimited amount.
Fees and amounts¶
open pulls exactly inputAmount USDC from the user. The protocol fee (protocolFeeBps(), currently 30 basis points) is deducted from that amount, and the rest stays in escrow for the solver. The user never pays more than inputAmount.
For example, with a 30 basis point fee, an inputAmount of 100000000 (100 USDC) pulls 100 USDC, keeps 0.30 USDC as the fee, and escrows 99.70 USDC.
If protocolFeeBps() is higher than maxProtocolFeeBps when open runs, the call reverts with FullSettler__ProtocolFeeExceedsMax.
Events and order status¶
Open¶
Emitted on the origin chain when an order is opened.
event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder);
orderId is the identifier for the order on both chains. Read it from the transaction receipt and store it with the transaction hash.
Fill¶
A solver fills the order by calling fill on the Intents contract on the destination chain. To check whether an order has been filled, read fillRecords(orderId) on the destination contract. It returns a non-zero value once the order is filled.
function fillRecords(bytes32 orderId) external view returns (bytes32);
The destination contract also emits IntentFilled(address indexed solver, OrderData order) when it fills an order.
Refund¶
If the order is not filled before fillDeadline, the protocol refunds the escrowed USDC to user on the origin chain. The refund includes the protocol fee, so the user gets back the full inputAmount. The origin contract emits:
event Refunded(bytes32 indexed orderId, address indexed user, uint256 amount);
Refunds are sent by the protocol after the deadline passes. You do not need to call anything, and users cannot trigger a refund themselves.
Order status¶
orderStatus(orderId) on the origin contract returns:
| Value | Meaning |
|---|---|
0 |
Unknown order. |
1 |
Deposited. The USDC is escrowed. A filled order also stays in this state on the origin chain, so use fillRecords on the destination to detect a fill. |
2 |
Not used. |
3 |
Refunded. |
To decide what to show the user:
| Check | Result |
|---|---|
fillRecords(orderId) on the destination is non-zero |
Filled. The output was delivered. |
A Refunded event exists for orderId on the origin |
Refunded. |
Neither, and fillDeadline has not passed |
Waiting for a solver. |
Neither, and fillDeadline has passed |
Waiting for the refund. |
Dry run with resolve¶
resolve(order) is a view function you can use to check an order before asking the user to sign. It runs the same order-shape validation as open (deadline, token, fee cap, recipient) without moving any funds, but it skips two checks that only make sense with a real caller and a live contract:
- It does not check
user == msg.sender.resolvehas no caller context to check against, so it accepts anyuser. - It does not check whether the contract is paused.
openreverts withEnforcedPausewhile paused;resolvedoes not.
So a successful resolve does not guarantee open will succeed: it can still revert on the caller check or on the pause state. It also does not check the caller's allowance or USDC balance, which open needs to pull the input.
function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory);
The orderId it returns does not match the one open will emit, because open adds a per-order nonce. Use resolve for validation only.
Reverts¶
The contract uses custom errors, so a failed call returns a 4-byte selector instead of a message. Include the error definitions from the Minimal ABI so your library can show the name. Otherwise, match the selector in the table.
| Error | Selector | Cause |
|---|---|---|
EnforcedPause |
0xd93c0665 |
The contract is paused. Try again later. |
FullSettler__InvalidOrderDataType |
0x226b9fe3 |
orderDataType does not match ONCHAIN_ORDER_DATA_TYPEHASH(). |
FullSettler__InvalidOrderUser |
0xbb9cbd62 |
user is not the caller. |
FullSettler__InvalidInputToken |
0xb656cc64 |
inputToken is not the chain's USDC. |
FullSettler__InvalidAmount |
0xf4111478 |
inputAmount is zero. |
FullSettler__TimestampPassed |
0xe30a999a |
fillDeadline is not in the future. |
FullSettler__FillDeadlineTooFar |
0xe3399579 |
fillDeadline is more than 1 day in the future. |
FullSettler__DestinationSettlerNotSet |
0x22fa3d33 |
destinationChainId is not a supported destination. This includes the origin chain itself. |
FullSettler__NonCanonicalRecipient |
0x5a5d77fa |
recipient is not a left-padded 20-byte address. |
FullSettler__ProtocolFeeExceedsMax |
0x6a52296e |
The live protocol fee is higher than maxProtocolFeeBps. |
FullSettler__InvalidCall |
0x43eebf0d |
inputTokenToSwapFrom or inputAmountToSwapFrom is non-zero for a USDC input, msg.value is not zero, or (destination is Subtensor EVM only) subtensorData is not empty and malformed. For the flows in this guide, pass 0x. For Substrate deliveries, see subtensorData. |
If the USDC transfer fails because the allowance or balance is too low, open reverts with the USDC token's own error.
Minimal ABI¶
Enough to call open, read the state used in this guide, decode the Open event, and decode reverts. This block uses viem's human-readable syntax. For ethers, see the note below it.
import { parseAbi } from "viem";
export const intentsAbi = parseAbi([
"function open((uint32 fillDeadline, bytes32 orderDataType, bytes orderData) order) payable",
"function resolve((uint32 fillDeadline, bytes32 orderDataType, bytes orderData) order) view returns ((address user, uint256 originChainId, uint32 openDeadline, uint32 fillDeadline, bytes32 orderId, (bytes32 token, uint256 amount, bytes32 recipient, uint256 chainId)[] maxSpent, (bytes32 token, uint256 amount, bytes32 recipient, uint256 chainId)[] minReceived, (uint256 destinationChainId, bytes32 destinationSettler, bytes originData)[] fillInstructions))",
"function ONCHAIN_ORDER_DATA_TYPEHASH() view returns (bytes32)",
"function protocolFeeBps() view returns (uint16)",
"function paused() view returns (bool)",
"function orderStatus(bytes32 orderId) view returns (uint8)",
"function fillRecords(bytes32 orderId) view returns (bytes32)",
"struct Output { bytes32 token; uint256 amount; bytes32 recipient; uint256 chainId; }",
"struct FillInstruction { uint256 destinationChainId; bytes32 destinationSettler; bytes originData; }",
"struct ResolvedCrossChainOrder { address user; uint256 originChainId; uint32 openDeadline; uint32 fillDeadline; bytes32 orderId; Output[] maxSpent; Output[] minReceived; FillInstruction[] fillInstructions; }",
"event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder)",
"event Refunded(bytes32 indexed orderId, address indexed user, uint256 amount)",
"error EnforcedPause()",
"error FullSettler__InvalidOrderDataType()",
"error FullSettler__InvalidOrderUser()",
"error FullSettler__InvalidInputToken()",
"error FullSettler__InvalidAmount()",
"error FullSettler__TimestampPassed()",
"error FullSettler__FillDeadlineTooFar()",
"error FullSettler__DestinationSettlerNotSet()",
"error FullSettler__NonCanonicalRecipient()",
"error FullSettler__ProtocolFeeExceedsMax()",
"error FullSettler__InvalidCall()",
]);
ethers does not accept struct declarations. Declare the Open event with an inline tuple instead:
"event Open(bytes32 indexed orderId, (address user, uint256 originChainId, uint32 openDeadline, uint32 fillDeadline, bytes32 orderId, (bytes32 token, uint256 amount, bytes32 recipient, uint256 chainId)[] maxSpent, (bytes32 token, uint256 amount, bytes32 recipient, uint256 chainId)[] minReceived, (uint256 destinationChainId, bytes32 destinationSettler, bytes originData)[] fillInstructions) resolvedOrder)"
Next¶
See the Integration Guide for a complete example that gets a quote, opens the order, and waits for the fill.