Files
2026-08-30 19:32:07 -07:00

19 KiB
Raw Permalink Blame History

maker

Automated market maker engine for Lux Network exchanges. Aggregates orderbook depth from multiple external venues, calculates a fair mid price, and generates limit order quotes on both sides. Also seeds and maintains liquidity directly on the venue D-Chain over its FROZEN clob_* CLOB ZAP surface — it is the V4 liquidity seeder.

Mainnet 0x9999 fill — PROVEN (2026-07-14)

First real DEX settlement proven on Lux mainnet 96369 post-reimport, on the C-Chain 0x9999 embedded-book precompile (NOT the native D-Chain path — D is unmounted on mainnet). Driver: the one-shot cmd/provefill (since deleted; maker rests a BID, a distinct taker SELLs to cross; uses Pool9999, the same money-path calldata coherence uses).

  • Settlement tx 0xb3c561bb530d0c758f84df9df2d86009d49894b43a693e87ede58b227b309861, block 1087057, status 1. Market LSOL/WLUX (poolId 0xeb8c1af7c9dd79282bb04a82fd2d0e02545d9be1f38ef02e8b673f16d7e27bda), maker 0x5806b0e0…, taker 0xcD0cDEd2… (the coherence maker/taker keys, secret a k8s Secret since deleted, keys now in KMS at /bots/retired). Emitted DEXFill (amountOut 500000000600072843) + 2 real ERC-20 Transfers. Conservation holds (0.5 LSOL ↔ 0.5 WLUX). Indexed by the explorer dex subgraph (explorer:8090/v1/graph/cchain/dex/graphql, entities markets/orders/fills).

Root cause of the prior "maker up but 0 fills" (two independent causes, both found live):

  1. coherence RPC was dead: COHERENCE_RPC pointed at a host/path pair that 404s. The endpoint that works is /v1/chain/C/rpc — everywhere, on both hops. Re-measured 2026-07-27 against the live mainnet fleet (luxd v1.34.0, LuxNetwork CR spec.ports.http = 9630):

    target /v1/chain/C/rpc /v1/chain/C/rpc control /v1/chain/ZZZ/rpc
    luxd-1:9630 direct 200 0x17871 404 404
    api-gateway-gateway.lux-mainnet.svc:80 200 0x17871 404 404
    https://api.lux.network 200 0x17871 404 404

    So the earlier claim in this file — "luxd serves the C-Chain RPC ONLY through the api-gateway; direct :9630 404s BOTH prefixes" — was wrong on both halves. Direct :9630 works fine on /v1, and the gateway 404s /ext. luxd has exactly one prefix (node/server/http/server.go, baseURL = "/v1"); /ext/ is gone, not deprecated. Never generalise the port either: mainnet is 9630, devnet 9650, testnet 9640 — read spec.ports.http from that net's LuxNetwork CR.

  2. taker probe disabled: coherence logs probe=0s — the taker-probe (ProbeInterval) is off, so coherence only rests a book + arbs; it never self-crosses. Fills need an external taker OR COHERENCE_PROBE_INTERVAL/ProbeSize set (owner decision).

CLOB conventions that matter (precompile/dex v0.15.0 embedded book, the deployed ABI):

  • base = currency0, quote = currency1 (PoolKey auto-orders by address). swapPlace isBid=BUY locks quote(c1); !isBid=SELL locks base(c0).
  • swap exact-input BUY is refused without a price limit (ErrBuyRequiresLimit); a SELL needs a min-out floor (carried in hookData). Pool9999.Swap hardcodes sqrtPriceLimit=0, so it only drives SELLs — the price-limit-free settle path.
  • amounts are raw token wei, capped at uint64 (~18.4 tokens at 18 dec per op).
  • TWO distinct ledgers: the V4 ERC-6909 balance balanceOf(account,asset) (0xf7888aec) is NOT the swap-custody CLOB available — reading the former for a swapDeposit shows 0 and looks like a dropped-credit bug; it isn't. The precompile's own revert/return is truth.
  • Build: GOWORK=off (the repo go.work uses the local precompile = v0.19.1 settle-only, which removed the embedded book; the maker pins v0.15.0 = the deployed embedded-book ABI) and CGO_ENABLED=0 (skip pkg/lx GPU cgo).

