Skip to main content

What is Intent Fulfillment?

Intent fulfillment is the process by which solvers execute user intents on destination chains. Solvers:
  1. Monitor published intents across chains
  2. Evaluate profitability based on rewards vs. execution costs
  3. Provide required tokens/assets on the destination chain
  4. Execute the intent’s calls on the destination chain
  5. Prove fulfillment to claim rewards on the source chain

Fulfillment Contract

The Inbox contract (contracts/Inbox.sol) handles intent fulfillment on destination chains.

Key Functions

Inbox.sol
/**
 * @notice Fulfills an intent to be proven via storage proofs
 * @dev Validates intent hash, executes calls, and marks as fulfilled
 * @param intentHash The hash of the intent to fulfill
 * @param route The route of the intent
 * @param rewardHash The hash of the reward details
 * @param claimant Cross-VM compatible claimant identifier
 * @return Array of execution results from each call
 */
function fulfill(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant
) external payable returns (bytes[] memory);

/**
 * @notice Fulfills an intent and initiates proving in one transaction
 * @dev Executes intent actions and sends proof message to source chain
 * @param intentHash The hash of the intent to fulfill
 * @param route The route of the intent
 * @param rewardHash The hash of the reward details
 * @param claimant Cross-VM compatible claimant identifier
 * @param prover Address of prover on the destination chain
 * @param sourceChainDomainID Domain ID of the source chain where the intent was created
 * @param data Additional data for message formatting
 * @return Array of execution results
 */
function fulfillAndProve(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant,
    address prover,
    uint64 sourceChainDomainID,
    bytes memory data
) public payable returns (bytes[] memory);

Fulfillment Process

Here’s a complete guide to fulfilling intents as a solver.
1

Monitor Published Intents

Listen for IntentPublished events on source chains:
// Listen to IntentSource contract events
event IntentPublished(
    bytes32 indexed intentHash,
    uint64 destination,
    bytes route,
    address creator,
    address prover,
    uint64 deadline,
    uint256 nativeAmount,
    TokenAmount[] tokens
);
Filter for intents on chains you support:
const filter = intentSource.filters.IntentPublished(
    null,  // any intentHash
    destinationChainId,  // your supported chain
);

intentSource.on(filter, (intentHash, destination, route, creator, prover, deadline, nativeAmount, tokens) => {
    // Evaluate if profitable
    evaluateIntent(intentHash, route, tokens);
});
2

Evaluate Profitability

Calculate if the intent is worth executing:
async function evaluateIntent(intentHash, routeData, rewardTokens) {
    // Decode route
    const route = decodeRoute(routeData);

    // Calculate costs
    const executionGas = estimateGas(route.calls);
    const tokenCosts = calculateTokenCosts(route.tokens);
    const gasCost = executionGas * gasPrice;

    // Calculate rewards
    const rewardValue = calculateRewardValue(rewardTokens);

    // Check profitability (including profit margin)
    const minProfit = 0.05; // 5% minimum profit
    if (rewardValue > (tokenCosts + gasCost) * (1 + minProfit)) {
        await fulfillIntent(intentHash, route, rewardHash);
    }
}
3

Verify Intent is Funded

Check that the reward vault has sufficient funds:
// On the source chain
bool isFunded = intentSource.isIntentFunded(intent);
require(isFunded, "Intent not funded");

// Or check vault balance directly
address vaultAddress = intentSource.intentVaultAddress(intent);
uint256 balance = IERC20(rewardToken).balanceOf(vaultAddress);
4

Approve Required Tokens

The Portal needs approval to pull tokens for execution:
// Approve Portal to spend your tokens
for (uint256 i = 0; i < route.tokens.length; i++) {
    IERC20(route.tokens[i].token).approve(
        route.portal,
        route.tokens[i].amount
    );
}
5

Fulfill the Intent

Execute the intent on the destination chain:
// Calculate hashes
bytes32 routeHash = keccak256(abi.encode(route));
bytes32 intentHash = keccak256(
    abi.encodePacked(destinationChainId, routeHash, rewardHash)
);

// Your address as claimant (for EVM)
bytes32 claimant = bytes32(uint256(uint160(msg.sender)));

