Skip to main content
The Portal is the main entry point for the Eco Routes, combining intent publishing, fulfillment, reward claiming, and ERC-7683 compatibility in a unified contract. It inherits from both IntentSource (source chain operations) and Inbox (destination chain operations).

Architecture

Portal
├── IntentSource (intent publishing and management)
│   └── OriginSettler (ERC-7683 origin functionality)
└── Inbox (fulfillment and proving)
    └── DestinationSettler (ERC-7683 destination functionality)
The Portal does not hold funds between transactions. All rewards are escrowed in deterministically-deployed vault contracts.

Intent Lifecycle

1. Publishing (Source Chain)

Users create intents specifying execution instructions for the destination chain and rewards for solvers. Direct Publishing:
function publish(Intent calldata intent) 
    public returns (bytes32 intentHash, address vault)
Creates an intent without funding. Emits IntentPublished event. Atomic Publishing with Funding:
function publishAndFund(Intent calldata intent, bool allowPartial) 
    public payable returns (bytes32 intentHash, address vault)
Creates and funds an intent in a single transaction. Universal Format Publishing:
function publish(
    uint64 destination,
    bytes memory route,
    Reward memory reward
) public returns (bytes32 intentHash, address vault)
Cross-VM compatible publishing using bytes format for route data.

2. Funding (Source Chain)

Intents can be funded separately after creation:
function fund(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward,
    bool allowPartial
) external payable returns (bytes32 intentHash)
Funding for Others:
function fundFor(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward,
    bool allowPartial,
    address funder,
    address permitContract
) external payable returns (bytes32 intentHash)
Uses permit contracts for gasless token approvals. Cannot be used for intents with native token rewards.

3. Fulfillment (Destination Chain)

Solvers execute intent instructions on the destination chain:
function fulfill(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant
) external payable returns (bytes[] memory)
Parameters:
  • intentHash: Unique identifier of the intent
  • route: Execution instructions and token requirements
  • rewardHash: Hash of reward structure for verification
  • claimant: Cross-VM compatible identifier for reward recipient (bytes32)
Atomic Fulfillment with Proving:
function fulfillAndProve(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant,
    address prover,
    uint64 sourceChainDomainID,
    bytes memory data
) public payable returns (bytes[] memory)
Executes intent and initiates cross-chain proof in one transaction. ⚠️ Important: sourceChainDomainID is NOT the chain ID. Each bridge uses its own domain ID system:
  • Hyperlane: Custom domain IDs
  • LayerZero: Endpoint IDs
  • Metalayer: Domain IDs specific to routing
Consult the bridge provider’s documentation for correct domain IDs.

4. Proving (Destination Chain)

Generate proofs for fulfilled intents:
function prove(
    address prover,
    uint64 sourceChainDomainID,
    bytes32[] memory intentHashes,
    bytes memory data
) public payable
Sends cross-chain message to source chain verifying intent execution. Can batch multiple intents for gas efficiency.

5. Claiming Rewards (Source Chain)

After proof verification, solvers claim rewards:
function withdraw(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward
) public
Transfers rewards from vault to the claimant address specified during fulfillment. Batch Withdrawal:
function batchWithdraw(
    uint64[] calldata destinations,
    bytes32[] calldata routeHashes,
    Reward[] calldata rewards
) external
Efficiently claim multiple rewards in one transaction.

6. Refunds (Source Chain)

Creators can reclaim rewards after expiry:
function refund(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward
) external
Only succeeds if:
  • Intent has expired (block.timestamp >= reward.deadline)
  • Intent was not fulfilled on the correct destination chain

ERC-7683 Compatibility

The Portal implements both ERC-7683 origin and destination settler interfaces.

Origin Functions

Direct Order Opening:
function open(OnchainCrossChainOrder calldata order) external payable
Creates and funds an intent from an on-chain order structure. Gasless Order Opening:
function openFor(
    GaslessCrossChainOrder calldata order,
    bytes calldata signature,
    bytes calldata /* originFillerData */
) external payable
Creates an intent using EIP-712 signature. Validates:
  • Signature matches order.user
  • openDeadline not passed
  • originSettler matches contract address
  • originChainId matches current chain
Order Resolution:
function resolve(OnchainCrossChainOrder calldata order) 
    public view returns (ResolvedCrossChainOrder memory)

function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) 
    public view returns (ResolvedCrossChainOrder memory)
Converts Eco orders into ERC-7683 standard format.

Destination Functions

ERC-7683 Fulfillment:
function fill(
    bytes32 orderId,
    bytes calldata originData,
    bytes calldata fillerData
) external payable
Parameters:
  • orderId: Intent hash from origin chain
  • originData: Encoded (bytes route, bytes32 rewardHash)
  • fillerData: Encoded (address prover, uint64 source, bytes32 claimant, bytes proverData)
Decodes data and calls fulfillAndProve() internally.

Vault System

The Portal uses deterministic CREATE2 deployment for intent vaults.

Computing Vault Address

function intentVaultAddress(Intent calldata intent) 
    public view returns (address)

function intentVaultAddress(
    uint64 destination,
    bytes memory route,
    Reward calldata reward
) public view returns (address)
Returns the deterministic vault address without deployment.

Vault States

Vaults track intent lifecycle through status enum:
  • Initial: Intent created but not funded
  • Funded: Fully funded and ready for fulfillment
  • Withdrawn: Rewards claimed by solver
  • Refunded: Rewards returned to creator
function getRewardStatus(bytes32 intentHash) 
    public view returns (Status status)

Funding Validation

function isIntentFunded(Intent calldata intent) 
    public view returns (bool)

function isIntentFunded(
    uint64 destination,
    bytes memory route,
    Reward calldata reward
) public view returns (bool)
Checks if vault contains sufficient tokens and native currency.

Token Recovery

Recover tokens mistakenly sent to vaults:
function recoverToken(
    uint64 destination,
    bytes32 routeHash,
    Reward calldata reward,
    address token
) external
Restrictions:
  • Cannot recover zero address
  • Cannot recover any token in reward.tokens
  • Intent must have zero native rewards OR already be claimed/refunded

Intent Hashing

The Portal uses a deterministic hashing scheme:
function getIntentHash(Intent memory intent)
    public pure returns (
        bytes32 intentHash,
        bytes32 routeHash,
        bytes32 rewardHash
    )
Formula:
routeHash = keccak256(abi.encode(route))
rewardHash = keccak256(abi.encode(reward))
intentHash = keccak256(abi.encodePacked(destination, routeHash, rewardHash))
This allows verification on destination chains without transmitting full intent data.

Execution Model

Source Chain Execution

The Portal transfers tokens from users to vaults:
  1. Native tokens via payable function calls
  2. ERC20 tokens via SafeERC20.safeTransferFrom
  3. Excess ETH refunded automatically

Destination Chain Execution

The Portal delegates call execution to an Executor contract:
  1. Transfers tokens from solver to Executor
  2. Executor performs calls with delegated tokens
  3. Executor has no persistent state between transactions
This isolation ensures:
  • Portal storage is protected during arbitrary calls
  • Failed calls don’t corrupt Portal state
  • Executor can be upgraded independently

Security Features

Replay Protection

  • Intent hashes are unique (include salt in route)
  • Fulfilled intents cannot be fulfilled again
  • Withdrawn/refunded intents cannot be withdrawn/refunded again
  • ERC-7683 gasless orders use nonces

Validation

Publishing:
  • Cannot republish withdrawn/refunded intents
  • Validates reward structure consistency
Fulfillment:
  • Verifies intent hash matches route and reward
  • Checks portal address in route matches contract
  • Validates deadline hasn’t passed
  • Prevents zero claimant address
  • Requires minimum native token amount
Withdrawal:
  • Validates intent is proven on correct destination
  • Challenges proofs for incorrect destinations
  • Prevents withdrawal before proof
Refund:
  • Only after deadline expiry
  • Only if not proven on correct destination
  • Prevents refund of claimed intents

Reentrancy Protection

All external calls are made through:
  • SafeERC20 for token transfers
  • Isolated Executor for user-specified calls
  • State updates before external interactions

Cross-VM Compatibility

The Portal supports both EVM and non-EVM chains:

Address Conversion

import {AddressConverter} from "./libs/AddressConverter.sol";

// EVM address to bytes32
bytes32 universalId = address.toBytes32();

// bytes32 to EVM address
address evmAddress = bytes32Id.toAddress();

Claimant Identifiers

On destination chains, claimants are stored as bytes32:
mapping(bytes32 => bytes32) public claimants;
This supports:
  • EVM addresses (20 bytes, left-padded)
  • Solana addresses (32 bytes)
  • Other blockchain address formats

Events

IntentPublished

