Skip to content

Repository files navigation

zFi

the first onchain superdapp

zRouter zQuoter zSwap source

Canonical deployments

Ethereum mainnet, all Etherscan-verified. Every address below was placed by SafeSummoner CREATE2 at 0x00000000004473e1f31C8266612e7FD5504e6f2a; salts and creation payloads live in deploy/, and node script/check-create2-artifacts.mjs reproduces each address from the committed source.

Orderbooks

address
Swapboard 0x000000dA7bb4B2A9E3e80e9A4D4157E26CA6189b escrowed peer-to-peer orders
Dutchboard 0x000000a213b430D14Bae6062c176289B05e04489 descending-price lots
Floorboard 0x00000080198137F790DA4C52bb902cf87c276748 ascending-price standing bids
SwapboardView 0x000000E0b25449F32f7D9259aC449bA88E78dFCE read-only lens over Swapboard v1/v2 + Dutchboard
FloorboardView 0x0000004E376e9dB5D9EC28E6711E1a64997C6ba7 read-only lens over Floorboard bids

A position on any board is an ERC-721 whose tokenURI is a self-contained SVG card, rendered onchain by an immutable contract the board deploys in its own constructor. Those renderers are CREATE-derived at nonce 1 of each board, so they carry no vanity prefix and are not deployed separately:

address
SwapboardMetadata 0xA94786d3Dfb08a661C28F00517c0cf98EfA93b9e
DutchboardMetadata 0xeC6d97A413f1e7268AeC383F501d6F9a7134176A
FloorboardMetadata 0x65c85E0ACfb3E1F3CF90F46b1155F24c8c509Bc8

Forwarders

address
Orderbol 0x000000fADa565c5608570a4F66Fb5E0bD08ef91B zRouter → Swapboard / Dutchboard
Swapbol 0x0000003069053df109F47acac630e03C77804AD8 zRouter → board fills, legacy and current
Cowol 0x0000003B59007E8aa43B0e508AfF8a304438333B CoW Protocol, ERC-1271
Swapbatch 0x0000005471EEF58dD16Aeccda21C37758E36a0b6 batch board fills paying native ETH

Swapbatch binds legacyBoard = address(0): no legacy board is reachable through it, so tokensOut must be empty and legacyBoardMode false on every call.

Reproducing an address

Every board pins max_optimizer_runs in foundry.toml — Dutchboard at 20, Swapboard, Floorboard and SwapboardView at 200 — because they do not otherwise fit EIP-170. A salt is only valid for initcode built at the pinned setting, so forge verify-contract needs --num-of-optimizations set to it explicitly; the default profile's 9,999,999 produces bytecode Etherscan will reject. The same applies to the metadata renderers, which compile at their parent board's setting rather than their own, because the board embeds their creation code.

zSwap: permanent onchain HTML dapp

zSwap.sol is a swap dapp whose entire UI — zSwap.html — is designed to live on Ethereum mainnet as contract code. No IPFS pin, no gateway server, no frontend build pipeline. As long as Ethereum produces blocks, the dapp resolves byte-identical forever.

Current v0.1 deployment flow: deploy the six generated HTML data contracts, then deploy zSwap(data1, data2, data3, data4) with those addresses. Record the final wrapper address here after deployment.

The permanent-HTML pattern

The HTML payload is installed as the runtime bytecode of six data contracts created before the wrapper. The wrapper keeps six immutable pointers (DATA1DATA6). At read time, html() copies all six chunks back with EXTCODECOPY into one ABI-encoded string return — any RPC client decodes it directly.

  • Why multiple data contracts? EIP-170 caps deployed code at 24,576 bytes. Splitting the page makes that limit apply per chunk instead of to the full dapp. The current payload is 126,674 bytes across 6 data contracts (21,113 / 21,113 / 21,113 / 21,113 / 21,113 / 21,109 bytes), with 20,782 bytes of 6-chunk headroom.
  • Why runtime bytecode instead of SSTORE? Code is cheaper to deploy than equivalent storage, and EXTCODECOPY reads the blob directly. Storage-backed HTML would pay 20k gas per 32-byte word at write time and multiple SLOADs on read.
  • Why immutable? Each chunk is deployed with a minimal data-contract init stub (PUSH2 <len> DUP1 PUSH1 0x0A PUSH0 CODECOPY PUSH0 RETURN | <payload>). The wrapper constructor rejects missing or duplicated chunks, then stores the addresses immutably. Nothing in the wrapper can mutate the response, which is why the dapp ships Cache-Control: public, max-age=31536000, immutable.

