Encoding Reference

This page gives the exact packed bit and byte layouts used by the on-chain SwapVM and Aqua contracts, so an off-chain decoder can reconstruct every field without guessing. Every offset below is taken directly from the source symbols cited under each section. Bit positions are counted from the least-significant bit (bit 0) of the underlying uint256; byte offsets are counted from the start of the byte string and are big-endian, matching Solidity's abi.encodePacked layout.

All numbers here are verified against the deployed contract source. Where a field's position depends on run-time data (variable-length hook slices), the layout tells you which packed index to read to locate it; the concrete offset is only known once the order's own data is present.

MakerTraits — packed uint256 header

Source: swap-vm/src/libs/MakerTraits.sol, type MakerTraits is uint256 and library MakerTraitsLib. The value is assembled in MakerTraitsLib.build; the constants below are the literal bit-flag definitions.

| Bit(s) | Width | Field | Source constant / expression | | ------- | ----- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | ------------- | | 255 | 1 | shouldUnwrapWeth | SHOULD_UNWRAP_BIT_FLAG = 1 << 255 | | 254 | 1 | useAquaInsteadOfSignature | USE_AQUA_INSTEAD_OF_SIGNATURE_BIT_FLAG = 1 << 254 | | 253 | 1 | allowZeroAmountIn | ALLOW_ZERO_AMOUNT_IN = 1 << 253 | | 252 | 1 | hasPreTransferInHook | HAS_PRE_TRANSFER_IN_HOOK_BIT_FLAG = 1 << 252 | | 251 | 1 | hasPostTransferInHook | HAS_POST_TRANSFER_IN_HOOK_BIT_FLAG = 1 << 251 | | 250 | 1 | hasPreTransferOutHook | HAS_PRE_TRANSFER_OUT_HOOK_BIT_FLAG = 1 << 250 | | 249 | 1 | hasPostTransferOutHook | HAS_POST_TRANSFER_OUT_HOOK_BIT_FLAG = 1 << 249 | | 248 | 1 | preTransferInHookHasTarget | PRE_TRANSFER_IN_HOOK_HAS_TARGET = 1 << 248 | | 247 | 1 | postTransferInHookHasTarget | POST_TRANSFER_IN_HOOK_HAS_TARGET = 1 << 247 | | 246 | 1 | preTransferOutHookHasTarget | PRE_TRANSFER_OUT_HOOK_HAS_TARGET = 1 << 246 | | 245 | 1 | postTransferOutHookHasTarget | POST_TRANSFER_OUT_HOOK_HAS_TARGET = 1 << 245 | | 244–224 | 21 | reserved (written as zero) | gap between flag bits and index block | | 223–208 | 16 | index3 — end of PostTransferOutHook slice = program start offset | orderDataIndexes << ORDER_DATA_SLICES_INDEXES_BIT_OFFSET where ORDER_DATA_SLICES_INDEXES_BIT_OFFSET = 160; four uint16 packed as (index0<<0) | (index1<<16) | (index2<<32) | (index3<<48) | | 207–192 | 16 | index2 — end of PreTransferOutHook slice | | | 191–176 | 16 | index1 — end of PostTransferInHook slice | | | 175–160 | 16 | index0 — end of PreTransferInHook slice | | | 159–0 | 160 | receiver address (if zero, defaults to maker) | uint160(args.receiver) |

The four packed indexes are read back by _getOffset(traits, sliceNumber):

Solidity
1
2
bitShift = sliceNumber << ORDER_DATA_SLICES_INDEX_BIT_SIZE_SHL;   // sliceNumber * 16
offset   = (traits >> 160 >> bitShift) & type(uint16).max;      // ORDER_DATA_SLICES_INDEX_BIT_MASK

Order.data byte layout (the bytes referenced by MakerTraits)

The data field of ISwapVM.Order is built with bytes.concat / abi.encodePacked in MakerTraitsLib.build. It is a packed (non-ABI) byte string. The enum OrderDataSlices { PreTransferInHook, PostTransferInHook, PreTransferOutHook, PostTransferOutHook, Program } names the slices. In the deployed v1.0.1 layout there is no token prefix — the token pair is passed to quote/swap as explicit tokenIn/tokenOut arguments, not stored in data — and the four packed index0..index3 values in MakerTraits mark the slice boundaries:

Byte range Slice Notes
[0 : index0] PreTransferInHook Starts at 0. If PRE_TRANSFER_IN_HOOK_HAS_TARGET is set, first 20 bytes are the hook target address, remainder is hook calldata; otherwise the whole slice is calldata and the target defaults to maker.
[index0 : index1] PostTransferInHook Same target-prefix rule with POST_TRANSFER_IN_HOOK_HAS_TARGET.
[index1 : index2] PreTransferOutHook Same rule with PRE_TRANSFER_OUT_HOOK_HAS_TARGET.
[index2 : index3] PostTransferOutHook Same rule with POST_TRANSFER_OUT_HOOK_HAS_TARGET.
[index3 : data.length] Program (VM bytecode tail) The SwapVM program runs from index3 to the end of data. With no hooks, index0..index3 are all 0, so the program is the entire data. See program() and _getDataSlice(...) for OrderDataSlices.Program.

