Opcode gallery

This page is a directory of every opcode family registered on the deployed Aqua router, AquaSwapVMRouter v1.0.2 (0x111111338c5091e8440b67b168bae16a668ac0de). Each family maps to one instruction contract in 1inch/swap-vm. The router dispatches an opcode through _dispatch, which calls _runOpcode in AquaOpcodes.sol: the numeric index in the bytecode selects the instruction function directly.

An opcode is one byte in the program, immediately followed by a one-byte args length and then that many argument bytes. The dispatcher (AquaOpcodes._opcodes()) is a fixed jump table. Reserved gap indices (0–9 and 22–26) map to _notInstruction, an empty function — reaching one is a no-op (execution continues; it neither prices nor reverts). An opcode byte beyond the registered table reverts with a Solidity array-out-of-bounds panic (Panic(0x32)). There is no named UnknownOpcode/OpcodeNotFound error and no dynamic registration on the deployed v1.0.2 router.

Registered opcode summary

The table lists every opcode registered in the deployed _runOpcode dispatcher, grouped by instruction family. Indices are the exact numeric slots from AquaOpcodes.sol. The SDK builder column names the exported Opcode from @1inch/swap-vm (package path typescript/swap-vm/src/swap-vm/instructions).

Family Opcode index Function SDK builder Args (byte schema) Source
Controls 10 _jump jump nextPC 2 (uint16) src/instructions/Controls.sol
Controls 11 _jumpIfTokenIn jumpIfTokenIn token 20 + nextPC 2 src/instructions/Controls.sol
Controls 12 _jumpIfTokenOut jumpIfTokenOut token 20 + nextPC 2 src/instructions/Controls.sol
Controls 13 _deadline deadline deadline 5 (uint40) src/instructions/Controls.sol
Controls 14 _onlyTakerTokenBalanceNonZero onlyTakerTokenBalanceNonZero token 20 src/instructions/Controls.sol
Controls 15 _onlyTakerTokenBalanceGte onlyTakerTokenBalanceGte token 20 + minAmount 32 src/instructions/Controls.sol
Controls 16 _onlyTakerTokenSupplyShareGte onlyTakerTokenSupplyShareGte token 20 + minShareE18 8 (uint64) src/instructions/Controls.sol
Controls 20 _salt salt salt 8 (uint64); arbitrary bytes also accepted src/instructions/Controls.sol
Controls 33 _onlyTxOriginTokenBalanceNonZero onlyTxOriginTokenBalanceNonZero token 20 src/instructions/Controls.sol
XYCSwap 17 _xycSwapXD xycSwapXD none (0 bytes) src/instructions/XYCSwap.sol
XYCConcentrate 18 _xycConcentrateGrowLiquidity2D concentrateGrowLiquidity2D sqrtPriceMin 32 + sqrtPriceMax 32 src/instructions/XYCConcentrate.sol
Decay 19 _decayXD decayXD period 2 (uint16) src/instructions/Decay.sol
Fee 21 _flatFeeAmountInXD flatFeeAmountInXD feeBps 4 (uint32) src/instructions/Fee.sol
Fee 27 _protocolFeeAmountInXD protocolFeeAmountInXD feeBps 4 + to 20 src/instructions/Fee.sol
Fee 28 _aquaProtocolFeeAmountInXD aquaProtocolFeeAmountInXD feeBps 4 + to 20 src/instructions/Fee.sol
Fee 29 _dynamicProtocolFeeAmountInXD dynamicProtocolFeeAmountInXD feeProvider 20 src/instructions/Fee.sol
Fee 30 _aquaDynamicProtocolFeeAmountInXD aquaDynamicProtocolFeeAmountInXD feeProvider 20 src/instructions/Fee.sol
PeggedSwap 31 _peggedSwapGrowPriceRange2D peggedSwapGrowPriceRange2D x0 32 + y0 32 + linearWidth 32 + rateLt 32 + rateGt 32 (160 total) src/instructions/PeggedSwap.sol
Extruction 32 _extruction extruction target 20 + extructionArgs N src/instructions/Extruction.sol