Browsing it

zSwap implements ERC-5219 and advertises resolution mode "5219" per ERC-4804, so any web3:// gateway serves it as a normal web page:

  • HTTP gateway: https://<zswap-address>.1.w3link.io/
  • Native web3://: wallets/browsers with web3:// protocol support (e.g. the Web3URL extension).
  • Raw RPC: cast call <zswap-address> "html()(string)" --rpc-url <rpc> > zSwap.html then open the file locally.

Path and query parameters are ignored — the contract is a single-page app served from any URL under its address.

What the dapp does

Once loaded, zSwap.html is a self-contained swap UI that:

  • Connects an injected wallet (MetaMask, Rabby, etc.) on Ethereum mainnet.
  • Quotes via zQuoter across Uniswap V2/V3/V4, Sushi, Curve, Lido, and zAMM.
  • Compares those AMM routes with live current Swapboard, legacy Swapboard v1, and Dutchboard liquidity, then composes a better exact-in or exact-out split when one exists.
  • Routes the swap through zRouter, handling ERC-20 approvals and native ETH.
  • Keeps split routes atomic for ETH and ERC-20 input, with EIP-2612, Permit2, and EIP-5792 wallet batching available through the same swap button.
  • Places fixed-price or linearly decaying Dutch orders, and fills public fungible orders, through the same permit → Permit2 → existing allowance → atomic wallet batch → legacy approval funding paths.
  • Pins AMM and book reads to one block, re-quotes the AMM remainder after allocating book liquidity, and accepts a hybrid only when its expected improvement clears a gas-aware threshold.
  • Bounds order discovery and planning work, and discloses Book scan capped or Planner heuristic/capped whenever the displayed result may omit a better combination.
  • Displays the chosen source DEX, effective rate, and Min received / Max paid under the user's slippage setting.
  • Sends native ETH or ERC-20s directly, with recipient resolution for 0x, .eth, .wei, and .gwei.
  • Manages SLOW time-locked sends: deposit with a delay, reverse before maturity, and claim when ready.

No JavaScript bundler, no external asset loads at runtime — icons are inlined SVG and the script speaks JSON-RPC directly to the injected provider.

Routed order ownership

Swapboard and Dutchboard each expose a regular creation function that delegates to an internal helper with msg.sender, plus a ...For(maker) variant. The For caller supplies the complete escrow, while the named maker owns cancellation, returned escrow, and fill proceeds. No EIP-712/ERC-1271 maker authorization or nonce is needed for that boundary: naming another address cannot debit it and can only give it a funded order. A signature becomes necessary if a future path pulls the named maker's assets, reimburses a sponsor, or executes reusable offchain instructions. Accordingly, an indexed maker field proves who owns the funded order, not that the address requested or endorsed it; unsolicited sponsored orders are possible but cannot cost the named maker assets.

Dutchboard adds a third creation route for a single NFT: safeTransferFrom the token into the board with an ABI-encoded PushTerms — quote asset and decay curve — as the transfer's data. This exists to eliminate an authorization rather than a transaction. listNFT needs a per-id approve or, in practice, a blanket setApprovalForAll that leaves the board holding standing authority over every token in the collection long after the listing closes; a push grants nothing and hands over exactly one token. It is a ...For-shaped route in that the seller can differ from the caller, but safely so: from has provably parted with the token by the time the hook runs, so naming them seller is the only non-lossy choice. Because the hook can be called directly by anything claiming to be a collection, the board treats msg.sender as the only trustworthy source of the collection address and re-verifies that it actually holds the token before opening a listing — otherwise a caller transferring nothing could mint a listing against escrow already backing someone else's. Bundles still require listNFT, since safeTransferFrom delivers one token per call.