// Fulfill the intent
bytes[] memory results = portal.fulfill{value: route.nativeAmount}(
    intentHash,
    route,
    rewardHash,
    claimant
);

Complete Fulfillment Example

Here’s a complete Solidity example of fulfilling a token transfer intent:
pragma solidity ^0.8.26;

import {IInbox} from "./interfaces/IInbox.sol";
import {Route, TokenAmount, Call} from "./types/Intent.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Solver {
    IInbox public portal;

    constructor(address _portal) {
        portal = IInbox(_portal);
    }

    /**
     * @notice Fulfill a cross-chain token transfer intent
     * @param intentHash Hash of the intent
     * @param route Route details
     * @param rewardHash Hash of reward details
     */
    function fulfillTokenTransfer(
        bytes32 intentHash,
        Route calldata route,
        bytes32 rewardHash
    ) external {
        // 1. Verify route is for this chain
        require(route.portal == address(portal), "Wrong portal");

        // 2. Verify not expired
        require(block.timestamp <= route.deadline, "Intent expired");

        // 3. Approve tokens for Portal to pull
        for (uint256 i = 0; i < route.tokens.length; i++) {
            IERC20(route.tokens[i].token).approve(
                address(portal),
                route.tokens[i].amount
            );
        }

        // 4. Fulfill intent
        bytes32 claimant = bytes32(uint256(uint160(address(this))));

        portal.fulfill{value: route.nativeAmount}(
            intentHash,
            route,
            rewardHash,
            claimant
        );

        // Intent is now fulfilled!
        // Next step: prove fulfillment to claim rewards
    }
}

Fulfill and Prove in One Transaction

For faster reward claiming, use fulfillAndProve to combine fulfillment and proof initiation:
// Calculate domain ID for the bridge
// WARNING: Domain ID != Chain ID for some bridges!
// Check your bridge's documentation
uint64 sourceChainDomainID = getHyperlaneDomainId(sourceChainId);

// Prover-specific data (e.g., bridge fees)
bytes memory proverData = abi.encode(
    bridgeFeeAmount,
    otherProverParams
);

// Fulfill and prove in one call
bytes[] memory results = portal.fulfillAndProve{value: totalValue}(
    intentHash,
    route,
    rewardHash,
    claimant,
    proverAddress,        // Prover contract on destination chain
    sourceChainDomainID,  // Bridge domain ID for source chain
    proverData            // Bridge-specific data
);
Domain ID vs Chain ID: The sourceChainDomainID parameter is NOT necessarily the same as the chain ID. Different bridge protocols use different domain ID systems:
  • Hyperlane: Custom domain IDs (e.g., Ethereum mainnet = 1, but Polygon = 137)
  • LayerZero: Uses endpoint IDs that differ from chain IDs
  • Polymer: Uses chain IDs directly
Always consult your bridge provider’s documentation for the correct domain ID mapping.

Real-World Examples

Example 1: Simple Token Transfer

// Intent: Transfer 1000 USDC to user on Arbitrum
// Solver has USDC on Arbitrum and wants to earn rewards on Ethereum

function fulfillUSDCTransfer(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash
) external {
    // Verify this is a token transfer to the expected recipient
    require(route.calls.length == 1, "Expected single call");

    // Approve USDC for portal
    IERC20 usdc = IERC20(route.tokens[0].token);
    usdc.approve(address(portal), route.tokens[0].amount);

    // Fulfill
    bytes32 claimant = bytes32(uint256(uint160(msg.sender)));
    portal.fulfill(intentHash, route, rewardHash, claimant);

    // The portal will:
    // 1. Pull 1000 USDC from solver
    // 2. Execute the transfer to recipient
    // 3. Mark intent as fulfilled with our address as claimant
}

Example 2: DeFi Interaction

// Intent: Deposit into Aave on Optimism
// Solver executes multi-call intent

