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
12
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_FLAG — unreleased 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
12345
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:
Reads
makerfrom the low 20 bytes of[0x00:0x20].Reads
traitsfrom[0x20:0x40]and decodes the MakerTraits bit map (above) — in particularindex3at bits 223–208, which is the program start offset withindata.Reads
len = data.lengthfrom[0x60:0x80]and takesdata = strategy[0x80 : 0x80 + len].Within
data(no token prefix at v1.0.1): the SwapVM program bytecode tail =data[index3 : len]; with no hooksindex3 = 0, so the program is all ofdata. The token pair is not indata— it is supplied as thetokenIn/tokenOutarguments toquote/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.sol—MakerTraits,MakerTraitsLib(flag constants,build,_getOffset,OrderDataSlices)swap-vm/src/libs/TakerTraits.sol—TakerTraits,TakerTraitsLib(flag constants,build,parse,TakerDataSlices,_getStartOffset/_getStopOffset)swap-vm/src/interfaces/ISwapVM.sol—ISwapVM.Orderswap-vm/src/SwapVM.sol—hash()(keccak256(abi.encode(order))for Aqua orders),ORDER_TYPEHASHswap-vm/test/base/AquaStrategyBuilders.sol—abi.encode(order)shipped as the strategyaqua/src/interfaces/IAqua.sol—Shippedevent,rawBalancesaqua/src/Aqua.sol—ship/dock/push/safeBalances,_DOCKEDaqua/src/libs/Balance.sol—Balancestruct,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 0x0001–0x0040) and the SDK defines exactly the seven matching bit indices (0n–6n). 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.