Skip to main content

What is an Intent?

An intent represents a user’s desired outcome on a destination chain. It contains:
  • Route: The calls to execute on the destination chain, including required tokens
  • Reward: The compensation offered to solvers who execute the intent
  • Destination: The target chain ID where execution should occur
Intents separate what you want done from how it gets done, allowing specialized solvers to compete to execute your transactions efficiently.

Intent Structure

Eco Routes uses three core data structures defined in contracts/types/Intent.sol:

Intent

Intent.sol
struct Intent {
    uint64 destination;  // Target chain ID
    Route route;         // Execution instructions
    Reward reward;       // Solver compensation
}

Route

Intent.sol
struct Route {
    bytes32 salt;              // Unique identifier
    uint64 deadline;           // Execution deadline
    address portal;            // Destination portal address
    uint256 nativeAmount;      // Native tokens for execution
    TokenAmount[] tokens;      // ERC20 tokens for execution
    Call[] calls;              // Contract calls to execute
}

Reward

Intent.sol
struct Reward {
    uint64 deadline;           // Claim deadline
    address creator;           // Intent creator
    address prover;            // Proof contract address
    uint256 nativeAmount;      // Native token reward
    TokenAmount[] tokens;      // ERC20 token rewards
}

Creating Your First Intent

Here’s a complete example of creating an intent for a cross-chain token transfer.
1

Define the Intent Parameters

First, define what you want to happen on the destination chain:
// Destination chain (e.g., Arbitrum)
uint64 destinationChainId = 42161;

// Unique salt to prevent duplicates
bytes32 salt = keccak256(abi.encodePacked(msg.sender, block.timestamp));

// Deadline: 1 hour from now
uint64 deadline = uint64(block.timestamp + 3600);
2

Create Token Transfer Calls

Define the calls to execute on the destination chain:
// Transfer 1000 USDC to recipient
Call[] memory calls = new Call[](1);
calls[0] = Call({
    target: usdcTokenAddress,
    data: abi.encodeWithSignature(
        "transfer(address,uint256)",
        recipientAddress,
        1000e6  // 1000 USDC (6 decimals)
    ),
    value: 0
});
3

Specify Required Tokens

Declare which tokens the solver must provide:
// Solver must provide 1000 USDC
TokenAmount[] memory routeTokens = new TokenAmount[](1);
routeTokens[0] = TokenAmount({
    token: usdcTokenAddress,
    amount: 1000e6
});
4

Define the Route

Combine the execution parameters:
Route memory route = Route({
    salt: salt,
    deadline: deadline,
    portal: destinationPortalAddress,
    nativeAmount: 0,
    tokens: routeTokens,
    calls: calls
});
5

Define the Reward

Specify compensation for the solver:
// Offer 1010 USDC on source chain (10 USDC profit for solver)
TokenAmount[] memory rewardTokens = new TokenAmount[](1);
rewardTokens[0] = TokenAmount({
    token: sourceUsdcAddress,
    amount: 1010e6  // 1010 USDC reward
});

Reward memory reward = Reward({
    deadline: deadline,
    creator: msg.sender,
    prover: proverAddress,
    nativeAmount: 0,
    tokens: rewardTokens
});
6

Create the Complete Intent

Assemble the full intent:
Intent memory intent = Intent({
    destination: destinationChainId,
    route: route,
    reward: reward
});

Publishing and Funding Intents

Once you’ve created an intent, you need to publish and fund it. The IntentSource contract provides several methods.

Function Signatures

From contracts/IntentSource.sol:
IntentSource.sol
/**
 * @notice Creates and funds an intent in a single transaction
 * @param intent The complete intent struct to be published and funded
 * @param allowPartial Whether to allow partial funding
 * @return intentHash Hash of the created and funded intent
 * @return vault Address of the created vault
 */
function publishAndFund(
    Intent calldata intent,
    bool allowPartial
) public payable returns (bytes32 intentHash, address vault);

/**
 * @notice Creates an intent without funding
 * @param intent The complete intent struct to be published
 * @return intentHash Hash of the created intent
 * @return vault Address of the created vault
 */
function publish(
    Intent calldata intent
) public returns (bytes32 intentHash, address vault);

/**
 * @notice Funds an existing intent
 * @param destination Destination chain ID for the intent
 * @param routeHash Hash of the route component
 * @param reward Reward structure containing distribution details
 * @param allowPartial Whether to allow partial funding
 * @return intentHash Hash of the funded intent
 */
function fund(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward,
    bool allowPartial
) external payable returns (bytes32 intentHash);

Publish and Fund in One Transaction

For intents with ERC20 token rewards:
// 1. Approve the IntentSource contract to spend your reward tokens
IERC20(sourceUsdcAddress).approve(address(intentSource), 1010e6);

// 2. Publish and fund the intent
(bytes32 intentHash, address vault) = intentSource.publishAndFund(
    intent,
    false  // Don't allow partial funding
);

// The intent is now live and solvers can see it

Separate Publish and Fund

You can also publish an intent first and fund it later:
// 1. Publish without funding
(bytes32 intentHash, address vault) = intentSource.publish(intent);

// Intent is now published but not funded
// Solvers will see it but won't execute until funded

// 2. Fund later (by anyone)
IERC20(sourceUsdcAddress).approve(address(intentSource), 1010e6);

bytes32 routeHash = keccak256(abi.encode(intent.route));
intentSource.fund(
    intent.destination,
    routeHash,
    intent.reward,
    false  // allowPartial
);
Anyone can fund an intent once it’s published. The reward tokens are held in a deterministic vault contract until the intent is fulfilled.

Real-World Examples

Example 1: Cross-Chain Swap

