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:
-
inputTokenusesaddress, followed byinputAmountasuint256. -
outputTokenusesaddress, followed byoutputAmountasuint256. -
destinationChainIdusesuint256, followed byrecipientasbytes32. -
exclusiveRelayerusesaddress, followed bydepositNonceasuint256. -
exclusivityPeriodusesuint32, followed bymessageasbytes.
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.
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.
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.
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