LPsLux Proposals
Precompiles
LP-9999

DEX V4 Receipt-Settlement Precompile (0x9999)

Draft

The canonical EVM-side entry point to the Lux DEX. A Uniswap-V4-PoolManager-shaped precompile at 0x9999 that does NOT match — it settles BLS-certified D-Chain fill receipts. The D-Chain (dexvm) matches and signs receipts; the C-Chain verifies the certificate inline (deterministic, fork-safe) and debits/credits EVM balances under Block-STM parallel execution. BLS is the day-1 certificate; Q/Quasar (and PQ/ZK) swap in later via a certType registry with no ABI change.

Category
Core
Created
2025-12-21

Implementation status (code-audited 2026-07-03): PARTIAL 0x9999 address, AlwaysOn module, DEXFill event and activation confirmed (precompile-dex/dex/settle_addr.go:16, settle_module.go:120-145, events.go:35, evm/precompile/registry/bridge.go:53); the SHIPPING design is the two-phase C↔D atomic seam — settle9999.go:16-40 states NO BLS certificate / NO DFillReceipt; the BLS DFillReceipt model lives only in build-excluded _deprecated_bls/.

Documentation: dex.lux.network

Source: github.com/luxfi/dex

Status

Draft. No backwards compatibility. No flags. No replay. The production DEX precompile address is 0x0000000000000000000000000000000000009999; the prior 0x9010 (LP-9010) is deprecated (see §10 Migration).

Abstract

0x9999 is the canonical EVM-side entry point to the Lux DEX. It exposes the Uniswap-V4 PoolManager ABI unchanged, but it is not a matcher and not a backend selector — it is a receipt-settlement adapter. The matching engine, the resting book, fills, and value conservation live entirely on the D-Chain (dexvm, LP-032), which a permissioned subset of validators runs as a normal Lux chain. The D-Chain matches an order and a D-validator quorum BLS-signs a DFillReceipt. The C-Chain 0x9999 precompile then verifies that certificate inline — BLS aggregate verification is deterministic, so it is consensus-safe inside Run — and debits/credits EVM balances. Many such settlements execute in parallel under Block-STM (LP-1024) on the cEVM.

The design slogan:

D matches · C settles · BLS certifies (day-1) · Q strengthens later · X finalizes value.

The app-developer model stays exactly Uniswap/CowSwap/1inch:

amountOut = PoolManager(0x9999).swap(key, params, hookData);

swap returns amountOut or reverts. A revert means nothing happened — no input taken, no output credited, no partial DEX state.

This LP is the EVM settlement layer of the DEX. The core trading protocol is LP-9000; the matching engine is LP-9001; the matcher VM (D-Chain) is LP-032.

Motivation

