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.
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.
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()andswap()must return the same amounts.Rounding:
amountInrounds up (ceil),amountOutrounds down (floor); the maker is never short.No replay: the bitmap invalidator blocks re-execution; the token-out invalidator blocks overfill.
Balance sufficiency:
amountOut <= balanceOutmust hold; revert otherwise.
Related
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