How Strikeline works under the hood
Strikeline is a replicating market maker (RMM-01) written as two custom 1inch SwapVM instructions. This page covers the program an offer ships, the maths, the solvency check, the fee and how to run it. Every claim links to the source.
On this page
01Architecture#
A maker approves the official Aqua registry (0x1111113CCf1426A8E30e2bfF5E005d929bF6a90a) and ships a strategy whose app is our router. Aqua stores virtual balances and publishes the program bytes; it never holds tokens. When a taker trades, the router runs the program and Aqua moves tokens straight from the maker's wallet to the taker.
The router is StrikelineRouter, a redeployed SwapVM that adds exactly two opcodes to _runOpcode. It keeps every official instruction except PeggedSwap, dropped to fit under EIP-170.
02The program#
Every offer is the same five-instruction program, built by buildLegProgram:
| Instruction | What it does |
|---|---|
Deadline | Stops the offer from trading after maturity plus a grace window. |
FeeProtocol | Takes 0.10% of the taker's input for the treasury and runs the rest on the net amount. |
Coverage | Runs the curve, then refuses any output the maker's wallet can't deliver. |
RmmSwap | Prices the trade on the RMM-01 curve at the current block time. |
Salt | A nonce, so two offers with the same terms get different hashes. |
The fee comes first so both the check and the curve see the net trade. Coverage wraps the curve and runs it with a nested ctx.runLoop(): clamping the balance before pricing would move the reserve point and change the price, not just the size.
03RmmSwap · opcode 0x55#
RmmSwap implements RMM-01, the covered-call curve from Angeris, Evans & Chitra (arXiv:2103.14769, arXiv:2111.13740):
X and Y are the risky and stable reserves, K the strike, σ the implied volatility, L the liquidity, and τ the time left in years, read from block.timestamp with a one-hour floor. Holding reserves on this curve is long spot and short a call at K.
Arguments · 62 bytes
[uint8 flags][uint64 sigmaWad][uint40 maturity][uint128 strikeWad]
[uint128 liquidityWad][uint64 rateRisky][uint64 rateStable]The spread that opens with time
Reserves are pinned to the curve when the offer ships. As τ shrinks the curve moves away from them, so a trade must first close that gap. A trade inside it reverts with RmmInsideSpread(shortfall), which carries the exact amount. That gap is the maker's premium, and the program has no fee inside the reserves because a fee there would push them off the curve and leak it.
Expiry and puts
At τ = 0 the Gaussian drops out and the curve becomes Y = K·(L − X), a constant-sum order at the strike, so assignment is an ordinary swap; the reverse direction reverts RmmSettlementOneWay. The same arguments are a cash-secured put when the reserves start in the stable token.
Φ and Φ⁻¹ are fixed-point (Solady), about 5e-12 relative error against a 50-digit reference. The guard band EPS = 2e-6 is sized from the measured 1.18e-6 round trip and always favours the maker.
04Coverage · opcode 0x93#
Aqua lets a maker over-allocate: ship() checks no balance and safeBalances() never looks at the wallet. Coverage closes the gap inside the call that prices the trade.
args: [uint8 flags][uint16 haircutBps] // 3 bytes
check: amountOut ≤ min(balanceOf(maker), allowance(maker, Aqua)) × (1 − haircut)
else: revert NotCovered(needed, free)Every offer reads the same wallet, so a fill on one immediately lowers what the others can deliver, in the same block, with no keeper or shared storage. It reverts rather than partially filling, and coverage() publishes the same bound for UIs and solvers.
05Protocol fee#
The fee is 1inch SwapVM's own FeeProtocol, charged on the taker's input: 10_000 in SwapVM units where 1e7 is 100%, so 0.10%. The receiver is protocolFeeReceiver from the deployment manifest, or the router owner.
Only the net input reaches the maker's reserves, and that net amount is exactly what RmmSwap priced, so the curve and the premium are unchanged. Coverage only adds output-side fees to its obligation, so the solvency check is unaffected. Details in PROTOCOL-FEE.md.
06Reading offers#
Aqua's Shipped event carries the full program, so an offer's strike, expiry, size and volatility are public. The app finds them with getLogs, decodes the program, then reads everything else in block-pinned multicalls so every figure on screen comes from the same block.
stableFor/riskyForquote the curve at any reserve point.bandForreturns the spread a trade must clear, which the premium chart draws.SurfaceLens.bookprices a whole book in one call; it is a separate contract so it costs the router nothing in size.
07Numbers and tests#
| Router runtime size | 23,851 B, 725 B under EIP-170 |
| Gas per fill | about 211k, around a cent on Base |
| Offline Foundry tests | 147 passing (make test) |
| Mainnet-fork tests | 11, real WETH/USDC through the official contracts (make test-fork) |
| Test | Proves |
|---|---|
test_Theta_DecayOpensASpread | Time alone opens a two-sided spread. |
test_Book_FillOnOneLegShrinksSiblingDepth | One fill lowers the other offers' depth. |
test_Book_WithoutCoverageTheDepthIsPhantom | Without Coverage, quoted depth isn't real. |
test_Expiry_SettlesAtStrikeOneWay | At expiry it settles at the strike, one way. |
test_Roll_MovesNoTokensAndCanRepeatParameters | Rolling an offer moves zero tokens. |
testFuzz_QuoteEqualsSwap | Quotes always match swaps. |
test_Fee_LeavesTheCurveAndPremiumUnchanged | The protocol fee doesn't touch the premium. |
The suites live in contracts/test/strikeline.
08Running it#
Locally
make install # once
make fork # terminal 1: anvil fork of Base, chain id 31337
make story-setup # deploy the router, fund demo wallets, freeze the state
make story-load # rewind to that state (about a second)
make story-1 # ship four demo offers from one wallet
make web # http://localhost:3000/appHosted demo
The live demo at strikeline-mu.vercel.app/app talks to a fork of Base mainnet (chain ID 31337) run by anvil on a Google Cloud VM. The site reaches it through /api/rpc, which forwards JSON-RPC and blocks the anvil_, evm_ and debug_ admin methods.
09Limits and risks#
- Short volatility. The position loses when realised volatility exceeds the σ it was written at.
- No up-front premium. The premium is only realised when a taker crosses the spread.
- Reverts, not partial fills. A taker asking for more than the wallet covers gets nothing.
- Withdrawable.
dockis instant, so a buyer can't rely on an offer like a listed option. - Approximated Φ. Dust-sized trades hit the guard band rather than an unbounded error.
- Demo chain. The hosted demo is a fork; nothing is deployed to a public network.
This is hackathon code and unaudited. Don't point it at real funds.