Across: ABI Encoding for Crosschain Intent Orders

Across crosschain intent orders encode transfer instructions as application binary interface (ABI) data that must match the origin settlement contract’s decoder. The ten-field AcrossOriginSettler format belongs to the ERC-7683 v1 integration, whose destination fill(bytes32,bytes,bytes) interface is deprecated. Its payload contains token amounts, recipient details and optional message bytes. This layout applies to contracts that use that decoder. Matching types and tuple nesting establishes how the contract reads the order, while configured destinations and deadlines govern execution.

A ten-field ERC-7683 v1 payload needs one dynamic tuple, with an EVM recipient left-padded to fill its bytes32 field.

The Ten-Field Order Layout

AcrossOrderData contains token addresses, integer amounts, destination details and optional message bytes in a fixed sequence of ten fields. Its order matters even where neighboring fields share a type. Swapping two uint256 values can leave syntactically valid bytes while changing the transfer that the contract reads. These fields appear in the following order:

Which Encoding Matches the Settler’s Decoder?

The ten-field implementation expects one ABI-encoded AcrossOrderData tuple, matching abi.decode(order.orderData, (AcrossOrderData)) in the origin settler. Its component types are (address,uint256,address,uint256,uint256,bytes32,address,uint256,uint32,bytes). Because message is dynamic, the struct itself is dynamic. Encoding that struct as one argument introduces an outer offset to its content. A flat list of the ten members has a different top-level layout, even with identical values.

In Solidity, abi.encode(orderDataStruct) supplies the required struct wrapping. A client-side encoder needs one tuple parameter containing the same ordered components. An encoding that drops inputToken, inputAmount or depositNonce cannot reproduce this struct. The origin settler needs those fields alongside the destination instructions. Decoding with a different local schema does not establish compatibility with the contract’s decoder.

Type Hash and Function Calldata

OnchainCrossChainOrder wraps a uint32 fillDeadline, a bytes32 orderDataType and a bytes orderData payload for the struct-based open() interface. Its canonical function signature is open((uint32,bytes32,bytes)). Passing three separate top-level arguments describes a different function signature. The distinction changes the function selector, so the outer call requires the ABI of the selected deployment.

The type hash identifies the declared order-data format. Changing a transfer amount preserves that identifier. Changing the canonical type declaration changes its hash, including changes to member names or their types.

Function calldata adds another encoding layer around the order. A contract-call encoder supplies the function selector and outer arguments. The orderData field holds the inner encoded struct without another open() selector. Keep the type identifier, order payload and complete transaction calldata distinct; each has a different role in the call.

Integer Amounts and Token Units

Each token amount reaches the encoder as an integer in its own token units, so decimal display amounts need token-specific conversion. If a token uses d decimal places, a display amount A corresponds to A × 10^d base units, provided that the result is an exact integer. Each token has its own scale; ERC-20 makes its decimals() method optional. An encoder cannot infer that scale from an address or ticker. Keep inputAmount tied to the origin token and outputAmount tied to the destination token. Subtracting their raw integers has no economic meaning when their token units differ.

Recipient Padding and Address Conversion

Recipient data occupies the bytes32 field, while token identifiers and the exclusive relayer use address fields in this settlement format. An Ethereum Virtual Machine (EVM) address contains 20 bytes. Its wider recipient representation places 12 zero bytes before the address, leaving the address in the lower 20 bytes. The settler’s checked address conversion rejects a recipient with nonzero data in its upper 12 bytes. Left-padding preserves the account value; right-padding the address moves its bytes into different positions.

Illustration: Recipient Padding and Address Conversion (Across)

Open full-size image

A field’s width does not establish destination compatibility. This settler converts the wider recipient back into an EVM address for its deposit call. Arbitrary 32-byte account identifiers therefore do not become usable recipients simply because they fit inside bytes32.

Dynamic Message Offsets and Padding

Dynamic message bytes use an offset in the ABI head and a length word in the tail, followed by the actual contents. Inside this tuple, nine static fields and the message offset occupy ten words. The message offset measures from the start of the tuple’s content, independently of the surrounding function calldata. Treating it as an offset from the function selector points to the wrong location.

Standard ABI encoding stores dynamic bytes as a length word followed by contents padded to a multiple of 32 bytes. The length counts the actual contents, excluding padding. Trailing padding does not become part of the decoded message.

Packed encoding changes that structure by omitting normal padding and dynamic length information. Use standard ABI encoding for this order-data decoder. Message contents also have their own destination semantics. On the V3 contract-recipient callback path, a nonempty message requires the receiver to implement handleV3AcrossMessage(). If that callback fails, the fill reverts. Correct ABI wrapping alone does not establish that the receiver accepts its contents.

Message Length at a Padding Boundary

For a hypothetical offline comparison, keep the other nine fields and the outer order fixed, with values that fit their declared types. Compare an 18-byte message with a 38-byte message. Assume the selected contract uses the ten-field tuple decoder and the fill deadline remains valid. These message lengths illustrate encoding size without specifying executable destination instructions.

Encoding the 18-byte message produces 416 bytes of inner orderData. That total comprises a 32-byte outer tuple offset, a 320-byte tuple head, a 32-byte message-length word and 32 bytes for padded contents. The message contains 18 bytes even though its padded data occupies a complete word. Neither the outer order nor the function selector belongs in this total.