Pool9999.Withdraw (swapWithdraw) was added alongside Deposit so the client can round-trip custody (deposit → place/swap → withdraw).

Module

github.com/luxfi/maker — Go library + CLI.

Key Types

  • Maker — quote loop (start/stop, position tracking, requote)
  • Aggregator — merges orderbooks from N ProviderSource in parallel
  • Strategy — interface: MidSpreadStrategy (symmetric), InventorySkewStrategy (position-aware)
  • OMAProviderSource — reads on-chain OracleMirroredAMM prices via EVM RPC
  • Config — spread, levels, qty, refresh interval, phantom depth, position limits, Pairs, DEXEndpoint, MakerUser
  • DEXClient (dex.go) — venue D-Chain seeding path; speaks the venue's FROZEN clob_* surface (clob_ensure_market/clob_place/clob_cancel/clob_submit) over github.com/luxfi/rpc, keyed by 32-byte poolId. The frozen frame (poolId[32], place=65, cancel=40, submit=66, ack=24, fill=17) is re-declared locally byte-identical to github.com/luxfi/dex/pkg/zapwire (the same pattern chains/dexvm/relay.go and the precompile use — each client home re-declares the frozen frame rather than importing the package). The venue is poolId-keyed and symbol-agnostic (its stored "symbol" is just the poolId hex); the maker, as the gateway-level seeder, owns the symbol→poolId map: SymbolPoolID(sym) = keccak256(sym) by default, overridable per pair via Pair.PoolID to seed the exact book a deployed V4 PoolKey (key.ID()) resolves to. Book reads (BestBid/BestAsk) are NOT on the clob_* surface (write-only: reads are chain-state queries) → they return ErrReadUnsupported; the seeder requotes around its configured reference mid.
  • Seeder (seeder.go) — creates markets and maintains a 2-sided book per Pair: Seed places initial quotes, Maintain cancels+re-quotes around the live mid, Run does both on RefreshInterval.

Usage

Quote-only (aggregate external venues):

go run ./cmd/maker -symbols BTC-USD,ETH-USD -spread 10 -levels 5 -interval 5s

Seed + maintain the DEX (creates LUX/LUSD and LUX/LETH, places 2-sided quotes):

go run ./cmd/maker -dex 127.0.0.1:5000 -user maker -spread 10 -levels 5 -interval 5s

Wire real venues by implementing ProviderSource (see README.md).

Deploy (container + k8s)

The maker ships as ghcr.io/luxfi/maker — a long-running CLOB liquidity seeder deployed in-cluster ALONGSIDE luxd so it can reach the native D-Chain CLOB route group. It creates the canonical custody markets (LUX/LUSD, LUX/LETH) and maintains a 2-sided book around a reference mid, which is the orderbook the exchange Trade page renders (the exchange-api reads the SAME D-Chain over dex_get_*).

  • Image (Dockerfile): CPU, linux/amd64, CGO_ENABLED=0 static binary on luxfi/crypto's pure-Go path — same build shape as the sibling ghcr.io/luxfi/dex. Built by .github/workflows/docker.yml via the shared hanzoai/.github/.github/workflows/docker-build.yml@main reusable (secrets: inherit passes the gh_token BuildKit secret for private luxfi modules). Semver-only tags.
  • Config is 12-factor (env) — every flag's default reads an env var (cmd/maker/main.go): DEX_HTTP_URL (the .../v1/chain/D route prefix; setting it puts the binary in seed mode), MAKER_USER, MAKER_SPREAD_BPS, MAKER_LEVELS, MAKER_INTERVAL, MAKER_SKEW_BPS, MAKER_DEBUG. CLI flags still win.
  • k8s (k8s/): kubectl kustomize k8s | kubectl apply -f -. A ConfigMap (maker-config) carries the env; a single-replica Deployment (Recreate strategy — two seeders sharing MAKER_USER would double-deposit and fight over the book). Base targets lux-devnet; for testnet/mainnet change the namespace and the DEX_HTTP_URL port (devnet 9650 / testnet 9640 / mainnet 9630).
  • compose (compose.yml): local seeding — DEX_HTTP_URL=http://host.docker.internal:9650/v1/chain/D docker compose up --build.