Confirmed by MakerTraitsLib._getDataSlice: the PreTransferInHook slice starts at 0, and the Program slice stops at data.length. All intermediate boundaries are the packed indexes above. (The unreleased main branch prepends a tokenA || tokenB pair and shifts every slice by 40 bytes, but that revision is not tagged or deployed — build against the layout above.)

TakerTraits — packed header + slice table

Source: swap-vm/src/libs/TakerTraits.sol, type TakerTraits is uint256 and library TakerTraitsLib. Unlike MakerTraits, the taker value is a 22-byte (176-bit) header that is prepended to a packed tail. parse reads it as TakerTraits.wrap(uint176(bytes22(data.slice(0, 22, ...)))) and the tail is data.slice(22).

Header composition (as emitted by build)

build does abi.encodePacked(slicesIndexes /*uint160*/, flags /*uint16*/, ...tail...). So on the wire the first 20 bytes are the ten packed slice indexes and the next 2 bytes are the flag word:

Header byte range Content
[0 : 20] slicesIndexes — ten uint16 values (index0..index9), big-endian; index9 occupies bytes [0:2], index0 occupies bytes [18:20]
[20 : 22] flags — the uint16 flag word

Header as a uint176 (how the getters read it)

TAKER_DATA_SLICES_INDEXES_BIT_OFFSET = 16, so _getOffset(traits, n) = (traits >> 16 >> (n*16)) & type(uint16).max. The flag word sits in the low 16 bits.

Bit(s) of uint176 Field
15–0 flags (see flag table below)
31–16 index0
47–32 index1
63–48 index2
79–64 index3
95–80 index4
111–96 index5
127–112 index6
143–128 index7
159–144 index8
175–160 index9

Flag word (low 16 bits)

Bit Mask Flag
0 0x0001 IS_EXACT_IN_BIT_FLAG
1 0x0002 SHOULD_UNWRAP_BIT_FLAG
2 0x0004 HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG
3 0x0008 HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG
4 0x0010 IS_STRICT_THRESHOLD_BIT_FLAG
5 0x0020 IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG
6 0x0040 USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG
7 0x0080 IS_A_TO_B_BIT_FLAGunreleased main only; not present on the deployed v1.0.1 router (there, swap direction is the explicit tokenIn/tokenOut arguments)
15–8 unused

Tail slice table

Offsets are relative to the tail (the bytes after the 22-byte header). The enum TakerDataSlices ordering drives _getStartOffset/_getStopOffset: slice s starts at index[s-1] (or 0 for the first) and stops at index[s] (or dataLength for the last).

Slice (enum) idx Start Stop Fixed width, if any
Threshold 0 0 index0 0 or 32 bytes
To 1 index0 index1 0 or 20 bytes
Deadline 2 index1 index2 0 or 5 bytes (uint40)
PreTransferInHook 3 index2 index3 variable
PostTransferInHook 4 index3 index4 variable
PreTransferOutHook 5 index4 index5 variable
PostTransferOutHook 6 index5 index6 variable
PreTransferInCallback 7 index6 index7 variable
PreTransferOutCallback 8 index7 index8 variable
InstructionsArgs 9 index8 index9 variable
Signature 10 index9 dataLength tail remainder

There are 11 slices but only 10 packed indexes: the first slice always starts at 0 and the last always ends at dataLength, so only the 10 interior boundaries need storing. Threshold must be exactly 0 or 32 bytes (enforced in build).

The strategy shape emitted in Shipped.strategy

Source: aqua/src/interfaces/IAqua.sol (event Shipped(address maker, address app, bytes32 strategyHash, bytes strategy)) and aqua/src/Aqua.sol (ship computes strategyHash = keccak256(strategy) and emits Shipped). For a SwapVM strategy, strategy is abi.encode(order) where order is ISwapVM.Order — confirmed by swap-vm/test/base/AquaStrategyBuilders.sol (bytes memory strategy = abi.encode(order);) and by SwapVM.hash, which for Aqua orders returns keccak256(abi.encode(order)) (so strategyHash == orderHash).

ISwapVM.Order (from swap-vm/src/interfaces/ISwapVM.sol):

Solidity
1
2
3
4
5
struct Order {
    address    maker;    // liquidity provider
    MakerTraits traits;  // uint256, packed header above
    bytes      data;     // hooks... || program  (no token prefix at v1.0.1)
}