Opcodes outside this registered set are not executable on the deployed router: the reserved gaps (indices 0–9, 22–26) resolve to the no-op _notInstruction, and an index past the table reverts with an array-out-of-bounds panic (Panic(0x32)). This includes every experimental fee opcode in FeeExperimental.sol (for example flatFeeAmountOutXD, progressiveFeeInXD, protocolFeeAmountOutXD): those SDK builders exist for tooling but their opcodes are not wired into AquaSwapVMRouter v1.0.2.

Families

Controls

Control flow and access-guard instructions. They never compute swap amounts — they redirect execution (jumps) or revert on a failed state check. _jump, _jumpIfTokenIn and _jumpIfTokenOut set the next program counter (targets limited to uint16, 0–65,535). _deadline reverts once block.timestamp passes the encoded time. _onlyTakerTokenBalanceNonZero, _onlyTakerTokenBalanceGte and _onlyTakerTokenSupplyShareGte gate the fill on the taker's ERC-20 or NFT holdings; _onlyTxOriginTokenBalanceNonZero gates on tx.origin instead (a weaker check — see the source notes on delegation). _salt is a no-op that adds uniqueness to the order hash. Registered indices: 10–16, 20, 33. See the Controls instruction reference.

XYCSwap

Constant-product AMM (x × y = k). Index 17, function _xycSwapXD. Terminal swap formula that takes no args and requires both virtual balances to be non-zero; on exact-in it computes amountOut, on exact-out it computes amountIn with ceil-division to favor the maker. See the XYCSwap instruction reference.

XYCConcentrate

Concentrated-liquidity swap bounded by a sqrt price range (Uniswap-v3-style), for two tokens only. Index 18, function _xycConcentrateGrowLiquidity2D. Args are two uint256 sqrt-price bounds in 1e18 fixed point (sqrtPriceMin, sqrtPriceMax) where price P = tokenGt / tokenLt. The instruction recomputes liquidity L from the real balances each swap, derives virtual reserves, and performs a single constant-product step; fee reinvestment happens implicitly as real balances grow. See the XYCConcentrate instruction reference.

Decay

Mooniswap-style virtual-balance protection. Index 19, function _decayXD. Args are a uint16 period (2 bytes). It adjusts balanceIn/balanceOut by decaying offsets stored per order, token and direction, then runs the inner loop; after a real swap it records new offsets so a large swap temporarily worsens the rate and restores linearly over the decay period. Reads offset state in quote mode without updating it, so quote and swap can diverge if state changed between calls. See the Decay instruction reference.

Fee

Fee instructions applied to amountIn. All use the shared BPS scale where 1e9 = 100% and feeBps ≤ 1e9 (for example 0.30% = 3000000). _flatFeeAmountInXD (index 21) keeps the fee inside the pool — this is the only non-zero fee in v1. _protocolFeeAmountInXD (27) transfers the fee from the maker to a recipient via safeTransferFrom; _aquaProtocolFeeAmountInXD (28) pulls it from the maker's Aqua balance instead. _dynamicProtocolFeeAmountInXD (29) and _aquaDynamicProtocolFeeAmountInXD (30) read feeBps and recipient from an external provider via staticcall. Registered indices: 21, 27–30. See the Fee instruction reference.

PeggedSwap

Square-root linear curve for pegged assets (stablecoin, wrapped and LST/LRT pairs). Index 31, function _peggedSwapGrowPriceRange2D. Args are 160 bytes: five uint256 fields x0, y0, linearWidth, rateLt, rateGt (initial normalization reserves, the linear coefficient A scaled by 1e27, and per-token rate multipliers assigned by address order). The curve √(x/X₀) + √(y/Y₀) + A(x/X₀ + y/Y₀) = 1 + A is solved analytically at fixed curvature p = 0.5. See the PeggedSwap instruction reference.

