Files
precompile/dex/asset_resolver.go
zeekayandHanzo Dev d30014b419 fix(dex/9999): exact-integer ZAP price wire + fold dexcore/lx -> pkg/dex
Bump github.com/luxfi/dex v1.5.20 -> v1.14.0. v1.14.0 (a) folded pkg/lx and
pkg/dexcore into a single pkg/dex, and (b) froze the ZAP wire to EXACT
INTEGERS: price is uint64 fixed-point ×1e8 (the PriceInt grid), no float64.
The 0x9999 EVM surface must speak that same grid or it prices a limit the
native venue reads at a different scale — an interop break.

Package fold (mechanical, behavior-neutral — build + full suite green after):
- import path pkg/dexcore -> pkg/dex (alias dexcore), pkg/lx -> pkg/dex.
- v1.14.0 renames: NewCLOBSource->NewOrderBookSource, SourceCLOB->SourceOrderBook,
  ClassPublicCLOB->ClassPublicDEX.
- collapsed the 4 files that imported the fold under both aliases to a single
  dexcore alias (no redundant same-package import).

Exact-integer price wire (the 7 float-wire sites):
- settle9999.go priceLimitToCLOB: return the CLOB limit as a uint64 FIXED-POINT
  ×priceScale integer computed by EXACT big.Int (round-to-nearest of
  s^2·priceScale/2^192), not math.Float64bits of a float. Round-to-nearest (not
  floor) so a sqrt-derived "50" ceiling still crosses a maker resting AT 50.
- swap_sync.go: limitPrice = lx.PriceInt(limitUnits) directly — no
  PriceToInt(Float64frombits(...)) roundtrip.
- native_dchain_client.go enforceProceedsPriceFloor: the MEV floor compares the
  integer limit (limit = priceLimit/priceScale) by big.Int cross-multiply
  (out·priceScale vs priceLimit·spent), dropping math.Float64frombits +
  big.Rat.SetFloat64. Determinism + no 2^53 truncation preserved.
- new package const priceScale (== dexcore.PriceMultiplier == zapwire.PriceScale);
  the intent/relay PriceLimit is now this fixed-point grid value.

swap_custody.go's runSwapPlace keeps its derived float for dexcore.PlaceOrder —
that is the matcher's in-process crossing-loop API (unchanged signature), a
projection, never a consensus/settlement value.

Tests: MEV-floor / price-limit / exact-precision (>2^53) redteam tests
translated to the fixed-point integer domain without weakening any invariant.
New wire_parity_test.go pins priceScale == dexcore.PriceMultiplier ==
zapwire.PriceScale so the grid can never drift from the venue. 437 pass, 0 fail.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 10:56:00 -07:00