Orderbol connects that primitive to zRouter. Its deployment binds the reviewed current Swapboard and Dutchboard immutably; the ABI-compatible board argument must select one of those bindings. For ERC-20 orders, zRouter first calls checkpoint(token), then funds Orderbol and places the order through the same immediate executor. The transient, single-use checkpoint accepts exactly the post-checkpoint balance increase, so a later caller cannot convert donated or stranded tokens into an order they own. Orderbol returns and validates the created order/listing ID, rejects unrecoverable WETH refund destinations, and accepts an optional placement deadline. Native Swapboard orders instead bind the escrow to the exact msg.value; native Dutch sell orders are wrapped into canonical WETH before listing because Dutchboard escrows ERC-20 lots.

Swapbol applies the same checkpoint and per-call delta isolation to composed public fills. It grants each board or zRouter only an exact, call-scoped allowance, revokes it before returning, keeps output recipient separate from refundTo, and rejects zero/self destinations and ambiguous same-asset ETH/WETH routes. Private orders and NFTs remain direct-wallet fills because pretending that a router is an arbitrary taker would weaken Swapboard's counterparty == msg.sender authorization.

Native ETH, WETH, and Dutch liquidity

The UI presents ETH consistently while preserving each venue's settlement asset. Swapboard resting ETH is canonical WETH. For native input, Swapbol wraps only the exact amount consumed by each Swapboard leg; a Dutch leg is paid as literal ETH when its quote is address(0), or with freshly wrapped WETH when its quote is WETH. This makes WETH-quoted Dutch liquidity composable with an ETH-input route instead of silently dropping it. For native output, book legs deliver WETH to Swapbol, which unwraps only the current call's aggregate WETH increase and sends the resulting ETH to the recipient. Pre-existing WETH and forced ETH stay outside the route.

Makers can cancel a Swapboard WETH order or a fungible Dutchboard WETH listing back to native ETH through cancelOrderUnwrap and cancelUnwrap. Both redemption paths delete order state before external calls and verify exact WETH debit and ETH credit so one order cannot consume pooled wrapper escrow. Swapboard also refuses its own address or WETH as a maker/recipient; Dutchboard refuses zero/self fill recipients and exact-checks fungible transfers, while native-ETH sellers must accept ETH.

Dutchboard is intended for reviewed, standard ERC-20s and ERC-721s. Rebasing, reflection, taxed, upgradeable-with-confiscation, callback-dependent, and otherwise non-standard token contracts are unsupported: exact transfer deltas cannot protect pooled escrow from a later balance or ownership change. Partial fills round each costOf result upward independently, so splitting a low-decimal lot can cost more than one larger fill; endPrice == 0 is an intentional free terminal price, and listings remain live there until cancelled.

Bounded book planning and display safety

SwapboardView caps planning at 256 candidates, removes all-or-nothing rows that cannot fit the current request before that cap, and bounds token metadata calls by gas and returndata size. Invalid or hostile symbol() / decimals() responses fall back to a blank symbol and 18 decimals. The browser decoder independently validates every dynamic offset and string length, then strips markup-sensitive characters before order metadata reaches HTML.

The in-page planner compares rate-greedy execution, every single all-or-nothing seed, and every pair among the first 24 all-or-nothing candidates. It returns at most 32 book legs and re-quotes the remaining AMM amount for up to two rounds. This materially improves mixed AON selection without pretending to solve an unbounded knapsack: candidate, leg, or higher-order AON limits set the visible Planner heuristic/capped warning.

Regenerating the payload

zSwap.html at the repo root is the canonical source. To rebuild the Solidity payload after editing it:

node script/build-zSwap.mjs
node script/build-zSwap-chunks.mjs
forge test --match-path test/zSwap.t.sol

build-zSwap.mjs refreshes size natspec and the canonical source comment at the bottom of zSwap.sol. build-zSwap-chunks.mjs writes out/zSwap.chunk1.creation.txt through out/zSwap.chunk4.creation.txt; deploy those creation payloads first, then deploy zSwap with the resulting chunk addresses as constructor args.

Compiler pin: Foundry uses Solidity 0.8.36 with via_ir = true and optimizer runs 9_999_999. The zQuoter extraction script also uses 0.8.36, but keeps the low-runs/yul-disabled recipe needed to stay under EIP-170.

TokenList: the token registry as an NFT collection

TokenList · TokenListRenderer · source

