Documents

How Ilyris works, and the addresses to verify it against. Everything below is readable on-chain — nothing here asks you to take our word for it.

Bins, and why they exist

Liquidity sits in discrete price steps rather than along a continuous curve. Each bin holds reserves at one fixed price, so a trade that stays inside a bin has zero price impact — the price does not move because there is no curve to move along. Price only changes when a trade exhausts a bin and crosses into the next.

Bin n sits at price = (1 + binStep/10000)^n. Bin step is chosen per pool, not by the protocol: WETH/USDG uses 10 bps (each step 0.10%), WETH/YRIS uses 250 bps (each step 2.50%). Discover shows each pool's. The base token sits at and above the active bin, the quote token at and below — and which token is which is decided by address order, not by what you would call the pair.

Fees

The swap fee is charged once on the way in, before any bin is touched: netIn = amountIn × (1e9 − rate) / 1e9. Charging per bin would compound it across a multi-bin trade. The fee is per pool: WETH/USDG charges 30 bps, WETH/YRIS 100 bps. Discover lists each pool's base fee. The rate a swap actually pays can be higher than the base — a volatility surcharge is added on top after recent price movement.

A deposit that arrives imbalanced relative to the bin it lands in also pays a composition fee, on the imbalance only. Without it, depositing one-sided into a bin and immediately withdrawing would be a free swap at the bin price, paid for by the LPs already there.

Fees accrue per position through a share accumulator and are claimed separately, so claiming never touches principal. The protocol's share is skimmed before anything reaches LPs and is held outside bin reserves — the two can never draw on each other.

The market guard

Equity pools must respect a closed market. The guard can pause swaps and enforce a corporate-action freeze — and it makes no external calls, so a quote can never be invalidated by an oracle nobody can see.

It cannot block a withdrawal. That is structural, not policy: the guard is not consulted on the removal path at all. Whatever else happens, liquidity can leave.

⚠ Integrators: quoteExactIn and quoteExactOut do not consult the guard — only the swap functions do. A simulator built from quotes alone will happily price a swap that reverts. Read swapsPaused() and freezeEnd() alongside pool state.

Reading the app

Switching pools. The pair name at the top left is a menu listing every pool the factory has recorded. Deprecated pools are tagged; permanently mispriced ones are not offered at all.

Which way the price runs. A pool stores one price — quote token per base token — and the factory picks which is which by address order, so on some pairs the native number reads backwards. The ⇄ control beside the range, the position book and the liquidity chart flips the display; it is a labelling change only, remembered per pool. Ranges are measured in bins, so flipping cannot alter which bins a deposit touches. Bar colour always names a token and never moves; bid/ask follows the direction you are reading.

Creating a pool. The starting price is the one number that cannot be fixed later: activeId is immutable outside of swaps, and the factory key is (tokenX, tokenY, bin step, policy, guard) — so a pool seeded at the wrong price permanently burns that combination and it can never be created again. The create form therefore states the consequence as a sentence with no units to misread — "1 WETH = 24,038,448 YRIS" — and will not submit until you confirm it.

Mainnet addresses (chain 4663)

Verify on Blockscout. Discover lists every pool the factory has recorded, read live from the chain.

Factory0x3Bf76F2E41Ac7996c822455f4c78fa2026465C4D
Pool — WETH/USDG · 10 bps bin · 0.30% fee0xeEc7cdd6834b4f1feA1D63D32eE0cB7CbB3f24D7
Pool — WETH/YRIS · 250 bps bin · 1.00% fee0xcbb584e1c1259047f2fe03ccb34e8e2086c9f8ad
Lens0x306d65eA97421D33923cCe592dBd22792a789090
Planner0xB5c3f76209f3AfC00e0fe83857fC024BC8DB00ed
Market guard0xa984e5c16EBE9fa50bA41Ef5F823AF4f79FA37e5
Fee staking · rewards in WETH (deployed, NOT funded)0x868ae20E6c1EA3b6Fdab5042Ea721eB51b237183

Superseded, still on chain. These are listed so an LP with funds in one can find it, not because anything should be deposited into them. The factory was replaced; its pools remain withdrawable and are tagged deprecated in Discover.

Factory (legacy)0x4A943A11a6fFBF8D204Df4d5A080Ca741697ca33
Pool — WETH/USDG (deprecated)0x90D0950065C567B9324A08A9AaE8a28890fBab16

For integrators

BinPoolLens.getPoolState(pool, radius) is one-call discovery: tokens, decimals, bin step, active bin and its price, the resolved fee rate (totalFeeRate, 1e9 precision, volatility surcharge already applied) and the guard address. MAX_SCAN is 512 and the check is radius * 2 + 1 > MAX_SCAN, so the usable radius is 0–255 and 256 reverts ScanTooWide().