Because it is standard abi.encode of a (address, uint256, bytes) tuple, the outer byte layout of Shipped.strategy is:

Offset Word Content
0x00 head[0] maker — address, left-padded to 32 bytes (value in low 20 bytes)
0x20 head[1] traits — the full uint256 MakerTraits value
0x40 head[2] offset to data tail = 0x60 (96)
0x60 tail data.length (uint256)
0x80 tail data bytes, right-padded to a 32-byte boundary

A decoder therefore:

  1. Reads maker from the low 20 bytes of [0x00:0x20].

  2. Reads traits from [0x20:0x40] and decodes the MakerTraits bit map (above) — in particular index3 at bits 223–208, which is the program start offset within data.

  3. Reads len = data.length from [0x60:0x80] and takes data = strategy[0x80 : 0x80 + len].

  4. Within data (no token prefix at v1.0.1): the SwapVM program bytecode tail = data[index3 : len]; with no hooks index3 = 0, so the program is all of data. The token pair is not in data — it is supplied as the tokenIn/tokenOut arguments to quote/swap.

The strategy is emitted in full (not pre-hashed) specifically for data availability, so an indexer can recover the complete Order — including the executable program tail — from the Shipped event alone. strategyHash = keccak256(strategy) is the same value the router computes as the order hash for Aqua strategies.

rawBalances packing (Balance struct)

Source: aqua/src/libs/Balance.sol (struct Balance { uint248 amount; uint8 tokensCount; } and BalanceLib.load/store) and aqua/src/Aqua.sol (_balances mapping and _DOCKED). Each balance occupies exactly one 32-byte storage slot.

The pack/unpack is done in assembly:

// store
packed = or(amount, shl(248, tokensCount));   // amount in low 248 bits, count in top byte
// load
amount      = packed & 0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
tokensCount = shr(248, packed);
Slot bit(s) Slot byte(s), big-endian Field
255–248 byte 0 (most significant) tokensCount (uint8)
247–0 bytes 1–31 amount (uint248, max 2^248 − 1)

The external view rawBalances(maker, app, strategyHash, token) returns (uint248 balance, uint8 tokensCount) as two separate ABI words; the single-slot packing above is the storage form, not the return form.

tokensCount sentinel values and _DOCKED

uint8 private constant _DOCKED = 0xff; (source: aqua/src/Aqua.sol). The tokensCount byte doubles as a lifecycle marker:

Value Meaning
0x00 inactive — never shipped, or slot empty
0x01 … 0xFE (1–254) active strategy; the number of tokens in the strategy
0xFF (_DOCKED) docked (revoked); balance forced to 0

The maximum token count is _DOCKED - 1 = 254: ship reverts with MaxNumberOfTokensExceeded(tokensCount, _DOCKED - 1) if tokensCount == _DOCKED. safeBalances and push both treat a token as usable only when tokensCount > 0 && tokensCount != _DOCKED; dock stores (0, _DOCKED) for every token in the strategy.

Source files

  • swap-vm/src/libs/MakerTraits.solMakerTraits, MakerTraitsLib (flag constants, build, _getOffset, OrderDataSlices)

  • swap-vm/src/libs/TakerTraits.solTakerTraits, TakerTraitsLib (flag constants, build, parse, TakerDataSlices, _getStartOffset/_getStopOffset)

  • swap-vm/src/interfaces/ISwapVM.solISwapVM.Order

  • swap-vm/src/SwapVM.solhash() (keccak256(abi.encode(order)) for Aqua orders), ORDER_TYPEHASH

  • swap-vm/test/base/AquaStrategyBuilders.solabi.encode(order) shipped as the strategy

  • aqua/src/interfaces/IAqua.solShipped event, rawBalances

  • aqua/src/Aqua.solship/dock/push/safeBalances, _DOCKED

  • aqua/src/libs/Balance.solBalance struct, BalanceLib.load/store

TakerTraits bridge — SDK wrapper → Solidity flag → bit

The two canonical TakerTraits implementations are the on-chain library TakerTraitsLib (swap-vm/src/libs/TakerTraits.sol) and the off-chain wrapper class TakerTraits in @1inch/swap-vm-sdk (typescript/swap-vm/src/swap-vm/taker-traits.ts). They encode the same 22-byte header — uint176 = 20-byte slicesIndexes (ten uint16) followed by a uint16 flag word — but name the flags differently and, critically, store the flag constants in two different forms. This section is the exact field-by-field bridge so a value round-trips losslessly between them.

The two sides define the flag constants differently. In Solidity each constant is a mask already shifted into place (IS_EXACT_IN_BIT_FLAG = 0x0001, SHOULD_UNWRAP_BIT_FLAG = 0x0002, …) and is applied with traits & MASK. In the SDK each constant is a bit index (IS_EXACT_IN_BIT_FLAG = 0n, SHOULD_UNWRAP_BIT_FLAG = 1n, …) applied with BN.getBit(index) / BN.setBit(index). Mask 1 << index on the Solidity side equals bit index on the SDK side. Do not read the SDK's 0n…6n as masks.