125 lines
6.7 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"errors"
"fmt"
"os"
"sync/atomic"
dexcore "github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
)
// asset_resolver.go is the value-path wiring: it injects the canonical-identity
// AssetResolver into the 0x9999 cEVM value path. The resolver PROVES an asset's canonical
// identity on the node's bound network (permissionless, not an allowlist): it derives the
// canonical AssetID with the node's bound ids — pure canonical math, no manifest, no
// registered-asset set. The value path lives here in the precompile. The precompile depends
// only on the dexcore AssetResolver PORT (an abstraction it owns through the shared leaf),
// never on any registry/manifest package — so there is no upward dependency and no cycle.
// The EVM plugin boot constructs the canonical resolver and installs it here exactly like it
// installs the local D-Chain client (InstallDChainClient).
//
// THE PROPERTY THIS ENFORCES (PERMISSIONLESS + PUBLIC): any market over two REAL assets
// can be opened — permissionlessly. The left-padded address (assetID) NAMES an asset but
// does not let it TRADE: trading requires the asset to RESOLVE (well-formed canonical
// identity on the bound network) AND VERIFY ON-CHAIN (real contract code — the
// authoritative reality check). A swap whose base is a fabricated/synthetic asset RESOLVES
// to a well-formed id but REVERTS at the live-reality gate (OpenMarketChecked ->
// ErrAssetNotOnChain), because a synthetic address has no code. When the value path is live
// and no resolver was installed, the path FAILS CLOSED (ErrNoAssetResolver) rather than
// admitting an unbound left-padded address.
// installedAssetResolver is the package-level, identity-bound asset resolver the value
// path consults. nil until the EVM plugin installs one at boot. It is read on the hot
// swap path under an atomic load (the install is a one-shot boot event; the value never
// changes after, so an atomic.Pointer gives a race-free read without a hot-path mutex).
var installedAssetResolver atomic.Pointer[boundAssetResolver]
// boundAssetResolver is the installed resolver together with the network identity it was
// constructed for. The bound identity is CROSS-CHECKED against the runtime AtomicState's
// (networkID, cChainID) at swap time: a resolver built for a different network than the
// node actually runs is REFUSED (fail-closed, derive-don't-trust). This mirrors the
// startup RuntimeVerifier's identity binding, now applied on the value path.
type boundAssetResolver struct {
resolver dexcore.AssetResolver
networkID uint32
cChainID ids.ID
}
var (
// ErrAssetResolverIdentityMismatch is returned when the installed resolver's bound
// network identity does not match the runtime AtomicState's (networkID, cChainID).
// A resolver bound to the wrong chain must never admit assets for this one.
ErrAssetResolverIdentityMismatch = errors.New("dex: installed asset resolver is bound to a different network/C-Chain than the running chain (refusing to admit)")
)
// InstallAssetResolver wires the permissionless canonical asset resolver into the value
// path, bound to the network identity (networkID, cChainID) the node runs. The EVM plugin
// calls this once at boot, alongside the network identity already supplied to the
// precompile at runtime. resolver must be non-nil; cChainID must be set. Installing a
// resolver bound to a (networkID, cChainID) that disagrees with the running chain is
// caught at swap time (ErrAssetResolverIdentityMismatch), not here, because the runtime
// identity is supplied per-call by the AtomicState capability.
//
// Idempotent re-install with the SAME identity is allowed (re-wiring on a benign
// re-init); a re-install with a DIFFERENT identity is refused (a real misconfiguration).
//
// Logs to stderr on success so operators see in the boot log that real-asset admission
// is active on the value path. Not safe for concurrent use; the host binary is the
// single caller during process startup.
func InstallAssetResolver(resolver dexcore.AssetResolver, networkID uint32, cChainID ids.ID) error {
if resolver == nil {
return fmt.Errorf("dex: asset resolver must not be nil")
}
if cChainID == ids.Empty {
return fmt.Errorf("dex: asset resolver requires a non-empty C-Chain id")
}
next := &boundAssetResolver{resolver: resolver, networkID: networkID, cChainID: cChainID}
if cur := installedAssetResolver.Load(); cur != nil {
if cur.networkID != next.networkID || cur.cChainID != next.cChainID {
return fmt.Errorf("dex: asset resolver already installed for network %d C-Chain %s; refusing re-install for network %d C-Chain %s",
cur.networkID, cur.cChainID, next.networkID, next.cChainID)
}
}
installedAssetResolver.Store(next)
fmt.Fprintf(os.Stderr, "dex: 0x9999 real-asset admission active (resolver bound to network=%d C-Chain=%s)\n", networkID, cChainID)
return nil
}
// AssetResolverInstalled reports whether a resolver has been installed (used by tests
// and by boot diagnostics).
func AssetResolverInstalled() bool { return installedAssetResolver.Load() != nil }
// resolverForRuntime returns the installed resolver IF its bound identity matches the
// runtime (networkID, cChainID), else an error. A nil installed resolver yields a nil
// resolver and nil error — the caller (RequireResolvedAsset) maps that to the fail-closed
// ErrNoAssetResolver. A bound/runtime identity mismatch is a hard refusal.
func resolverForRuntime(networkID uint32, cChainID ids.ID) (dexcore.AssetResolver, error) {
b := installedAssetResolver.Load()
if b == nil {
return nil, nil // fail-closed downstream (no resolver -> ErrNoAssetResolver)
}
if b.networkID != networkID || b.cChainID != cChainID {
return nil, fmt.Errorf("%w (resolver network=%d C-Chain=%s; running network=%d C-Chain=%s)",
ErrAssetResolverIdentityMismatch, b.networkID, b.cChainID, networkID, cChainID)
}
return b.resolver, nil
}
// assetSideForCurrency maps a V4 currency to its real-asset (kind, ref) coordinates for
// admission: the zero address is the native coin (EVM_NATIVE + the native marker); any
// other address is an ERC-20 (its 20-byte contract address is the canonical ref). UTXO
// assets never appear as a V4 currency address, so the value path admits only the two
// EVM kinds — exactly matching dexcore.CanonicalRefFor.
func assetSideForCurrency(c Currency) dexcore.AssetSide {
if c.Address == (common.Address{}) {
return dexcore.AssetSide{Kind: dexcore.AssetKindEVMNative, Ref: append([]byte(nil), dexcore.EVMNativeMarker...)}
}
return dexcore.AssetSide{Kind: dexcore.AssetKindERC20, Ref: append([]byte(nil), c.Address.Bytes()...)}
}