Limit order

Limit Order is a fixed-rate, single-direction swap program. The maker embeds a price ratio as static balances, and takers fill at exactly that rate. The program supports one-shot execution (bitmap invalidator) or partial fills up to a cap (token-out invalidator).

Limit Order runs on the 1inch Limit Order Protocol router, not the Aqua router. It shares one backend with Fusion; the two differ only by Fusion's Dutch-auction price modifier. The order itself is a SwapVM instruction program that composes Balances (static), then LimitSwap, then an Invalidator.

It is self-custodial. The maker's tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance, the protocol holds no tokens, and a fill moves tokens atomically at the fixed rate.


Core instructions

Instruction Role
_staticBalancesXD Embed fixed balanceIn / balanceOut price ratio
_limitSwap1D Compute amountIn / amountOut at the fixed rate
_limitSwapOnlyFull1D Same, but reverts on partial fills

Program A: one-time execution (bitmap invalidator)

Fills at most once. The bitmap slot 123 is flipped atomically with the swap; a second call reverts.

Solidity
1
2
3
4
5
6
7
8
9
10
Program memory program = ProgramBuilder.init(_opcodes());
bytes memory bytecode = bytes.concat(
    program.build(_invalidateBit1D, InvalidatorsArgsBuilder.buildInvalidateBit(123)),
    program.build(_staticBalancesXD, BalancesArgsBuilder.build(
        dynamic([tokenA, tokenB]),
        dynamic([uint256(1000e18), uint256(2000e18)])
    )),
    program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenA, tokenB))
);

Why _invalidateBit1D first: it must run before amounts are computed; it writes state at the start so a swap that later reverts still flips the bit (preventing replay on retry).


Program B: partial fills (token-out invalidator)

Multiple takers can fill against the same order until balanceOut is exhausted. Each fill decrements the on-chain counter.

Solidity
1
2
3
4
5
6
7
8
9
10
Program memory program = ProgramBuilder.init(_opcodes());
bytes memory bytecode = bytes.concat(
    program.build(_staticBalancesXD, BalancesArgsBuilder.build(
        dynamic([tokenA, tokenB]),
        dynamic([uint256(1000e18), uint256(2000e18)])
    )),
    program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenA, tokenB)),
    program.build(_invalidateTokenOut1D)
);

Why _invalidateTokenOut1D last: it needs amountOut > 0, which _limitSwap1D sets. It reverts if the cumulative fill would exceed balanceOut.


Optional modifiers

Add modifiers without changing the core swap formula. Placement matters; follow the ordering shown in the table below.

Goal Add
Expiry _deadline before balance setup
Maker fee _flatFeeAmountInXD before _limitSwap1D, wrapping it
Gas compensation _baseFeeAdjuster1D after _limitSwap1D
Oracle anchor _oraclePriceAdjuster1D after _limitSwap1D
Hard price floor _requireMinRate1D after _limitSwap1D
Holder-only fills _onlyTakerTokenBalanceNonZero before balance setup

Invariants to verify

  • Quote/swap consistency: quote() and swap() must return the same amounts.

  • Rounding: amountIn rounds up (ceil), amountOut rounds down (floor); the maker is never short.

  • No replay: the bitmap invalidator blocks re-execution; the token-out invalidator blocks overfill.

  • Balance sufficiency: amountOut <= balanceOut must hold; revert otherwise.


  • LimitSwap: instruction args and behavior

  • Invalidators: bitmap and token-amount invalidation

  • Dutch Auction: the same program plus a time-decaying price modifier, which is what turns a limit order into a Fusion order

  • RFQ: off-chain quoted price using the same instruction set

Did you find what you need?