An onchain token list where each listing is an ERC-721. The card for a token on this chain is minted to the token's own address, so anyone looking at that contract on Etherscan sees a listing carrying its logo, symbol, decimals and links. Minting lists, burning delists, re-minting relists. Eleven entries are live.

Curation is the only power the owner holds over facts. For local tokens name/symbol/decimals are read from the token contract itself and can never be typed in by the owner; sync is permissionless, so the owner is never the reason a stale name persists. The owner authors only what has no onchain source — logo, theme, links, and a sort weight.

Weight is a sort key, not a position

rank is a weight: higher sorts first, 0 is unranked. The card prints WEIGHT, not RANK, because a renderer is handed one listing and cannot know an ordinal even in principle — position is the rankedIds() index. Seeded weights step by 1,000 so a token can be placed between two existing ones by picking a number in the gap, instead of renumbering everything below it.

Two orderings, chosen by the reader

ZorgConviction layers conviction voting on top: zOrg holders bond shares to a listing id and support accrues on an exponential half-life. It never writes to the registry — no setRank, no permission over it — and ZorgTokenListLens can only ever return ids the registry already lists. Conviction permutes the curated set; it cannot add to it or surface something delisted.

So the same registry serves both, and each consumer picks: tokenList.rankedIds() for the curator's order, or the lens for the earned order with the curator's as the tie-break. On an unvoted list the two are identical.

Why display text loses its apostrophes

Text sourced from third-party token contracts is sanitised at the storage boundary with an allowlist: printable ASCII minus the characters that can terminate a string or an element in the two documents the registry emits.

The filter drops rather than escapes, because the same text is rendered into both XML and JSON and correct escaping differs between them — &amp; is right for the SVG and wrong for the JSON, so no single escaping pass is correct for both. Dropping produces one output valid in both, unconditionally.

Because the registry is immutable and its renderer is replaceable, sanitisation is deliberately stricter than any single renderer requires: the guarantee has to hold for renderers that do not exist yet. The current renderer puts curated text only in text nodes, where an apostrophe is legal — but it delimits its attributes with single quotes, and storage outlives renderers.

The cost is typographic. Circle's stablecoin stores and renders as Circles stablecoin, and the same applies to a token whose own name() contains one. We took that over carrying an escaping bug we could never fix. It is deterministic, identical for every reader, and there is no ambiguity or collision — only missing punctuation.

Precision DeFi

Instead of building a generalized AMM that handles arbitrary token pairs — a singleton (Uniswap V4, Ekubo) or a factory (Uniswap V2/V3, Curve) — we build custom pool contracts for specific pairs. Everything the pool will ever need is known at compile time: token addresses, decimals, fee, and curve parameters are constants, not storage. No tick math, no bitmap traversal, no pool key lookups, no hook dispatch, no factory overhead.

Three pool archetypes:

PrecisionStablePool (USDT/USDC)

Hardcoded stableswap using Curve's invariant (A=2000), simplified for exactly 2 tokens with identical decimals. The curve keeps reserves balanced under arbitrage pressure while remaining nearly flat for normal-sized trades.

PrecisionRangePool (ETH/USDC $2200-$3000) — deprecated

Superseded by PrecisionPool, which is this design with the pair, band and fee as constructor parameters, behind a CREATE2 factory with an on-chain registry and a lens. The AMM step is unchanged. Retained as a gas baseline; not for deployment. See the contract header for the two behavioural differences and the overflow bound that motivated generalising it.

Concentrated constant-product pool with a hardcoded price range. The range is baked in as virtual reserve offsets — the core AMM step is a single multiplication and division, with no traversal loops, ticks, or bitmaps. Uses native ETH (not WETH).

Separate pools cover different ranges. zRouter/zQuoter queries all and routes to whichever covers the current price:

PrecisionRangePool_2200_3000  ← active when ETH is $2200-$3000
PrecisionRangePool_3000_4000  ← active when ETH is $3000-$4000
  • Fee: 500 pips (0.05% / 5 bps) — matches Uniswap V3's most popular ETH/USDC tier
  • LP revenue: 100% to LPs, no protocol fee
  • Integration: EIP-7702 batch wallet, zRouter snwap, or multisig executeBatch
  • Note: retained swap fees cause gradual range drift — redeploy fresh pools to recalibrate

