ERC-7683 - Cross Chain Intents

Created 2024-04-11
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This ERC defines a solver-facing interface for intent protocols. A protocol exposes orders as opaque payloads and provides a resolver contract that translates those payloads into a common order representation.

Resolvers are used by solvers, offchain via eth_call, to obtain the information needed to evaluate and fulfill an order. This enables programmable intent solvers to add support for intent protocols by vetting their resolvers instead of implementing protocol-specific execution logic.

Motivation

An order is an offer of payment in exchange for the fulfillment of a set of requirements. Intent protocols allow users to express desired outcomes as orders, while specialized actors called solvers, also known as fillers, execute the required actions and receive payment. This model is especially useful for cross-chain activity, where execution may require coordinating transactions, liquidity, and settlement across multiple chains, but the same design applies more generally to intent protocols.

In a typical order lifecycle, a user expresses an intent to an application, the application creates an order for a specific protocol, the order is submitted to an order feed, a solver evaluates the order, the solver executes the required steps, and settlement pays the solver.

Without a common solver-facing representation, solver liquidity is fragmented across protocol-specific integrations. Each solver must integrate separately with each protocol's payload format, execution flow, and payment semantics. This increases integration costs, weakens shared solver networks and order dissemination services, and makes it harder for new protocols to attract competitive execution.

Intent protocols also need room to differ in how users create orders, how funds are authorized, how settlement is verified, how prices are determined, and whether execution is escrow-first, fill-first using resource locks, or auction-based. This ERC does not require protocols to use a common escrow, settlement contract, or fill function.

Instead, this ERC standardizes how orders are consumed by solvers. A resolver translates a protocol-specific payload into general-purpose instructions that solvers can evaluate for safety, profitability, execution, and payment. When safety depends on conditions that a resolver cannot verify, the resolver surfaces those conditions as explicit assumptions for solvers to validate. The result is a path toward protocol-agnostic solvers while preserving flexibility for protocols to innovate in order creation and settlement.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Key Terms

Orders

An order's requirements are expressed as a list of steps and a list of variables.

To fulfill the requirements of an order, each step MUST be executed as specified, and variable values MUST be decided as specified and used consistently.

A step or variable can have hard dependencies on other steps and variables as documented below. Not every mention of a variable is a hard dependency. Hard dependencies MUST be acyclic. Fulfillment MUST proceed in hard-dependency order, i.e., a step MUST NOT execute before its hard dependencies have successfully executed.

A step MAY abort the order. When aborted, requirements are not fulfilled and no payment is offered.

Step: Call

Parameters:

To execute the step a solver MUST evaluate each element of arguments to a constant, encode them into call data together with selector (see Values and Encoding), and submit a transaction that makes a call to target with said data.

The step has a hard dependency on all variables mentioned in arguments.

Each element of attributes determines additional requirements or options.

Attribute: NeedsStep

Parameters:

The call has a hard dependency on the step numbered stepIdx.

The attribute MAY be omitted if the dependency is implied by a hard dependency on a variable.

Attribute: NeedsVariable

Parameters:

The call has a hard dependency on the variable numbered varIdx.

The attribute MAY be omitted if the dependency is implied by the call arguments, formulas, or another variable dependency.

Attribute: SpendsERC20

Parameters:

The call MAY transfer tokens from the caller, up to the amount given by amountFormula, using transferFrom on token called from spender. The tokens SHOULD be transferred to recipient.

The solver MUST ensure the caller account has balance of token and allowance for spender of at least said amount.

The call does not have a hard dependency on the variables in amountFormula. In particular, the formula MAY depend on step outputs such as inclusion timing (e.g., block timestamp). If the formula depends on timing, the amount SHOULD decrease with time so that a tight upper bound can be estimated by the solver.

Attribute: SpendsGas

Parameters:

The call MAY consume up to the amount given by amountFormula.

The solver MUST ensure the call executes with a gas limit of at least said amount of gas.

If this attribute is omitted, the solver MAY estimate gas cost by simulation prior to execution of any other step in the order. If simulation fails due to missing prerequisites, the order SHOULD include this attribute.

The call has a hard dependency on the variables in amountFormula.

Attribute: RevertPolicy

Parameters:

The call MAY revert.

If the call would revert with data that begins with expectedReason, the solver MUST proceed according to policy:

A call MUST NOT revert without a matching RevertPolicy attribute.

Attribute: TimingBounds

Parameters:

The call MUST be included onchain such that the value of field observed when executed is at least lowerBound and at most upperBound.