It does not price a swap, and its reserves are not the pool's reserves. reserveX/reserveY are summed over activeId ± radius only; scannedFrom, scannedTo and populatedBins say what was counted, and liquidity outside that window is invisible with no error and no flag. Measured on WETH/USDG: at radius = 10 the lens reports 0.00717 WETH, while a single 39 USDG swap pays out 0.01582 WETH and ends 14 bins beyond scannedTo. The swap's own output is more than twice what the lens called the reserves. If populatedBins equals the window width, assume you are clipping and widen.

Pricing needs the bins. A swap walks bin by bin and spends each bin's own reserve at that bin's own price, so no formula over an aggregate reproduces it. Two honest routes: getBins(pool, from, to) returns every populated bin, or quoteExactInWithGas / quoteExactOutWithGas return amountOut, feeAmount, finalId, binsCrossed and a gas estimate in one call. Both cap their scan at MAX_SCAN and revert rather than truncate silently.

A pool may have no guard. marketGuard is the zero address on WETH/YRIS, and an eth_call to an address with no code succeeds with empty return data rather than reverting. Decode that as "cannot be paused", not as a failed read — a bare 0x will otherwise reach your decoder and throw, which is exactly how it broke ours.

Swap the pool directly. No router is deployed on chain 4663 and the manifest has no router entry — a deployment fact, not an architectural one. BinRouter.sol exists in the repo and is deployed on testnet, so do not carry that address, or the TypeScript SDK's buildRouterExactInputCall, into a mainnet integration. There is no Permit2, no permit and no swap callback, so there is no shared allowance target: approve each pool you trade and call swapExactIn / swapExactOut on it.

The pool pulls, and only pulls. It takes the input from msg.sender via transferFrom and never reads its own balance for credit. A V2-style transfer-then-swap therefore fails — but it fails because you no longer hold the tokens you must approve, not because the pre-transfer was detected. Tokens sent to a pool are never credited and never recoverable: there is no skim, sync or rescue path. Settlement is exact-delta on both legs, so fee-on-transfer and rebasing tokens revert TransferFailed(). For exact-output, approve maxAmountIn — only the actual amountIn is pulled.

Gas hints are a routing input, not a limit. BASE_SWAP_GAS (130,000) and PER_EXTRA_BIN_GAS (41,500) are readable on the lens and applied by estimateSwapGas(binsCrossed) — but getPoolState does not return binsCrossed, so that number comes from quoteExactInWithGas or your own walk. The figure is execution gas only: it excludes the 21,000 intrinsic, calldata, cold-account access and the guard's staticcall, and on a guarded pool it currently understates — a real five-bin WETH/USDG swap used 372,159 against a 296,000 hint. Rank routes with it; never send it as a gas limit. Call eth_estimateGas.

HTTP quoting API

Most aggregators do not need this — 0x, KyberSwap and ParaSwap all read the chain directly, and the section above is what they build against. It exists for integrators whose pipeline requires an HTTP quote source rather than a contract call.

Base URL https://ilyris-quote-api.ilyris.workers.dev — no API key, no auth, CORS open. Chain 4663 only.

GET /healthliveness plus the number of routable pools
GET /poolsevery routable pool with its tokens, bin step, fee and guard
GET /quotetokenIn, tokenOut, amountIn (wei, decimal string)

A quote returns amountOut, feeAmount, binsCrossed, gasEstimate, the pool to approve and call, and the guard state. It is quoteExactInWithGas on the lens — the same call our own front end prices with — so it agrees with the chain to the wei.

Two pools are deliberately excluded from /pools and from routing. They were created at an inverted starting price, hold nothing, and anything deposited into them is lost on the first trade. Do not route to them if you find them by enumerating the factory yourself.

Errors say what was wrong with the request. A size the book cannot fill returns 422 insufficient_liquidity rather than a raw revert string — on this lens a quote makes no external calls and does not consult the guard, so depth is essentially the only way it fails. An unknown pair returns 404 no_pool; a malformed address returns 400 naming the field.

Rate: provisioned for 100 req/s per client, and measured well above it — 7,000 consecutive requests at 428 req/s drew zero throttling. A rate limiter exists as a backstop against abuse, but it is Cloudflare's per-colo limiter rather than a hard ceiling; when it does trigger you get 429 with a Retry-After header rather than a silent drop. Ask if you need more and it will be raised. Latency: about 0.2 s warm, up to ~1 s on a cold edge isolate.

If you get a 403 from a script, check your user-agent. The endpoint sits behind Cloudflare, whose bot rules reject Python-urllib with error 1010. Go, node-fetch, axios, python-requests, okhttp and curl all pass — this only bites Python's standard-library client, and it is not us refusing you.

⚠ A quote is not a guarantee, and the gas figure is not a limit. Quotes do not consult the market guard but swaps do, so check tradable in the response. gasEstimate is execution gas only and currently understates a guarded pool — rank routes with it and call eth_estimateGas for a limit.

Status

Unaudited. Extensively tested — a full Solidity suite, invariant tests that unwind every position to prove solvency, and wei-exact parity between the contract and the off-chain simulators — but tests are not an audit. Treat every deposit accordingly.

The source is the documentation for anything not covered here: the contracts and this interface are commented to explain why they are built the way they are, not just what they do.