event IntentPublished(
    bytes32 indexed hash,
    uint64 destination,
    bytes route,
    address indexed creator,
    address indexed prover,
    uint256 deadline,
    uint256 nativeAmount,
    TokenAmount[] tokens
);

IntentFunded

event IntentFunded(
    bytes32 indexed intentHash,
    address indexed funder,
    bool complete
);

IntentFulfilled

event IntentFulfilled(
    bytes32 indexed intentHash,
    bytes32 claimant
);

IntentProven

event IntentProven(
    bytes32 indexed intentHash,
    bytes32 claimant
);

IntentWithdrawn

event IntentWithdrawn(
    bytes32 indexed hash,
    address indexed recipient
);

IntentRefunded

event IntentRefunded(
    bytes32 indexed hash,
    address indexed recipient
);

IntentTokenRecovered

event IntentTokenRecovered(
    bytes32 indexed intentHash,
    address indexed recipient,
    address indexed token
);

Open (ERC-7683)

event Open(
    bytes32 indexed orderId,
    ResolvedCrossChainOrder resolvedOrder
);

OrderFilled (ERC-7683)

event OrderFilled(
    bytes32 indexed orderId,
    address indexed filler
);

Error Handling

  • ArrayLengthMismatch(): Array parameters have mismatched lengths
  • ChainIdTooLarge(uint256): Chain ID exceeds uint64 maximum
  • InsufficientFunds(bytes32): Incomplete funding when partial not allowed
  • InsufficientNativeAmount(uint256, uint256): Insufficient native tokens for execution
  • IntentAlreadyExists(bytes32): Cannot republish existing intent
  • IntentAlreadyFulfilled(bytes32): Intent already fulfilled
  • IntentExpired(): Past route deadline
  • IntentNotClaimed(bytes32): Cannot refund claimed intent
  • IntentNotFulfilled(bytes32): Intent not in fulfilled state
  • InvalidClaimant(): Zero address claimant
  • InvalidHash(bytes32): Computed hash doesn’t match provided hash
  • InvalidOriginChainId(uint256, uint256): Chain ID mismatch in ERC-7683 order
  • InvalidOriginSettler(address, address): Settler address mismatch in ERC-7683 order
  • InvalidPortal(address): Route portal doesn’t match contract
  • InvalidRecoverToken(address): Cannot recover specified token
  • InvalidSignature(): EIP-712 signature verification failed
  • InvalidStatusForFunding(Status): Cannot fund withdrawn/refunded intent
  • InvalidStatusForRefund(Status, uint256, uint256): Invalid refund conditions
  • InvalidStatusForWithdrawal(Status): Cannot withdraw non-funded intent
  • OpenDeadlinePassed(): ERC-7683 order opening deadline exceeded
  • TypeSignatureMismatch(): ERC-7683 orderDataType mismatch
  • ZeroClaimant(): Claimant cannot be zero

Integration Examples

Creating and Funding an Intent

Intent memory intent = Intent({
    destination: 42161, // Arbitrum
    route: Route({
        salt: keccak256(abi.encode(user, nonce)),
        deadline: block.timestamp + 1 hours,
        portal: destinationPortalAddress,
        nativeAmount: 0.1 ether,
        tokens: tokenAmounts,
        calls: calls
    }),
    reward: Reward({
        creator: msg.sender,
        prover: hyperProverAddress,
        deadline: block.timestamp + 1 hours,
        nativeAmount: 0.05 ether,
        tokens: rewardTokens
    })
});

portal.publishAndFund{value: totalNativeAmount}(intent, false);

Fulfilling on Destination

bytes32 intentHash = /* from IntentPublished event */;
Route memory route = /* from event data */;
bytes32 rewardHash = keccak256(abi.encode(reward));
bytes32 claimant = bytes32(uint256(uint160(solverAddress)));

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

// Fulfill and prove
portal.fulfillAndProve{value: route.nativeAmount + bridgeFee}(
    intentHash,
    route,
    rewardHash,
    claimant,
    proverAddress,
    sourceChainDomainID,
    proverData
);

Claiming Rewards

// After proof verification on source chain
portal.withdraw(destination, routeHash, reward);

Batch Operations

uint64[] memory destinations = new uint64[](3);
bytes32[] memory routeHashes = new bytes32[](3);
Reward[] memory rewards = new Reward[](3);

// Populate arrays...

portal.batchWithdraw(destinations, routeHashes, rewards);
I