Variables

An order includes variables as placeholders for values that the solver decides.

Each variable MUST be decided as specified by its variable role. Once a value is decided it MUST be used consistently wherever the variable is referenced.

Role: PaymentRecipient

No parameters.

The variable MUST be assigned to the account where the solver prefers to receive payment.

Role: PaymentChain

No parameters.

The variable MUST be assigned to the ID of the chain where the solver prefers to receive payment.

Role: StepCaller

Parameters:

The variable MUST be assigned to the account used as the caller when executing the step numbered stepIdx.

Role: ExecutionOutput

Parameters:

The variable MUST be assigned to the value observed in execution. For example, the "block.number" field must be the block number where the call in a step was included onchain.

The variable has a hard dependency on the step numbered stepIdx.

Role: Witness

Parameters:

kind MUST identify some offchain procedure by which the solver can obtain a value, as a function of data and the values of variables.

The variable MUST be assigned to the result of invoking this procedure.

The variable has a hard dependency on all variables mentioned in variables.

Role: Query

Parameters:

The solver MUST evaluate each element of arguments to a constant, encode them into call data together with selector (see Values and Encoding), and invoke eth_call to target with said data.

If blockNumber is omitted, the latest block is used.

The call MUST NOT revert, and the variable MUST be assigned to the value produced by interpreting the value returned by eth_call as a framed ABI encoding (see Values and Encoding).

Role: QueryEvents

Parameters:

The solver MUST invoke eth_getLogs for emitter at blockNumber filtered by the provided topics.

If blockNumber is omitted, the latest block is used.

The variable MUST be assigned to the results of eth_getLogs as the framed ABI encoding (see Values and Encoding) of an array of the following struct.

struct EthLog {
    address emitter;
    bytes32[] topics;
    bytes data;
    uint256 blockNumber;
    bytes32 transactionHash;
    uint256 transactionIndex;
    bytes32 blockHash;
    uint256 logIndex;
}

Payments

An order includes a list of payments that will be made.

Payment: ERC20

Parameters:

When the step numbered onStepIdx is executed, a payment MUST be made of at least the amount given by amountFormula of token sourced from sender. The payment MUST be transferred to the address value of the variable numbered recipientVarIdx, on the chain indicated by token. The payment SHOULD be delayed by estimatedDelaySeconds with high confidence.

Values and Encoding

Values assigned to variables are untyped and represented only by their ABI encoding and whether they are statically or dynamically sized.

Some contexts that consume values may interpret them as a particular type. For example, the amount formula of SpendsGas is interpreted as uint256.

Framed ABI Encoding

In the EVM, values are represented by their framed ABI encoding.

The framed ABI encoding of a value is the canonical ABI encoding of the two-element tuple ("", value), whose first element is the empty string and whose second element is the value. In Solidity, it is the bytes produced by abi.encode("", value).

A framed ABI encoding corresponds to a dynamically sized value exactly when it starts with the 96-byte prefix

0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000000

The remaining bytes after the prefix are the ABI encoding of the value on its own.

A framed ABI encoding for a statically sized value begins with a 32-byte prefix and ends with a 32-byte suffix. The remaining bytes in between prefix and suffix are the ABI encoding of the value. The numeric value of the prefix will be exactly 32 plus the length of the value encoding, and the suffix will be the zero word.

Call Data Encoding

To encode call data, concatenate the 4-byte function selector with the ABI encoding of the function arguments.

The ABI encoding of the function arguments is like standard ABI encoding. For statically sized values, the head is its ABI encoding, and the tail is empty. For dynamically sized values, the head is the offset to the tail, and the tail is the ABI encoding.

Resolvers

An order can be transmitted in the form of a payload encoded for a specific resolver.

A resolver provides a way to decode the payload as an order (i.e., steps, variables, and payments), validating and guaranteeing that the order is well formed and safe for solvers, except for additional named assumptions that the resolver cannot check for itself.

A resolver MUST guarantee that an order may only abort as explicitly specified in revert policies. If no abort policy is triggered, a solver that begins to execute the steps of an order MUST be able to fulfill all requirements and receive all payments.

Liveness and censorship resistance of the underlying chains MAY be implicitly assumed. A particular resolver MAY make and document additional implicit assumptions (e.g., the security of a particular protocol). A solver MUST review all such implicit assumptions before a trusting a resolver.

Liveness and censorship resistance of any tokens used in SpendsERC20 attributes MAY be implicitly assumed.