Swap USDC on Ethereum for USDT on Arbitrum:
// Swap 1000 USDC (Ethereum) -> 1000 USDT (Arbitrum)
Call[] memory calls = new Call[](1);
calls[0] = Call({
    target: usdtArbitrumAddress,
    data: abi.encodeWithSignature(
        "transfer(address,uint256)",
        msg.sender,
        1000e6
    ),
    value: 0
});

TokenAmount[] memory routeTokens = new TokenAmount[](1);
routeTokens[0] = TokenAmount(usdtArbitrumAddress, 1000e6);

TokenAmount[] memory rewardTokens = new TokenAmount[](1);
rewardTokens[0] = TokenAmount(usdcEthereumAddress, 1005e6);  // 5 USDC fee

Intent memory swapIntent = Intent({
    destination: 42161,  // Arbitrum
    route: Route({
        salt: keccak256(abi.encodePacked(msg.sender, block.timestamp)),
        deadline: uint64(block.timestamp + 1800),  // 30 minutes
        portal: arbitrumPortalAddress,
        nativeAmount: 0,
        tokens: routeTokens,
        calls: calls
    }),
    reward: Reward({
        deadline: uint64(block.timestamp + 1800),
        creator: msg.sender,
        prover: proverAddress,
        nativeAmount: 0,
        tokens: rewardTokens
    })
});

Example 2: Cross-Chain DeFi Interaction

Deposit funds into a lending protocol on another chain:
// Deposit 1 ETH into Aave on Optimism
Call[] memory calls = new Call[](2);

// Approve Aave pool
calls[0] = Call({
    target: wethOptimismAddress,
    data: abi.encodeWithSignature(
        "approve(address,uint256)",
        aavePoolAddress,
        1 ether
    ),
    value: 0
});

// Supply to Aave
calls[1] = Call({
    target: aavePoolAddress,
    data: abi.encodeWithSignature(
        "supply(address,uint256,address,uint16)",
        wethOptimismAddress,
        1 ether,
        msg.sender,
        0
    ),
    value: 0
});

TokenAmount[] memory routeTokens = new TokenAmount[](1);
routeTokens[0] = TokenAmount(wethOptimismAddress, 1 ether);

TokenAmount[] memory rewardTokens = new TokenAmount[](1);
rewardTokens[0] = TokenAmount(wethEthereumAddress, 1.005 ether);

Intent memory defiIntent = Intent({
    destination: 10,  // Optimism
    route: Route({
        salt: keccak256(abi.encodePacked(msg.sender, block.timestamp)),
        deadline: uint64(block.timestamp + 3600),
        portal: optimismPortalAddress,
        nativeAmount: 0,
        tokens: routeTokens,
        calls: calls
    }),
    reward: Reward({
        deadline: uint64(block.timestamp + 3600),
        creator: msg.sender,
        prover: proverAddress,
        nativeAmount: 0,
        tokens: rewardTokens
    })
});

Example 3: Native Token Transfer

Transfer ETH to another chain:
// Transfer 1 ETH to recipient on Base
Call[] memory calls = new Call[](1);
calls[0] = Call({
    target: recipientAddress,
    data: "",
    value: 1 ether
});

Intent memory ethTransferIntent = Intent({
    destination: 8453,  // Base
    route: Route({
        salt: keccak256(abi.encodePacked(msg.sender, block.timestamp)),
        deadline: uint64(block.timestamp + 1800),
        portal: basePortalAddress,
        nativeAmount: 1 ether,  // Solver must provide 1 ETH
        tokens: new TokenAmount[](0),
        calls: calls
    }),
    reward: Reward({
        deadline: uint64(block.timestamp + 1800),
        creator: msg.sender,
        prover: proverAddress,
        nativeAmount: 1.01 ether,  // 0.01 ETH fee on source chain
        tokens: new TokenAmount[](0)
    })
});

// Fund with ETH
intentSource.publishAndFund{value: 1.01 ether}(ethTransferIntent, false);

Checking Intent Status

You can check if an intent has been funded using contracts/IntentSource.sol:
IntentSource.sol
/**
 * @notice Checks if an intent is completely funded
 * @param intent Intent to validate
 * @return True if intent is completely funded, false otherwise
 */
function isIntentFunded(Intent calldata intent) public view returns (bool);

/**
 * @notice Retrieves reward status for a given intent hash
 * @param intentHash Hash of the intent to query
 * @return status Current status of the intent
 */
function getRewardStatus(bytes32 intentHash) public view returns (Status status);
Usage:
// Check if intent is funded
bool isFunded = intentSource.isIntentFunded(intent);

// Get detailed status
bytes32 intentHash = keccak256(
    abi.encodePacked(
        intent.destination,
        keccak256(abi.encode(intent.route)),
        keccak256(abi.encode(intent.reward))
    )
);

IIntentSource.Status status = intentSource.getRewardStatus(intentHash);
// Status can be: Initial, Funded, Withdrawn, or Refunded

Important Considerations

Deadline Management: The route deadline must be before or equal to the reward deadline. Solvers need time to claim rewards after execution.
Sufficient Rewards: Ensure your rewards are attractive enough to incentivize solvers. Consider gas costs on both chains and solver profit margins.
Deterministic Vaults: Reward tokens are stored in deterministic vault contracts created via CREATE2. The vault address is the same across all chains for the same intent hash.
Partial Funding: Set allowPartial = true if you want to fund the intent gradually. This is useful for large intents or when multiple parties contribute to the reward.

Next Steps

Fulfilling Intents

Learn how solvers execute intents on destination chains

Proving Intents

Understand how fulfillment is proven cross-chain

ERC-7683 Integration

Use the standardized ERC-7683 interface

Build docs developers (and LLMs) love