PrecisionOraclePool (ETH/USDC)

No AMM curve. Chainlink ETH/USD sets the price directly — swaps execute at the oracle price ± a dynamic fee. Zero price impact at any size: a $10M swap gets the same rate per unit as a $100 swap.

The dynamic fee ramps linearly from 1 bps (oracle just updated) to 50 bps (at the 1-hour heartbeat limit), matching Chainlink's 0.5% deviation threshold. When the oracle price changes, the first swap pays max fee — blocking sandwich attacks around oracle update transactions. Uses native ETH.

This design eliminates curve-based LVR (loss-versus-rebalancing), the dominant source of LP loss on Uniswap V3 ETH/USDC. Residual adverse selection from oracle lag is bounded by the deviation threshold and mitigated by the dynamic fee. LPs earn fee revenue on every trade without being systematically arbed through a bonding curve. Prior art: DODO's PMM uses oracle-priced pools with configurable parameters in storage.

Why this only works as a precision pool: the oracle address, deviation threshold, heartbeat, and decimal conversion (ETH 18 / oracle 8 / USDC 6) are all compile-time constants. The dynamic fee is calibrated to the specific feed's parameters. A generalized AMM can't embed feed-specific risk parameters per pair.

  • Fee: 100–5000 pips (1–50 bps) — dynamic, based on oracle freshness
  • LP revenue: 100% to LPs, no protocol fee
  • Integration: EIP-7702 batch wallet, zRouter snwap, or multisig executeBatch

Gas Benchmarks

All numbers measured via Foundry fork tests on Ethereum mainnet.

Pool-level gas (swap function only, warm storage):

Pool Direction Gas
PrecisionRangePool USDC→ETH 12,821
PrecisionRangePool ETH→USDC 40,667
PrecisionOraclePool USDC→ETH 40,626
PrecisionOraclePool ETH→USDC 84,227*
PrecisionStablePool USDC→USDT 42,841

*Oracle pool ETH→USDC includes a cold Chainlink staticcall (~2,600 gas). When the oracle is warm (multiple swaps per block), both directions are ~40k.

End-to-end via EIP-7702 (transfer + swap, measured):

Swap Gas
USDC→ETH 36,922
ETH→USDC 65,748
USDC→USDT 74,254
USDT→USDC 78,425

End-to-end via zRouter snwap (measured):

Swap Gas
USDC→ETH 52,172
USDC→USDT 91,750
USDT→USDC 90,896

Competitors (router-inclusive, all from official snapshots):

Swap type Us (7702) Ekubo V3 V4 Curve
ERC-20→ERC-20 74,254 85,675 ~105k ~117k ~120-150k
ERC-20→ETH 36,922 75,644 ~105k ~117k
ETH→ERC-20 65,748 69,243 ~105k ~117k

Competitor numbers are from standard swap benchmarks (not necessarily identical pairs); gas cost is pair-independent in generalized AMMs. Sources: Ekubo, Uniswap V3, Uniswap V4. Ekubo's specialized MEVCaptureRouter achieves ~30-41k (snapshot) but is not the standard user path.

Why it's cheaper

Every generalized AMM pays a "generality tax" per swap:

Operation Generalized AMM Precision Pool
Token address lookup SLOAD (2100 gas cold) Constant (0 gas)
Fee tier lookup SLOAD or pool key param Constant (0 gas)
Price curve math Tick traversal loop, sqrtRatio Stableswap, single mul/div, or oracle read
Pool key hashing keccak256 N/A
Hook/extension dispatch External call N/A
Flash accounting / till Transient storage bookkeeping N/A
Decimal normalization Runtime math Compile-time known
Transfer safety Generic safeTransfer with return check Minimal (trusted tokens)
ETH handling WETH wrap/unwrap overhead Native ETH (range pool)

Precision pools eliminate every row except the curve math and the transfer itself — and for the range pool, the curve math reduces to a single xy=k step. The oracle pool goes further: it replaces the curve entirely with a Chainlink read, enabling zero-price-impact execution that generalized AMMs can't offer at any gas cost.

About

the first onchain superdapp

Resources

Stars

14 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages