Program model

A Program is an ordered sequence of instructions encoded as bytecode and executed on-chain when a taker calls swap() or quote(). The Program lives inside a SwapVM Order, the container that holds the Program together with maker metadata and authorization headers. Once ABI-encoded, that Order is what Aqua registers as a Strategy.

Program (opcodes + args)  →  SwapVM Order  →  Aqua Strategy (ABI-encoded)

Aqua treats the Strategy body as opaque bytes; it does not interpret instructions or execute pricing logic. AquaSwapVMRouter executes the Program instructions at swap time.


SwapRegisters

Every instruction reads from and writes to the same five registers:

Register Type Who sets it Meaning
balanceIn uint256 Balance instruction Maker's available input token reserve
balanceOut uint256 Balance instruction Maker's available output token reserve
amountIn uint256 Taker (exactIn) or swap formula Input amount for this swap
amountOut uint256 Taker (exactOut) or swap formula Output amount for this swap
amountNetPulled uint256 Fee instructions Cumulative amount pulled from maker for fees

The taker sets exactly one of amountIn or amountOut (isExactIn flag). The program computes the other. Instructions then adjust both.


Bytecode format

Each instruction in the program is encoded as:

[opcode_index : 1 byte][args_length : 1 byte][args_data : N bytes]

Instructions are executed sequentially. Some instructions (fees, decay) call ctx.runLoop() internally: they invoke all subsequent instructions as a nested sub-program, then adjust the returned amounts. This is how wrapping works.

Instruction ordering is security-critical. Reordering changes pricing, settlement amounts, and invalidation behavior. Fee placement in particular changes which leg the fee applies to and its economic meaning.


Two balance archetypes

Static balances

Currently not supported

Only dynamic balances are currently supported.

_staticBalancesXD → [pricing] → [invalidation]
  • Balance values are embedded in the program args and never change

  • Single direction only (e.g. always sell tokenA for tokenB)

  • No on-chain state read or write for balances

  • Used for: limit orders, Dutch auctions, TWAP, RFQ, DCA, range orders

Dynamic balances

_dynamicBalancesXD → [fee] → [swap formula]
  • Balances are loaded from SwapVM storage keyed by (orderHash, token)

  • After each swap, consumed amounts are written back: balanceIn += swapAmountIn, balanceOut -= swapAmountOut

  • Bidirectional: taker can swap either direction

  • Used for: constant product AMMs, concentrated liquidity, pegged swaps


Three settlement paths

Path Setup Authorization Balance source
Signature + Static (not currently supported) Sign order off-chain EIP-712 signature Embedded in program args
Signature + Dynamic Sign order off-chain EIP-712 signature SwapVM storage (per-maker isolation)
Aqua aqua.ship(token, amount) on-chain Aqua balance check Aqua shared liquidity layer

Set useAquaInsteadOfSignature = true in MakerTraits for the Aqua path. The program bytecode (instructions, args) is identical; only settlement and authorization differ.

On the Aqua path, liquidity is self-custodial. The maker's tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance, and Aqua tracks only a virtual balance in Aqua.sol. Tokens move only when a taker fills atomically, and the protocol itself holds no tokens. Smart-contract and approval risk still apply.


Instruction ordering rules

  1. Balance setup first. _staticBalancesXD or _dynamicBalancesXD must be the first instruction that sets reserves. Nothing can use balanceIn/balanceOut before they are set.

  2. Wrapping instructions before the formula. Fee instructions (_flatFeeAmountInXD, etc.) and _decayXD call ctx.runLoop() to invoke subsequent instructions, so place them before the swap formula so the formula runs inside the nested loop.

  3. Post-swap adjusters after the formula. _baseFeeAdjuster1D and _oraclePriceAdjuster1D require amountIn and amountOut to already be set, so place them after the swap formula.

  4. Invalidators after amounts are computed. _invalidateTokenIn1D and _invalidateTokenOut1D need the relevant amount to be non-zero.

  5. Control flow anywhere, but test all paths. Jump instructions can appear anywhere but introduce execution paths that must each satisfy invariants independently.


Core invariants

Every program must maintain these properties:

Invariant Meaning
Exact in/out symmetry exactIn(X) → Y implies exactOut(Y) → X within rounding tolerance
Monotonicity Larger swaps get equal or worse prices (price cannot improve with size)
Rounding favors maker amountIn rounds up (ceil), amountOut rounds down (floor)
Balance sufficiency amountOut <= balanceOut must hold; revert otherwise
Quote/swap consistency quote() and swap() must produce identical amounts for the same inputs
Strategy liveness When one reserve is depleted, reverse-direction swaps should restore it

Did you find what you need?