Live seeding requires a reachable luxd serving the native D-Chain VM at <DEX_HTTP_URL>/dex/* (chain alias D); the D-Chain is not publicly exposed (api.lux.network/v1/chain/D 404s), so this is an in-cluster surface.

Coherence — continuous Kraken-oracle maker on the C-Chain 0x9999

MAKER_MODE=coherence runs the continuous price-coherence service on the C-Chain 0x9999 PoolManager (NOT the D-Chain CLOB above — orthogonal venue, orthogonal transport). It pegs a 2-sided book to the live Kraken mid for each configured market every COHERENCE_REQUOTE, an arb loop corrects drift past the band, and an optional probe taker exercises the book so the realized price is MEASURED. It is the continuous, persistent-market production form of the trader-pkg coherence study (~/work/lux-private/memos/dex-price-coherence.md); the strategy is private, hence it lives here in luxfi/maker (private), never in public luxfi/trader.

  • Files: pool9999.go (the 0x9999 EVM client — initialize/swapDeposit/ swapPlace/swapCancel/swap + getBestBidAsk/getOpenOrders + DEXFill reads, multi- endpoint broadcast for nets that don't gossip the mempool), coherence.go (the service: setup → requote → arb → probe → metrics), coherence_config.go (market spec parse + composite oracle), coherence_http.go (Prometheus /metrics + /healthz). Reuses provider_kraken.go as the live oracle (no new oracle code).
  • Architecture: V2/V3/V4 are three liquidity modes of the ONE 0x9999 pool (one pool id, one fee, one maker peg), so coherence = one maker pegging one price.
  • Why the maker pegs the book to ~Kraken: bid = mid·(1−s), ask = mid·(1+s); the book MID ≈ Kraken (the "right price"). The half-spread s is the maker's income, not tracking error. A market order realizes ≈ −s bps (it crosses the spread).
  • CLOB units: price grid = price·1e8 (priceGridMul); size + deposits are uint64 units that map 1:1 to ERC-20 wei. Size ≫ 1 (default 100000) so grid quantization is sub-bps. The maker tops its vault once at setup (COHERENCE_DEPOSIT_BUFFER); cancels refund locks, so per-cycle deposits are unnecessary.
  • Config (12-factor): COHERENCE_RPC (comma-separated for multi-broadcast), COHERENCE_MARKETS ("SYMBOL=SOURCE:BASE:QUOTE[:SIZE]"), COHERENCE_SPREAD_BPS, COHERENCE_REQUOTE (~60–120 s optimum), COHERENCE_ARB_BAND_BPS, COHERENCE_PROBE (taker cadence; 0=off), COHERENCE_PROBE_SIZE, COHERENCE_{MAKER,TAKER}_IDX or COHERENCE_{MAKER,TAKER}_KEY, COHERENCE_MNEMONIC/LUX_MNEMONIC, COHERENCE_METRICS_ADDR (:2112). The maker and taker MUST be distinct accounts (matcher self-trade prevention).
  • Reference SOURCE forms (the three orthogonal pricing realities, one per market): a Kraken query pair (ETHUSD) → live CEX mid (USD-quoted major); fixed:<price> → a configured constant (a non-Kraken asset like LUX); ratio:<pair>/<denom> → Kraken(pair)/denom, the cross-rate that prices a base in a numeraire the CEX does not list. An L/LUX book uses ratio*: LETH/LUX=ratio:ETHUSD/12.5 peg-mid = (ETH/USD)/(LUX/USD) ≈ Kraken ÷ 12.5. One shared Kraken feed serves both direct majors and every ratio leg — no second oracle, no fabricated price (unknown → error).
  • Bootstrap (one-shot, dev/test): MAKER_MODE=coherence-init deploys ONE shared quote + a base per market, opens the 0x9999 markets, funds maker+taker, and prints the COHERENCE_MARKETS to bake into the ConfigMap. The quote token is env-selected (COHERENCE_INIT_QUOTE/_NAME, default LUSD): set it to WLUX and drive the market list with ratio sources to seed a LUX-quoted book (L* bases vs one WLUX quote). The init's COHERENCE_MAKER_KEY MUST equal the service's, so the funded inventory lands where the running maker's vault deposit pulls from (else Setup's swapDeposit reverts — the classic exit-1). Pure restart-safe service: env in, peg out, no token redeploy.
  • Testnet luxd read-surface caveat (chain 96368, as of 2026-07): the deployed testnet luxd's 0x9999 precompile serves the WRITE surface (swapDeposit/swapPlace land) but predates the READ selectors — getBestBidAsk/getOpenOrders return "unknown 0x9999 selector". So the maker quotes fine (logs msg=requote with bid/ask; 2-sided places confirmed on-chain by decoding swapPlace calldata) but orders= shows 0 and coherence_peg_error_bps=0 (BestBidAsk unavailable). Proving the book on this luxd = decode the successful swapPlace txs, not an eth_call read. Bumping testnet luxd to a precompile ≥ v0.15.0 restores the read path.
  • Deploy: no overlay ships here. The k8s/coherence and k8s/hum overlays were three Deployments of one program quoting a venue that cannot settle, and are deleted. Per-net an overlay must set the overlay sets the in-cluster RPC, the COHERENCE_MARKETS addresses, and the image tag; the maker/taker keys come from KMS (/bots), synced by a KMSSecret, on mainnet) out-of-band. Metrics: coherence_peg_error_bps / _p99_bps (book-mid vs Kraken — the "right price"), coherence_realized_error_bps (probe fill vs Kraken), coherence_burn_lux (gas·25 gwei), requote/arb/probe/fill counters.

Coherence go-live (gated — mainnet is REAL VALUE)

  1. Build + publish the image (CI, never local): tag luxfi/maker → CI builds ghcr.io/luxfi/maker:vX.Y.Z.
  2. Supply REAL mainnet ERC-20 addresses (WETH/WBTC/WLUX + a USD stable) in the mainnet overlay's COHERENCE_MARKETS — NEVER throwaway deploys on real value.
  3. Fund two ops accounts (maker + taker) from the lux-deployer treasury via cast, store their keys in KMS, sync via a KMSSecret as COHERENCE_{MAKER,TAKER}_KEY.
  4. COHERENCE_PROBE stays UNSET on mainnet (external flow trades; never wash-trade).
  5. Deploy ONLY after explicit go-live sign-off; watch coherence_peg_error_p99_bps and inventory; start with one market (LUX/USD).

The AMM the maker can actually trade — V3 only (measured 2026-08-08)

Read from a PINNED node (luxd-4, chain 96369) — never the public LB, which round-robins across heights.

The constant-product stack this repo once carried a V2Venue for is gone. Every address from standard/deployments/mainnet-96369-amm.json (deployed 2026-07-28) returns 0 bytes of code: WLUX 0x634a2398, LETH 0x2702d8f8, LBTC 0x9858086e, LUSDC 0x6b23efd9, AMMV2Factory 0xcf3ccd95, AMMV2Router 0x80ee1653, and all three pools. The RLP restore's restore point sits below that deploy, so the restore silently un-deployed it — the same failure mode that un-deployed governance. ammv2.go and its V2Venue are therefore deleted: a corrector whose factory has no code can only skip. hanzo (36963) and zoo (200200) have no AMM and no tokens at all.

What IS live on 96369 is the pre-existing V3 stack: factory 0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84, router02 0x939bC0Bca6F9B9c52E6e3AD8A3C590b5d9B9D10E, position manager 0x7a4C48B9dae0b7c396569b34042fcA604150Ee28. (V3_QUOTER 0x12e2B76F is 0 bytes — quote by simulating the swap, not by calling the quoter.) Six pools hold liquidity:

market pool spot (t1/t0) pool holds
WLUX/LZOO 0.3% 0x1c000d5dbe1246fb84ad431e933e5563f212a62b 21.749 131.9M WLUX / 2.87B LZOO
LSOL/WLUX 0.3% 0xa9330ee37bba7139c7697d82253b3d5fbb430004 275,920 1.038 LSOL / 2.17M WLUX
WLUX/LUSD 0.3% 0x37011bb281676f85962fb35c674f7e9eb7584452 0.000613 12,091 WLUX / 6.26 LUSD
LPOL/WLUX 0.3% 0xe0128e0fb36a1dedb69ba8a62084074dc4be50e0 2,697.9 2.647 LPOL / 7,003 WLUX
WLUX/LETH 0.3% 0xfafbf1a0c5d6604f48378872c534545612a17aec 2.207e-07 4.81M WLUX / 0.00635 LETH
LBTC/WLUX 0.3% 0xa06e94ed014034e804c674f64c6f4c9313f2a6d7 1.163e08 4.18e-05 LBTC / 10.0M WLUX

Six more pools exist at zero liquidity (WLUX/LUSD 0.05%, LBTC/LUSD, LETH/LUSD, LPOL/LUSD, LZOO/LUSD, LBTC/LETH) holding 1e-18 dust.

The tokens themselves barely exist. totalSupply() on 96369, priced off Kraken:

asset total supply worth in an AMM pool pooled worth
LSOL 18.11 $1,361.61 1.038 $78.03
LETH 1.808 $3,469.05 0.00635 $12.19
LUSD 355.89 $355.89 6.260 $6.26
LBTC 0.0057 $370.22 0.0000418 $2.72
LPOL 17.08 $1.29 2.647 $0.20
TOTAL $5,558 $99

Every reference-bearing asset on the chain, summed, is worth $5,558, and $99 of it sits where a price can be corrected. So this is not a treasury-funding problem the 2T LUX could solve: there is no LBTC to add to a pool, because 0.0057 LBTC is the entire supply. It is a bridge/issuance problem. WLUX (150.1M) and LZOO (10.86B) are the only real supplies, and their pool is the only real market.

And nothing external trades. Over the last 60 blocks: 155 txs, 140 from the traffic bot, 15 from the treasury, 2 distinct senders on the whole chain, 0 empty blocks. The arbitrage that normally drags a pool back to reference has no participants.

Why no bot can converge these markets, and what would. Price the non-LUX leg off Kraken and each pool implies a LUX/USD: LUSD $0.000613, LBTC $0.000558, LETH $0.000423, LSOL $0.000272, LPOL $0.0000279. They span 21.9x. That dispersion is the entire arbitrage, and it is uncorrectable, because correcting it means buying or selling the scarce leg — and the scarce leg across all five reference-bearing pools is worth $99 in total (LSOL $78.03, LETH $12.19, LUSD $6.26, LBTC $2.72, LPOL $0.20). A maker pointed at these pools drains the short side in a handful of trades and leaves the price further off than it found it. Convergence here is a FUNDING problem: the pools need real two-sided inventory before a reconciler has anything to reconcile. WLUX/LZOO is the one deep pool and neither leg lists anywhere, so it has no external reference to converge to — its price is definitionally whatever the pool says.

The reconciler that would do the work already exists and needs no new code: decideReconcile (reconcile.go) is a pure two-sided decision under a no-trade band, a per-trade notional cap, inventory floors and a daily realized-loss bound, with V3Venue (v3router.go) as its live corrector.

CI builds this repo (corrected 2026-08-08)

The prior note here said ghcr.io/luxfi/maker "cannot currently be built" because all 47 luxfi runners were offline. That is stale. The workflow moved to .hanzo/workflows/docker.yml and builds on the forge: ghcr.io/luxfi/maker:sha-0b6c660 was published 2026-08-07T00:50:37Z from main. CI watches git.hanzo.ai, not GitHub — git push hanzo <ref> creates the build; pushing only to origin creates nothing and looks like broken CI.

go.sum: luxfi/zap v0.8.10 moved tag

Its recorded module-zip hash no longer matched the published tag, so go build failed closed with SECURITY ERROR ... checksum mismatch on any machine with a cold cache — the same class of breakage as the earlier luxfi/pq v1.0.3 fix. Corrected in place (h1:QKNTAsen… → h1:4prCYVqL…); the /go.mod hash was already right and is unchanged.