Named Assumptions

Additional assumptions for a given order must be identified by a name and may be parameterized by data.

A solver MUST validate the assumption (e.g., checking against a whitelist) before fulfilling the order.

struct Assumption {
    string name;
    bytes data;
}

EVM Resolvers

A resolver can be implemented and deployed as a contract for the EVM with the IResolver interface.

interface IResolver {
    struct ResolvedOrder {
        /// Array of `IStep` ABI calldata.
        bytes[] steps;
        /// Array of `IVariableRole` ABI calldata.
        bytes[] variables;
        /// Array of `IPayment` ABI calldata.
        bytes[] payments;
        Assumption[] assumptions;
    }

    struct Assumption {
        string name;
        bytes data;
    }

    function resolve(bytes calldata payload) external view returns (ResolvedOrder memory);
}

interface IStep {
    /// @param arguments Each element is either an ABI-encoded variable index, or a framed ABI encoding of a value.
    /// @param attributes Each element is `IAttribute` ABI calldata.
    function Call(
        bytes calldata target,
        bytes4 selector,
        bytes[] calldata arguments,
        bytes[] calldata attributes
    ) external;
}

interface IVariableRole {
    function PaymentRecipient() external;
    function PaymentChain() external;
    function StepCaller(uint256 stepIdx) external;
    function ExecutionOutput(string calldata field, uint256 stepIdx) external;
    function Witness(string calldata kind, bytes calldata data, uint256[] calldata variables) external;
    /// @param arguments Each element is a variable index or a framed ABI encoding.
    /// @param blockNumber `uint256(-1)` means none.
    function Query(bytes calldata target, bytes4 selector, bytes[] calldata arguments, uint256 blockNumber) external;
    /// @param topicMatch Bitmask of topics to filter by; e.g., `0x01` filters by `topic0` only.
    /// @param blockNumber `uint256(-1)` means none.
    function QueryEvents(bytes calldata emitter, bytes1 topicMatch, bytes32 topic0, bytes32 topic1, bytes32 topic2, bytes32 topic3, uint256 blockNumber) external;
}

interface IPayment {
    /// @param amountFormula `IFormula` ABI calldata.
    function ERC20(bytes calldata token, bytes calldata sender, bytes calldata amountFormula, uint256 recipientVarIdx, uint256 onStepIdx, uint256 estimatedDelaySeconds) external;
}

interface IAttribute {
    /// @param amountFormula `IFormula` ABI calldata.
    function SpendsERC20(bytes calldata token, bytes calldata amountFormula, bytes calldata spender, bytes calldata recipient) external;
    /// @param amountFormula `IFormula` ABI calldata.
    function SpendsGas(bytes calldata amountFormula) external;
    /// @param lowerBound Empty, or `IFormula` ABI calldata.
    /// @param upperBound Empty, or `IFormula` ABI calldata.
    function TimingBounds(string calldata field, bytes calldata lowerBound, bytes calldata upperBound) external;
    function NeedsStep(uint256 stepIdx) external;
    function NeedsVariable(uint256 varIdx) external;
    function RevertPolicy(string calldata policy, bytes calldata expectedReason) external;
}

interface IFormula {
    function Constant(uint256 val) external;
    function Variable(uint256 varIdx) external;
}

Rationale

Resolver-Centric Standardization

A key consideration is to ensure that a broad range of intent designs can work within the same standard. To enable this, the specification is designed around resolving a protocol-specific payload into a common solver-facing representation. Resolution enables solvers to validate and assess orders without specific knowledge of the protocol-specific payload at hand.

Within this model, implementers of the standard have design flexibility to customize behavior such as:

The payload allows implementations to take arbitrary specifications for these behaviors while still enabling solvers to parse the resolved requirements of the order.

A compliant intent protocol must implement and deploy a resolver, along with a payload format that encodes orders for that protocol. The resolver contract must validate and interpret order payloads, and produce instructions for solvers to follow.

Resolution happens offchain via eth_call, even though the resolver is published onchain. Because resolution is not required to be included in an onchain transaction, the payload and translation process are not constrained by onchain gas costs. Resolvers can therefore support semantically rich orders and perform complex decoding, validation, and instruction generation without forcing that complexity into calldata or emitted events.

Publishing the resolver onchain is useful because the resolver is the point of trust. Solver operators can whitelist resolver addresses, and once vetted, the instructions they produce are trusted. Vetting is done through the usual means, such as security audits, bounties, and lindiness. However, neither resolvers nor the resolved order representation are inherently EVM-specific: the EVM resolver interface is only one concrete encoding of the more general model defined by this ERC.