Extruction

Delegation to external maker logic for advanced strategies. Index 32, function _extruction. Args are a 20-byte target address followed by N bytes of extructionArgs forwarded to the callee. In quote mode it calls IStaticExtruction.extruction (view); in swap mode IExtruction.extruction (state-modifying). The target returns an updated program counter, a chopped-length of consumed taker args, and updated swap registers. Targets must be deterministic and consistent across both interfaces or quote and swap will diverge. See the Extruction instruction reference.

Precision details

This section pins down four things that trip up program authors reading the summary table above: the exact byte offset of each argument field, how the SDK's raw-price helper maps onto the sqrt-price bytes the opcode actually stores, the difference between the decimal indices shown here and the hex bytes in a program, and why the flat-fee argument is called feeBps on-chain but built with FlatFeeArgs.fromBps in the SDK. Everything below is verified against 1inch/swap-vm at tag v1.0.1 and the matching @1inch/swap-vm TypeScript coders.

Argument byte offsets

Argument blobs are built with abi.encodePacked and read back with fixed slice(start, end) calls, so fields are concatenated in declaration order with no padding and no length prefixes between them. The one length byte that precedes the blob (see the info panel at the top of this page) is the total args length; individual fields are located purely by offset. The table gives the offset of every field for the opcodes where the layout is non-trivial (more than one field, or a field wider than the opcode's mnemonic implies).

Opcode Field Offset (bytes) Length Type
_jumpIfTokenIn (11) / _jumpIfTokenOut (12) token 0 20 address
nextPC 20 2 uint16
_onlyTakerTokenBalanceGte (15) token 0 20 address
minAmount 20 32 uint256
_onlyTakerTokenSupplyShareGte (16) token 0 20 address
minShareE18 20 8 uint64
_flatFeeAmountInXD (21) feeBps 0 4 uint32
_protocolFeeAmountInXD (27) / _aquaProtocolFeeAmountInXD (28) feeBps 0 4 uint32
to 4 20 address
_xycConcentrateGrowLiquidity2D (18) sqrtPriceMin 0 32 uint256
sqrtPriceMax 32 32 uint256
_peggedSwapGrowPriceRange2D (31) x0 0 32 uint256
y0 32 32 uint256
linearWidth 64 32 uint256
rateLt 96 32 uint256
rateGt 128 32 uint256

The guard opcodes all lead with the 20-byte token address and then append their value: _onlyTakerTokenBalanceGte reads minAmount as a full uint256 at offset 20 (total 52 bytes), while _onlyTakerTokenSupplyShareGte reads only an 8-byte minShareE18 at offset 20 (total 28 bytes). PeggedSwap requires the blob to be at least 160 bytes (5 × 32) and reverts PeggedSwapInvalidArgsLength otherwise; it then casts the calldata pointer directly onto the five fields with no copy. rateLt/rateGt are assigned to input/output by address order at runtime (tokenIn < tokenOut keeps them in order, otherwise they swap), which is why they are stored as an ordered pair rather than an in/out pair.

rawPrice → sqrtPrice bridge (XYCConcentrate)

The _xycConcentrateGrowLiquidity2D opcode stores sqrt prices, not prices: two uint256 fields, sqrtPriceMin and sqrtPriceMax, each 32 bytes in 1e18 fixed-point, where P = tokenGt / tokenLt (higher-address token over lower-address token). The Solidity builder XYCConcentrateArgsBuilder.build2D takes those sqrt values directly and requires 0 < sqrtPriceMin < sqrtPriceMax.

The SDK exposes two constructors on ConcentrateGrowLiquidity2DArgs so callers can supply whichever form they hold. If you already have sqrt values, use fromSqrtPrices; if you hold plain prices, use fromRawPrices, which does the sqrt conversion for you before encoding. The bridge is:

// raw price P in 1e18 fixed-point  ->  stored sqrt price in 1e18 fixed-point
sqrtPrice = bigintSqrt(rawPrice * 1e18)   // so (sqrtPrice / 1e18)^2 == rawPrice / 1e18

// @1inch/swap-vm
ConcentrateGrowLiquidity2DArgs.fromRawPrices(rawPriceMin, rawPriceMax)
// is equivalent to
ConcentrateGrowLiquidity2DArgs.fromSqrtPrices(
  bigintSqrt(rawPriceMin * 1e18n),
  bigintSqrt(rawPriceMax * 1e18n),
)

Both paths land in the same coder, which writes exactly addUint256(sqrtPriceMin) then addUint256(sqrtPriceMax) — the 64-byte layout in the offsets table above. The contract reads them back with slice(0, 32) and slice(32, 64).

Do not hand a raw price to fromSqrtPrices (or a sqrt price to fromRawPrices). Either mistake encodes valid-looking bytes that price a completely different range: the opcode never re-derives one form from the other, it trusts the stored sqrt values as-is when it recomputes liquidity L from real balances each swap.

Opcode radix: decimal here, hex in program bytes

Every opcode index in this gallery is written in decimal, matching the numeric slots in AquaOpcodes.sol. In an actual program each opcode is a single byte, so when you inspect or hand-assemble order.data (hooks || program) you will see the index in hex. Convert before you read raw bytes:

Decimal (gallery) Hex (program byte) Function
17 0x11 _xycSwapXD
18 0x12 _xycConcentrateGrowLiquidity2D
21 0x15 _flatFeeAmountInXD
27 0x1b _protocolFeeAmountInXD
31 0x1f _peggedSwapGrowPriceRange2D
32 0x20 _extruction
33 0x21 _onlyTxOriginTokenBalanceNonZero

So a byte reading 0x11 in the program is index 17, XYCSwap — not the decimal value 11 (which is 0x0b, _jumpIfTokenIn). The reserved gap indices are equally worth converting: decimal 0–9 is 0x000x09 and decimal 22–26 is 0x160x1a, all mapping to the no-op _notInstruction.

Fee argument naming: feeBps vs FlatFeeArgs.fromBps

The names collide but describe different scales. On-chain the field is called feeBps, yet it is not basis points: Fee.sol defines BPS = 1e9 and treats feeBps as a value on a 1e9 = 100% scale, requiring feeBps ≤ 1e9. The SDK stores that same 1e9-scale integer in the field FlatFeeArgs.fee (a uint32), while the factory FlatFeeArgs.fromBps takes real basis points (10,000 bps = 100%) and scales up by 100,000 to reach the on-chain units:

TypeScript
1
2
3
4
5
6
// @1inch/swap-vm — FlatFeeArgs
FlatFeeArgs.fromBps(30); // 30 bps = 0.30%  ->  fee = 30 * 100000     = 3_000_000
FlatFeeArgs.fromPercent(0.3); // 0.30%          ->  fromBps(0.3 * 100)    = 3_000_000
FlatFeeArgs.fromBps(10000); // 100%           ->  fee = 10000 * 100000  = 1_000_000_000 (1e9)
new FlatFeeArgs(3_000_000n); // raw 1e9-scale constructor, same as fromBps(30)

The coder encodes fee as a single uint32 (addUint32), which is the 4-byte feeBps field the opcode parses with parseFlatFee at offset 0. So the summary table's feeBps 4 (uint32) and the SDK's FlatFeeArgs.fromBps(...) agree on the bytes; only the input unit differs (1e9-scale on-chain and in the fee field, real bps at the fromBps boundary). The example in the Fee family note above — 0.30% = 3000000 — is exactly FlatFeeArgs.fromBps(30). The same 1e9 scale and the same fromBps/fromPercent convention apply to the protocol-fee opcodes (27–30), whose feeBps field shares the identical 4-byte uint32 layout.

Did you find what you need?