function fulfillAaveDeposit(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash,
    address hyperProver,
    uint64 ethereumDomainId
) external payable {
    // Verify calls are for Aave
    require(route.calls.length == 2, "Expected approve + supply");

    // Approve WETH for portal to pull
    IERC20 weth = IERC20(route.tokens[0].token);
    weth.approve(address(portal), route.tokens[0].amount);

    // Fulfill and prove in one tx
    bytes memory proverData = "";
    bytes32 claimant = bytes32(uint256(uint160(address(this))));

    portal.fulfillAndProve{value: msg.value}(
        intentHash,
        route,
        rewardHash,
        claimant,
        hyperProver,
        ethereumDomainId,
        proverData
    );

    // Portal will:
    // 1. Pull WETH from solver
    // 2. Approve Aave pool
    // 3. Supply to Aave on behalf of user
    // 4. Send proof to Ethereum via Hyperlane
}

Example 3: Cross-Chain Swap

// Intent: Swap USDC (Ethereum) for USDT (Arbitrum)
// Solver provides USDT on Arbitrum, claims USDC on Ethereum

function fulfillCrossChainSwap(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash
) external {
    // Approve USDT for portal
    IERC20 usdt = IERC20(route.tokens[0].token);
    usdt.approve(address(portal), route.tokens[0].amount);

    // Fulfill
    bytes32 claimant = bytes32(uint256(uint160(address(this))));
    portal.fulfill(intentHash, route, rewardHash, claimant);

    // Solver provides: 1000 USDT on Arbitrum
    // Solver receives: 1005 USDC on Ethereum (5 USDC profit)
}

Handling Fulfillment Results

The fulfill function returns execution results for each call:
bytes[] memory results = portal.fulfill(
    intentHash,
    route,
    rewardHash,
    claimant
);

// Check results
for (uint256 i = 0; i < results.length; i++) {
    // Decode based on expected return type
    if (route.calls[i].target == usdcAddress) {
        bool success = abi.decode(results[i], (bool));
        require(success, "Transfer failed");
    }
}

Claimant Identifiers

The claimant parameter is a cross-VM compatible identifier:
For EVM chains, convert your address to bytes32:
// Your EVM address as claimant
bytes32 claimant = bytes32(uint256(uint160(msg.sender)));

// Or for a specific address
bytes32 claimant = bytes32(uint256(uint160(solverAddress)));

Intent Validation

The Portal validates intents before execution:
Inbox.sol
// From _fulfill function
if (block.timestamp > route.deadline) {
    revert IntentExpired();
}

if (route.portal != address(this)) {
    revert InvalidPortal(route.portal);
}

if (computedIntentHash != intentHash) {
    revert InvalidHash(intentHash);
}

if (claimants[intentHash] != bytes32(0)) {
    revert IntentAlreadyFulfilled(intentHash);
}

if (claimant == bytes32(0)) {
    revert ZeroClaimant();
}
This ensures:
  • Intent hasn’t expired
  • Intent is for the correct portal
  • Intent hash is valid
  • Intent hasn’t been fulfilled already
  • Claimant is valid

Gas Optimization Tips

Batch Processing: Monitor multiple intents and fulfill them in batches using multicall patterns to save gas.
Token Approval: Use approve(type(uint256).max) for tokens you frequently use to avoid repeated approval transactions.
Fulfill and Prove: Use fulfillAndProve instead of separate fulfill and prove calls to save one transaction.

Error Handling

Common errors and their solutions:
// Intent expired
if (block.timestamp > route.deadline) {
    // Skip this intent, monitor for new ones
}

// Insufficient token balance
try IERC20(token).balanceOf(address(this)) returns (uint256 balance) {
    if (balance < requiredAmount) {
        // Rebalance or skip
    }
} catch {
    // Token doesn't exist or error
}

// Intent already fulfilled
try portal.fulfill(...) {
    // Success
} catch (bytes memory reason) {
    if (bytes4(reason) == IInbox.IntentAlreadyFulfilled.selector) {
        // Someone else fulfilled it first
    }
}

Next Steps

After fulfilling an intent, you need to prove the fulfillment to claim rewards:

Proving Intents

Learn how to prove fulfillment and claim your rewards

Creating Intents

Understand the user perspective of creating intents

ERC-7683 Integration

Use the standardized interface for fulfillment

Build docs developers (and LLMs) love