A central-limit-order-book DEX cannot put every order through EVM consensus and still scale. It also cannot let the EVM precompile call a live matcher during block execution: a live clob_submit against a moving book is re-executed by every validator at Verify at a different instant, producing different fills and a different state root — a consensus fork (this exact scenario is forbidden by the D-Chain's own red-team test TestRED_PerValidatorRelay_SplitsConsensus).

The resolution is a strict build-vs-verify split carried by a certificate:

  • The build/propose path (a node that validates D) obtains the fill from the local matcher and produces a signed DFillReceipt.
  • The verify/replay path (every C-Chain validator) settles only from the certificate — never from a live matcher.

Because the certificate is verified deterministically, the C-Chain validator set need not run the matcher. With ~10 D-Chain validators and ~1000 C-Chain validators, all 1000 verify the same compact receipt and settle in parallel, while only the ~10 match. This is the scalable split.

Specification

1. Address and ABI

The precompile lives at 0x0000000000000000000000000000000000009999 and exposes the Uniswap-V4 PoolManager ABI unchanged (swap selector 0xF3CD914C, plus initialize 0x6276CBBE, modifyLiquidity 0x5A6BCFDA, donate, deposit, withdraw, balanceOf, extsload). A contract or wallet compiled against V4 runs unchanged; only the deployment address is configured to 0x9999. The fill certificate rides in the V4 hookData parameter (or, equivalently, the block-header predicate rail; see §4), so the ABI itself is untouched. The precompile address is registered in LP-9015.

0x9999 is the canonical settlement address and is identical to the LXSettleAddress / DEXPoolManagerAddress constants — it is the money path.

The 0x999X address family. The DEX EVM surface occupies a fixed, contiguous block. Only 0x9999 moves value; 0x9996 composes it; the views are structurally read-only; the tail is reserved.

AddressNameStatusWrites value?
0x9999DEXPoolManager / LXSettleLive (always-on)Yes — the sole settlement path
0x9998DEXQuoterLiveNo (read-only view)
0x9997DEXStateViewLiveNo (read-only view)
0x9996DEXPositionManagerLiveNo — composes 0x9999 (CLOB position adapter)
0x9995DEXPermitReserved (constant only)
0x9994DEXFHEReserved (constant only)
0x9993DEXAdminReserved (constant only)

The EVM sub-call allowlist that grants ERC-20 movement power is exactly {0x9999, 0x9996}; the views can never write.

Activation gate (deterministic, genesis-preserving). A precompile that injects account state at a fork must activate identically on every node, or the state root diverges and the chain forks. The safe trigger is therefore a value that is already part of the agreed-upon consensus record and is monotone: the block header timestamp. 0x9999 is an always-on module gated on a single protocol constant ActivationTime = 1766704800 (2025-12-25 23:20 UTC). The genesis-injection is a pure function of consensus header timestamps: at the block whose (parentTime < ActivationTime ≤ blockTime) — the genesis-or-crossing transition — the state processor sets the 0x9999 account marker (nonce=1, code=0x01); genesis passes parentTime=nil and installs the marker iff ActivationTime ≤ genesisTime. Because the decision reads only the block and parent header timestamps (consensus fields every node agrees on) and compile-time constants — no wall-clock, RNG, env, or filesystem input — every validator recomputes the identical activation on the verify/accept path. This is the same mechanism Ethereum uses for timestamp forks (Shanghai/Cancun) and is consensus-fork-safe by construction. Any chain whose genesis predates ActivationTime reproduces its original pre-0x9999 genesis state byte for byte (no marker, no activation nonce); a chain genesised at or after activation carries the marker from block 0. This preserves the genesis hash of every pre-2025 Lux chain while bringing 0x9999 online at the fork.

The DEXFill settlement event. On a successful settlement 0x9999 emits the canonical, indexable native-CLOB fill signal:

event DEXFill(bytes32 indexed poolId, address indexed taker,
              uint256 amountOut, uint256 blockNumber);
  • topic0 = keccak256("DEXFill(bytes32,address,uint256,uint256)") = 0x6f744d074efc4fa8512636853f8a4c67842f230d4279421c7599dc5c5daf9874
  • topics[1] = poolId, topics[2] = taker, data = (amountOut, blockNumber).

DEXFill is the sole on-chain source a subgraph/indexer uses to materialise native fills (LP-9027 wire → graph). It is scoped to 0x9999 and emitted only on the Phase-B credit of a settled receipt, so a fill is indexable exactly once and exactly when value moved.

2. Roles

ChainVMRole
D-Chaindexvm (LP-032)Matches (CLOB, GPU/native, market-sharded). Emits DFillReceipt. A D-validator quorum BLS-signs the receipt root. Validated by a permissioned subset (the DEX validator set).
C-ChaincEVMSettles. 0x9999 verifies the receipt certificate, debits/credits EVM balances, marks the receipt consumed. Block-STM (LP-1024) parallelizes settlement. Never matches.
Q-ChainQuasarLater. Wraps D/C/X/Z roots, global ordering, PQ/finality cert profiles. Not required day-1.
X-ChainValue finality / universal settlement where required.

3. DFillReceiptV1

The receipt is strictly bound so it can settle exactly one swap, exactly once, on exactly the intended chain and precompile.

DFillReceiptV1 {
  domain            = "lux.dex.fill.v1"
  networkID
  dChainID
  cChainID
  dHeight
  dBlockID
  marketID
  fillID
  receiptID                 // unique; the replay key
  poolKeyHash               // == keccak(PoolKey)
  swapParamsHash            // == keccak(SwapParams)
  hookCommitment            // optional
  sender                    // == msg.sender (or an authorized operator)
  recipient
  tokenInAssetID            // full 32-byte AssetID, never an alias/fold
  tokenOutAssetID
  amountIn
  amountOut
  feeAmount
  feeAssetID
  deadline
  nonce
  precompileAddress = 0x9999
  dStateRoot
  bookRoot
  fillRoot
  accountDeltaRoot          // optional
  certType                  // see §5
  cert                      // see §5
}

0x9999.swap verifies, in order, and reverts on any failure:

  1. networkID, cChainID, precompileAddress equal the current chain/precompile.
  2. poolKeyHash == keccak(key) and swapParamsHash == keccak(params).
  3. sender == msg.sender (or a registered operator for sender).
  4. recipient == params.recipient; amountOut >= params.minAmountOut.
  5. deadline >= block.timestamp.
  6. receiptID is not in consumedReceipt.
  7. The certificate is valid for the D-validator set active at dHeight (§5).
  8. tokenInAssetID / tokenOutAssetID are registered; the pool is active.
  9. The DEX is not halted for the relevant scope (§7).

On success it debits tokenIn from sender, credits tokenOut to recipient, writes consumedReceipt[receiptID], and returns the V4 BalanceDelta.

4. Build-vs-verify split (the safety invariant)

  • Build/propose (a node that validates D, --dex-validator): obtain the fill from the local dexvm and assemble + sign the DFillReceipt. D_CHAIN_UNAVAILABLE is returned only on this path when the node does not run D.
  • Verify/replay (every C validator, inside Run): the precompile MUST NOT query dexvm, a venue, or any live matcher. It settles solely from the receipt certificate in hookData (or from the block-header predicate rail GetPredicateResults(txHash, 0x9999), the warp-style carrier — deterministic, sorted, already part of the consensus header).

Certificate verification is a pure function of the receipt bytes and the verifier key, so it produces an identical result on every validator. This is what makes a single-transaction synchronous swap consensus-safe.

5. Certificate types and the verifier registry (day-1 BLS, Q later)

The certificate kind is a versioned enum, dispatched through an on-chain registry, so the cryptography can be upgraded with no ABI change:

certType: 1 = BLS_FAST_PATH      // day-1
          2 = Q_CERT             // QuasarCert
          3 = Q_CERT_PQ          // PQ finality profile
          4 = ZK_PROOF           // STARK/zk batch proof

VerifierRegistry {
  dChainID
  certType
  verifierKey
  validatorSetID
  quorumNumerator           // registry-pinned quorum (governance authority);
  quorumDenominator         //   NEVER cert-supplied — see BLSCert below
  activationHeight
  status
}

Day-1 BLS_FAST_PATH:

BLSCert {
  version
  certType
  validatorSetID            // selects the VerifierRegistry entry (pubkeys + quorum)
  signerBitmap              // bit i set => validator i (canonical order) signed
  signedMessageHash         // == signedMessage(...); C recomputes and equality-checks
  aggregateSignature        // BLS12-381 G2 compressed
}

The quorum fraction is not on the wire — a cert cannot lower its own acceptance threshold (closing the "attacker sets quorum = 1/large -> single-signer drain"). It is registry-pinned (VerifierRegistry.quorumNumerator/Denominator, governance authority), and a hard absolute >=2/3 BFT floor is enforced on top regardless, matching the Lux warp VerifyWeight convention (signerWeight * 3 >= totalWeight * 2, 128-bit cross-multiply, no overflow).

signedMessage binds the full economic content of the fill — sender, recipient, assets, and amounts — not merely an opaque receiptID a forger could copy onto a different payload. Any tampered field yields a different message and the cert no longer verifies:

signedMessage = SHA256("lux.dex.bls.fill.v1",
                  networkID, dChainID, cChainID, dHeight, dBlockID,
                  receiptRoot, dStateRoot, fillRoot,
                  marketID, fillID, poolKeyHash, swapParamsHash,
                  sender, recipient, tokenInAssetID, tokenOutAssetID,
                  amountIn, amountOut, feeAmount, feeAssetID,
                  deadline, nonce, precompileAddress, certType)

The D-Chain signer hashes the identical extended message over the identical fields; SHA-256 (not keccak) is the agreed attestation domain for the D<->C pair.

For efficiency the D-validator quorum signs a receipt root per D block/batch; each C-Chain swap then carries { receipt, Merkle inclusion proof, one BLS cert over receiptRoot }. (Day-1 MAY begin with one cert per receipt for simplicity; the batch-root form is the target.) Q/PQ/ZK certificates replace the BLS verifier later by registering a new certType at an activationHeight; the V4 ABI and the DFillReceipt envelope are unchanged. The full certType rollout (BLS_FAST_PATH → Q_CERT → Q_CERT_PQ → ZK_PROOF) is governed by LP-9022.

6. Block-STM execution (cEVM)

0x9999 is Block-STM-aware: it declares its read/write access set so the executor runs non-conflicting settlements in parallel and only serializes genuine conflicts.

swap access set:
  R: dexHaltStatus, poolConfig[poolID],
     assetRegistry[tokenIn], assetRegistry[tokenOut],
     verifierRegistry[dChainID][validatorSetID],
     consumedReceipt[receiptID],
     balance[sender][tokenIn], balance[recipient][tokenOut],
     allowance[sender][0x9999][tokenIn]        // ERC-20 only
  W: consumedReceipt[receiptID],
     balance[sender][tokenIn], balance[recipient][tokenOut],
     feeBucket[feeAssetID][epoch % N],
     allowance[sender][0x9999][tokenIn]        // ERC-20 only

Storage keys are fine-grained; there are no global hot write slots (no global nonce, volume counter, single fee accumulator, single settlement counter, or last-receipt slot). Analytics use sharded accumulators (feeBucket[asset][epoch % N], volume[poolID][epochShard]). Two swaps conflict only if they share an account, asset, receipt, allowance, or pool key — so thousands of independent swaps settle in parallel. The cEVM read/write-set conflict detector (LP-1024) covers precompile writes.

7. Halt / safe stop

Trading is stoppable at both layers; halt state is replicated consensus state (not per-node), authorized by the protocol-fee controller / governance. The DEX halt modes integrate with the protocol-wide emergency system in LP-9016.

LayerModes
D-ChainHALT_NEW_ORDERS, HALT_MATCHING, CANCEL_ONLY, SETTLE_ONLY, FULL_HALT
0x9999HALT_NEW_SWAPS, SETTLE_ONLY, DISABLE_RECEIPT_TYPE, DISABLE_ASSET, DISABLE_MARKET

Halt keys are fine-grained: haltGlobal, haltMarket[id], haltAsset[id], haltReceiptType[certType], haltValidatorSet[id] (haltGlobal is a read-only hot key — reads do not contend; it is written rarely). The safe default on incident is: stop new swaps; allow cancel, settle of already-certified safe receipts, and withdrawals/refunds. A halted swap reverts cleanly with no partial state. The precompile retains the pre-existing pauseDEX/resumeDEX/pausePool/freezePool selectors, gated on the protocol-fee controller.

8. FHE and execution modes

FHE is not in the vanilla swap hot path. A receipt carries an executionMode ∈ { PUBLIC_CLOB, FHE_CLOB, Z_PRIVATE }; the public CLOB is the fastest path. Private/encrypted routes use the same receipt-settlement framework when enabled. Day-1, FHE is disabled or shadow-mode for the DEX so first-launch latency is governed by the public path alone.

9. Security invariants

  • Full 32-byte AccountID and AssetID everywhere — never a matcher handle, alias, or fold.
  • CustodyOpID binds the C-Chain tx hash and call index.
  • No declared-asset override; no fallback identity; no zero-lock executable orders.
  • ERC-20: day-1 an allowlist of strict tokens (no rebasing / fee-on-transfer / callback), observed-delta accounting, nonReentrant; arbitrary tokens are wrapped to canonical AssetID balances first.
  • All-or-nothing settlement (ownership invariant, not only conservation: only the named counterparties move value).
  • Replay protection via consumedReceipt[receiptID].
  • Restart replays carried receipts identically.

10. Migration from 0x9010

0x9999 is the sole production money path. 0x9010 (LP-9010) is deprecated and MUST be one of: (a) disabled for value-moving calls (revert PRECOMPILE_MOVED), or (b) a forwarder that calls the exact same implementation and the same consumedReceipt / halt / verifier-registry storage namespace (dex.precompile.v1.9999.*). There MUST NOT be two active money paths or two separate consumed-receipt maps. Web and mobile clients change only the configured address (0x9010 → 0x9999); the V4 call site is unchanged.

Rationale

  • Receipt-settlement over live relay is the only model that keeps a single-tx synchronous swap consensus-safe while the matcher is a separate chain: the cert is verified deterministically, the matcher is never called at Verify.
  • BLS first, Q later delivers the smooth day-1 trading UX without blocking on the full Quasar finality stack; the certType registry makes the upgrade an on-chain configuration change, not an ABI break.
  • Block-STM with fine-grained keys turns the universal C-Chain validator set into a parallel settlement fabric, which is the throughput story for day-1.
  • V4 ABI preserved means the entire Uniswap-tooling and the existing Lux exchange web/mobile clients work with only an address change.

Backwards Compatibility

None by intent. 0x9010 is retired to a single, namespace-shared path (§10). No flags, no dual money paths.

Reference Implementation

  • ~/work/lux/precompile/dex — the 0x9999 adapter.
  • ~/work/lux/chains/dexvm — the D-Chain matcher and DFillReceipt production (LP-032).
  • ~/work/lux/evm — cEVM / Block-STM settlement.
  • ~/work/lux/node — the DEX validator set and local chain resolution on the build path.

Benchmark matrix and day-1 latency targets (D p50 match < 1 ms/shard, C BLS root verified once per batch, E2E p50 within a few fast blocks) are maintained in LP-9020 and the implementation's benchmark suite.

Security Considerations

The fork-safety of the EVM path rests entirely on §4: the verify/replay path must never call a live matcher and must settle only from a deterministically verified certificate. Any change that reintroduces a live dexvm/venue call inside Run on the verify path reintroduces the consensus fork. The D-validator quorum and the verifier registry are the trust root for settlement; their key rotation and activationHeight discipline are consensus-critical. Halt authority is a privileged capability (protocol-fee controller / governance) and MUST default to fund-preserving modes.

  • LP-9000: DEX Core Specification
  • LP-9001: DEX Trading Engine (matching)
  • LP-9010: DEX Precompile (superseded by this LP)
  • LP-9015: Precompile Registry (0x9999 entry)
  • LP-9016: Emergency Procedures (DEX halt)
  • LP-9020: Performance Benchmarks
  • LP-9022: Upgrade Procedures (certType path)
  • LP-1024: Parallel Validation / Block-STM
  • LP-032: DEX VM (the D-Chain matcher)
  • LP-3520: Precompile Suite Overview

Copyright and related rights waived via CC0. </content> </invoke>