Flag word bridge (low 16 bits of the header)

All seven flags live in the low uint16 of the uint176 header. The SDK exposes each as a public readonly boolean property on the TakerTraits instance (there are no getter methods on the SDK side); Solidity exposes each via a library getter that masks the packed value.

Bit Solidity mask Solidity constant (TakerTraitsLib) Solidity Args field / getter SDK property (TakerTraits) SDK constant (bit index)
0 0x0001 IS_EXACT_IN_BIT_FLAG isExactIn / isExactIn() exactIn IS_EXACT_IN_BIT_FLAG = 0n
1 0x0002 SHOULD_UNWRAP_BIT_FLAG shouldUnwrapWeth / shouldUnwrapWeth() shouldUnwrap SHOULD_UNWRAP_BIT_FLAG = 1n
2 0x0004 HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG hasPreTransferInCallback / hasPreTransferInCallback() preTransferInCallbackEnabled HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG = 2n
3 0x0008 HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG hasPreTransferOutCallback / hasPreTransferOutCallback() preTransferOutCallbackEnabled HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG = 3n
4 0x0010 IS_STRICT_THRESHOLD_BIT_FLAG isStrictThresholdAmount / isStrictThresholdAmount() strictThreshold IS_STRICT_THRESHOLD_BIT_FLAG = 4n
5 0x0020 IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG isFirstTransferFromTaker / isFirstTransferFromTaker() firstTransferFromTaker IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG = 5n
6 0x0040 USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG useTransferFromAndAquaPush / useTransferFromAndAquaPush() useTransferFromAndAquaPush USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG = 6n

Bits 7–15 are unused at v1.0.1, and there is no IS_A_TO_B flag. TakerTraitsLib defines exactly the seven constants above (masks 0x00010x0040) and the SDK defines exactly the seven matching bit indices (0n6n). Neither side defines a bit-7 IS_A_TO_B_BIT_FLAG in the deployed protocol. A decoder must treat bits 7 through 15 of the flag word as reserved zero and must not attribute swap direction to any TakerTraits bit — direction is determined by the order's token layout, not by a taker flag.

Header slice fields (SDK constructor field → Solidity slice)

The non-flag header fields set the ten packed slicesIndexes that delimit the tail. Each SDK constructor field maps one-to-one to a TakerTraitsLib.Args field and to one TakerDataSlices entry. The SDK omits a slice (contributes zero length, leaving the boundary equal to the previous index) under the same emptiness rule the Solidity build uses.

SDK field (TakerTraits) Solidity Args field TakerDataSlices Slice idx Emitted when Fixed width
threshold (bigint) threshold Threshold 0 threshold > 0n 0 or 32 bytes
customReceiver (Address) to To 1 non-zero (Solidity also drops when to == taker) 0 or 20 bytes
deadline (bigint) deadline Deadline 2 deadline > 0n 0 or 5 bytes (uint40)
preTransferInHookData preTransferInHookData PreTransferInHook 3 non-empty variable
postTransferInHookData postTransferInHookData PostTransferInHook 4 non-empty variable
preTransferOutHookData preTransferOutHookData PreTransferOutHook 5 non-empty variable
postTransferOutHookData postTransferOutHookData PostTransferOutHook 6 non-empty variable
preTransferInCallbackData preTransferInCallbackData PreTransferInCallback 7 non-empty variable
preTransferOutCallbackData preTransferOutCallbackData PreTransferOutCallback 8 non-empty variable
instructionsArgs instructionsArgs InstructionsArgs 9 non-empty variable
signature signature Signature non-empty tail remainder (no packed index)

Two callback flags are coupled to their data on both sides. In the SDK, encode() sets bit 2 when preTransferInCallbackEnabled || preTransferInCallbackData is non-empty, and bit 3 likewise for the out-callback — so supplying callback data auto-raises the flag. In Solidity, build does not auto-raise it: it reverts (TakerTraitsMissingHasPreTransferInFlag / ...OutFlag) if callback data is present while hasPreTransferInCallback / hasPreTransferOutCallback is false. Set the flag explicitly when hand-building taker data for the contract.

SDK defaults (TakerTraits.new() / TakerTraits.default()): exactIn = true and useTransferFromAndAquaPush = true; every other flag defaults to false and every data field defaults to empty. Because customReceiver is dropped only when it is the zero address (whereas Solidity also drops it when it equals the taker), a header built on-chain with to == taker decodes in the SDK to customReceiver = Address.ZERO_ADDRESS — both mean "taker receives", so the round-trip is semantically stable.

Did you find what you need?