Files
precompile/dex/engine.go
T
zeekay bfa49d10ff precompile/dex: PoolManager → pure ingress + native lock-vault (kill the reserve); deposit/withdraw/balanceOf
Removed settleNativeLegs (the AMM-style native reserve). Native LUX is now lock/mint: real LUX
locked in 0x9010 vault, canonical balance minted in D-Chain available[caller][LUX]; withdraw burns
+ releases (refuses over-release). Added deposit/withdraw/balanceOf selectors + txHash-keyed
idempotency. Swap now BINDS caller identity into the submit frame (was zero → settlement fell back
to C-Chain) + InitializePool calls clob_open_market (markets were unbound → no-custody fallback).
ERC-20 deposit/withdraw return ErrERC20DepositUnsupported (explicit revert, NO fake reserve) pending
a real lock/mint Call() seam. Invariant balanceOf(0x9010)==Σavailable[LUX]+Σlocked[LUX].
2026-06-17 22:30:06 -07:00

117 lines
6.2 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"math/big"
"github.com/luxfi/geth/common"
)
// Engine abstracts the DEX computation backend.
// The precompile is a thin ABI shim — it holds NO canonical DEX state and runs
// NO matching itself. Matching, the resting book, fills, and value conservation
// all live on the d-chain, reached over ZAP. The precompile only translates the
// V4 PoolManager (CLOB facade) ABI into engine operations and commits the
// server-returned BalanceDelta.
//
// Two implementations live in-tree:
//
// - inertEngine (engine_inert.go) — the package DEFAULT. No backend wired:
// every call reverts ErrDEXBackendNotConfigured. The public EVM ships this
// so an unconfigured chain's LP-9010 cleanly reverts instead of running a
// wrong, second matcher.
// - ZAPEngine (engine_zap.go) — the stateless V4->CLOB adapter that forwards
// every operation to the d-chain CLOB gateway over ZAP. The venue installs
// it via dex.SetBackend(NewZAPEngine(...)) when dex-zap-endpoint is set.
//
// Adding a new backend is purely additive: implement Engine, ship it in
// its own package, and have the host EVM call dex.SetBackend() before the
// chain bootstraps. The on-chain ABI (selectors at LP-9010) is invariant
// across backends — a contract compiled against the precompile address
// runs unchanged on every chain.
type Engine interface {
// Initialize computes the initial tick from sqrtPriceX96 and creates pool state.
Initialize(sqrtPriceX96 *big.Int) (int24, error)
// Swap submits a marketable order to the CLOB and returns the fills' net
// BalanceDelta. The taker identity (caller) is bound so the order's spend is
// locked + settled against the caller's D-Chain ledger account (the funds the
// caller DEPOSITED via Deposit). Returns balance delta (amount0, amount1).
Swap(pool *PoolState, caller common.Address, params SwapParams) (BalanceDelta, error)
// ModifyLiquidity adds/removes concentrated liquidity.
// Engine handles tick updates, bitmap flips, position tracking, fee accrual.
// Returns callerDelta and feesAccrued.
ModifyLiquidity(pool *PoolState, owner common.Address, params ModifyLiquidityParams) (callerDelta BalanceDelta, feesAccrued BalanceDelta, err error)
// Donate distributes tokens to LPs via fee growth updates.
Donate(pool *PoolState, amount0, amount1 *big.Int) (BalanceDelta, error)
// Quote estimates swap output without mutating state. Used by router.
Quote(pool *Pool, amountIn *big.Int, zeroForOne bool) *big.Int
// Brand returns the human-readable identity of the backend. The precompile
// uses this in log lines and error wrapping so user-facing strings on a
// downstream L1 chain say e.g. "Hanzo DEX", on Lux say "Lux DEX", etc.
// Implementations MUST return a non-empty constant; an empty value trips
// a sanity check at SetBackend() time.
Brand() string
}
// custodyEngine is the OPTIONAL seam a backend implements when it carries a
// per-account balance ledger that funds and settles orders — i.e. the CLOB
// custody model where "the money lives in the order book". The PoolManager calls
// it to move value INTO the ledger (Deposit, after locking the asset in the 0x9010
// vault) and OUT of it (Withdraw, before releasing the asset), and to bind a
// market's assets (OpenMarket) so the ledger value-checks orders.
//
// The inertEngine does NOT implement this (no ledger to fund). The PoolManager
// type-asserts for custodyEngine and a deposit/withdraw selector reverts cleanly
// when the backend is not custody-capable. ZAPEngine implements it by relaying
// clob_deposit / clob_withdraw / clob_open_market to the D-Chain.
//
// asset handles: a Currency's 20-byte address folds to the 8-byte D-Chain asset
// handle (assetHandle in engine_zap.go); native LUX (address(0)) folds to 0.
type custodyEngine interface {
// OpenMarket binds (base=currency0, quote=currency1) asset handles for poolID
// so the D-Chain custody gate value-checks orders. Idempotent.
OpenMarket(poolID [32]byte, base, quote Currency) error
// Deposit credits exactly amount of asset into account's available D-Chain
// balance. Called AFTER the vault lock leg. Refuses a short credit.
Deposit(account common.Address, asset Currency, amount uint64) error
// Withdraw debits up to want of asset from account's available balance and
// returns the realized amount (clamped). The caller releases exactly realized
// from the vault. realized > want is refused (mint guard).
Withdraw(account common.Address, asset Currency, want uint64) (uint64, error)
// Balance returns account's AVAILABLE balance for asset (read-only, no
// mutation). Used by the balanceOf view selector.
Balance(account common.Address, asset Currency) (uint64, error)
}
// poolRouter is the OPTIONAL seam a backend implements when its canonical pool
// state lives elsewhere (e.g. ZAPEngine, whose pools live on the D-Chain DEX
// server). The PoolManager, which alone knows the V4 poolId, threads it to the
// backend so operations route to the right canonical pool.
//
// The inertEngine does NOT implement this: it has no backend to route to. The
// PoolManager type-asserts for poolRouter and only calls it when present, so a
// backend that holds its own state (none in-tree today) needs no routing.
type poolRouter interface {
// InitializePool creates the canonical pool on the backend keyed by poolID
// and records the route for ps. Returns the authoritative initial tick.
InitializePool(ps *PoolState, poolID [32]byte, sqrtPriceX96 *big.Int, tickSpacing int32, lpFee uint32) (int24, error)
// SetPoolID records the canonical poolID for ps so a later swap / modify /
// donate / quote forwards to the correct server-side pool. Idempotent.
SetPoolID(ps *PoolState, poolID [32]byte)
}
// Replay-idempotency for a marketable order (Swap) is NOT a backend concern: it
// lives in the PoolManager against StateDB, keyed by (txHash, poolId, params).
// That key is durable (survives a node restart) and consensus-shared (identical
// on every validator replaying the same block), so it maps a re-executed / reorg
// / retried tx to EXACTLY ONE d-chain match — properties an in-process backend
// cache cannot provide (RED H1). See PoolManager.swapBindKey / loadSwapBinding.