The 38-byte variant occupies 448 bytes because its contents require 64 bytes after padding. Increasing the actual message by 20 bytes increases this encoded payload by 32 bytes. Decoding must return the original fixed fields and the exact intended message. An observed message length of 38 identifies the larger variant; unexpected changes to an amount or recipient invalidate the comparison.

The larger payload carries more message data while preserving the same tuple schema. Its byte count provides a concrete encoding result, without proving that a recipient accepts the instructions. Deadline expiry leaves both payloads decodable while preventing a valid subsequent fill.

Fill Deadlines and Exclusivity Values

fillDeadline gives an absolute Unix timestamp in seconds, while the exclusivity field supplies a separate parameter governing exclusive fill rights. The outer order’s uint32 type constrains representation. The paired SpokePool also limits how far into the future a deposit can set its deadline through fillDeadlineBuffer. A deadline beyond the current origin-chain timestamp plus that buffer makes the deposit revert, even if it fits the integer type.

The SpokePool’s exclusivity conversion distinguishes zero, relative durations and absolute timestamps. Zero requests no exclusivity. Positive values at or below MAX_EXCLUSIVITY_PERIOD_SECONDS add the supplied seconds to the deposit timestamp. Larger values specify an absolute deadline. A positive exclusivity parameter requires a nonzero exclusive relayer; the deposit reverts if that relayer is zero. Preserve that relationship when constructing the tuple; the name exclusivityPeriod alone does not describe every accepted parameter interpretation.

Visual summary: Across: Fill Deadlines and Exclusivity Values

Open full-size image

Nonce Choice and Deposit Identity

depositNonce selects how this origin settler obtains a deposit identifier, while the ABI represents either choice with the same integer type. Zero selects depositV3() and the SpokePool’s sequential counter. A nonzero value selects unsafeDeposit() and an identifier derived from the settler, depositor and nonce. Reusing those inputs repeats the deposit identifier. If the resulting relay hash matches one that the destination SpokePool has already marked as filled, another fill reverts. A sequential identifier can change between a view call and mining if other deposits advance the counter. The emitted deposit record identifies the actual deposit. This nonce differs from the wallet’s transaction nonce and the gasless order’s authorization nonce.

Decoded Fields and Resolved Orders

A call to resolve() expands an encoded onchain order into the outputs and fill instructions that the settlement interface exposes. In this implementation, maxSpent describes the output that the filler supplies to the transfer recipient. minReceived records the input token and amount on the origin chain, naming exclusiveRelayer as recipient. That entry describes filler-side settlement. Read each output’s token, amount, recipient and chain together. The name minReceived does not identify the end user’s destination amount.

A view result does not move funds or reserve a fill. The configured destination settler and other chain state still govern execution.

Decoded Fields and Resolved Orders (Across) - diagram

Open full-size image

Local round-trip decoding checks the serialized fields. Origin-call simulation additionally exercises the selected function with its caller and execution state, including token spending permissions. Resolution alone does not test the token transfer that open() performs. A successful origin receipt containing Open establishes order opening. A matching destination fill establishes completion separately.

Quick answers

Does ABI Encoding Hide an Across order’s Recipient or Message?

ABI encoding does not encrypt an order’s recipient or message. Anyone with the encoded bytes and matching schema can decode those fields. A transaction included in a block carries its call data on the blockchain. Keep private keys, passwords and other secrets out of the message field; hexadecimal formatting provides no confidentiality.

How Should Large Token Amounts Reach a TypeScript ABI Encoder?

Large integer amounts should reach a compatible TypeScript ABI encoder as bigint values without passing through an imprecise number conversion. Preserve the exact base-unit amount from a decimal string or another exact integer representation. Converting an already rounded JavaScript number to bigint preserves its rounding error. Token decimal conversion and integer representation remain separate concerns.

Are Empty Message Bytes Equivalent to a Zero Byte?

Empty bytes and a single zero byte represent different messages. The hexadecimal value 0x has length zero, while 0x00 has length one. ABI padding after the message does not change that length. For the V3 contract-recipient callback path, 0x00 counts as a nonempty message even though its sole byte is zero.

What Additional Fields Belong to the v1 Gasless Order Envelope?

The v1 GaslessCrossChainOrder adds originSettler, user, nonce, originChainId and openDeadline to the onchain order’s fillDeadline, orderDataType and orderData. Its openFor() interface also takes a user signature and originFillerData. Those outer fields describe the signer, settlement context and authorization window. They do not replace the transfer details inside the encoded AcrossOrderData tuple.

Will Renaming Local ABI Labels Change the Encoded Order?

Renaming local labels leaves ABI bytes unchanged when the component types, positions and values remain identical. An encoder that accepts objects may require matching changes to object keys. EIP-712 type declarations have a different rule: member names contribute to the type hash. Changing a canonical member name therefore changes the format identifier even if the underlying ABI component types remain unchanged.

Can Payload Length Determine an Across transaction’s Gas Cost?

Payload length alone cannot determine a transaction’s gas cost. Byte contents and the contract operations that execute affect gas consumption. The origin network’s fee parameters also affect the amount paid. A calculation of inner order-data size excludes the surrounding call and transaction, so it cannot supply a complete gas estimate or destination execution charge.

updated