General-Purpose Building Blocks

A guiding principle of this standard is to use general-purpose building blocks instead of implementation-specific extensions. A solver made of general-purpose building blocks is a programmable solver: it can adapt to a wide variety of intent protocols without prior knowledge of the specific steps required in each case.

This is enabled by defining a common language for solver instructions and delivering those instructions with each order through resolution.

Execution Instructions

An important component of the standard is creating a flexible and robust mechanism for solvers to ensure their executions are valid. For execution to be valid, it typically must satisfy the following constraints:

  1. It must be executed on the correct chain(s)
  2. It must be executed on the correct contract
  3. It must include some (not necessarily all) information from the order payload
  4. It may require some execution information from earlier steps, e.g. auctions based on inclusion timing

The steps, variables, attributes, and payments in a resolved order are intended to ensure it's simple for the solver to meet all of these requirements by calling resolve.

This functionality also makes it feasible for a user, solver, or order distribution system to perform an end-to-end simulation of the order execution to evaluate all resulting state transitions without understanding the nuances of a particular execution system.

Cross-compatibility

This standard is intended to be cross-compatible with other ecosystems. It standardizes interfaces and data types on EVM chains, but uses ERC-7930 interoperable addresses so orders can refer to accounts and contracts outside the EVM address space. It also allows for the creation of sibling standards that define compatible interfaces, data types, and flows within other ecosystems. Intents created within these sibling standards should be able to be filled on an EVM chain and vice versa.

Previous Draft

An earlier draft of this ERC standardized a broader portion of the order lifecycle:

Although the earlier draft defined standard data types for orders, these types were parameterized by implementation-specific fields. Because of this field, orders were only superficially standardized. A solver that intended to fill orders under that draft still had to implement support for different protocols' subtypes, which is not meaningfully different from a situation where each protocol implements an entirely custom interface.

The earlier draft also defined maxSpent and minReceived in ResolvedCrossChainOrder so that solvers could compute whether an order was profitable. However, this only provided a lower bound on profit. If the bound was not tight, orders could incorrectly appear unprofitable and not get filled. In the worst case, protocols could not provide a maxSpent other than UINT256_MAX. This can happen when the order signed by the user binds the worst price they are willing to accept, or when a solver's actual cost depends on a variable chosen during execution, such as priority fees in a Priority Gas Auction.

The earlier draft was also centered on protocols that escrow user funds per order as a necessary step prior to fulfillment. This is not the case in protocols based on resource locks, also referred to as fill-first protocols: if the user funds are under a lock that is trusted by the solver, there is no need to open the order on the origin chain before it can be safely filled.

Finally, the earlier draft imposed gas overhead on protocols by requiring large calldata structs and by emitting the entire resolved order in the Open event. These costs can impact prices for end users and make a standard-compliant interface less attractive than a protocol-specific interface.

The present design preserves the interoperability goal while moving the standardization boundary to the solver-facing resolution interface. This preserves protocol flexibility for user authorization, order creation, settlement verification, and execution ordering, while still giving solvers a common representation for evaluating and fulfilling orders.

Security Considerations

This ERC standardizes how a protocol describes an order to solvers; it does not standardize or guarantee the security of the protocol that ultimately settles the order. A resolver can describe the steps, payments, and assumptions for an order, but the safety of following those instructions depends on the resolver implementation, settlement contracts, assets involved, and any offchain or cross-chain systems used by the protocol.

Resolver implementations should document any implicit assumptions they rely on beyond those allowed by this ERC, so solvers and auditors can evaluate them alongside the explicit assumptions included in resolved orders.

Solvers are exposed to risk from the time they commit capital, approvals, or transactions to an order until their expected payment is final and spendable. Security analysis should therefore cover the full execution and settlement window, including adversarial changes to protocol state, chain state, token behavior, oracle values, permissions, or message-delivery paths that could make previously valid instructions or assumptions unsafe.

Audits of resolver and protocol implementations should focus on whether a solver that correctly follows resolver instructions can be led into an insecure position. In particular, they should verify that all requirements needed for solver safety are either enforced by the resolver or protocol or surfaced as assumptions, that execution steps cannot spend solver assets, require solver permissions, or create solver obligations except as disclosed by the resolved order, and that payment paths cannot be invalidated after the solver has incurred costs.

Copyright

Copyright and related rights waived via CC0.