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 -= swapAmountOutBidirectional: 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
Balance setup first.
_staticBalancesXDor_dynamicBalancesXDmust be the first instruction that sets reserves. Nothing can usebalanceIn/balanceOutbefore they are set.Wrapping instructions before the formula. Fee instructions (
_flatFeeAmountInXD, etc.) and_decayXDcallctx.runLoop()to invoke subsequent instructions, so place them before the swap formula so the formula runs inside the nested loop.Post-swap adjusters after the formula.
_baseFeeAdjuster1Dand_oraclePriceAdjuster1DrequireamountInandamountOutto already be set, so place them after the swap formula.Invalidators after amounts are computed.
_invalidateTokenIn1Dand_invalidateTokenOut1Dneed the relevant amount to be non-zero.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 |
Related
Program Order Strategy: the SwapVM container that wraps a Program and becomes an Aqua Strategy
SwapVM Instructions: full instruction reference
Modifiers: how to add fees, MEV protection, and rate guards to any program
Patterns & decision tree: pick a strategy pattern