Files
zeekayandHanzo Dev b52cb4efc5 coherence: cross-venue reconciler + position rebalance + V4 discovery
Venue reconciler pegs every live venue (0x9999 book, V3 slot0, V4 via
state.json) to the Kraken oracle; drift past band => bounded corrective
trade with notional caps + inventory floors. Rebalance re-centers ops
range positions on oracle as price moves (rate-limited, idempotent). V4
pools auto-discovered from the migration's state.json (absent => book+V3
only). Tests: reconcile/rebalance/v4state all pass.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 20:48:56 -07:00

256 lines
9.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// v4state.go is the TOLERANT reader of the V4 deployment descriptor the parallel
// migration workstream publishes at ~/work/lux/migration/state.json — plus the V4
// venue that reconciles a V4 pool toward the oracle. The schema is OWNED by that
// workstream; this file only READS it and NEVER writes it. Its absence is a
// first-class state, not an error: no file → no V4 venue → the reconciler runs on
// the book + V3 exactly as before. When the file lands, the V4 PoolManager address
// and per-market pool keys are picked up on the next pod start (12-factor: the state
// path is env, the values are read fresh).
//
// FAIL-CLOSED on the unknown execution ABI: until the migration workstream confirms
// the V4 PoolManager speaks the canonical 0x9999 CLOB swap ABI (the maker's own
// architecture note says V2/V3/V4 are liquidity modes of one PoolManager, so it is
// the LIKELY case — but "likely" is not "verified against real value"), V4
// corrections default to DRY-RUN: the intended bounded trade is COMPUTED, METRICED,
// and LOGGED, but not sent. An operator flips V4_EXECUTE=true only after confirming
// the deployed V4 PoolManager's swap entrypoint — one explicit opt-in, never a
// guessed-ABI tx against mainnet value.
package maker
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"math"
"math/big"
"os"
"strings"
dex "github.com/luxfi/precompile/dex"
"github.com/luxfi/geth/common"
)
// V4Pool is one V4 market as published by the migration workstream. Any field the
// descriptor omits is derived: an omitted PoolID is computed from the pool key; an
// omitted Fee/TickSpacing default to the canonical 3000/60 (the same PoolKeyFor uses).
type V4Pool struct {
Symbol string
Base common.Address
Quote common.Address
Fee uint32
TickSpacing int32
Hooks common.Address
PoolID common.Hash
}
// V4State is the parsed, tolerant view of state.json: the V4 PoolManager address and
// the per-symbol pool descriptors. A nil *V4State means "V4 not deployed".
type V4State struct {
PoolManager common.Address
Pools map[string]V4Pool // keyed by market symbol (e.g. "LBTC/LUX")
}
// rawV4State is the permissive decode target: it accepts the several field names the
// descriptor might use for the manager, and pools as either a symbol→object map or
// an array of objects with a "symbol" field. Unknown fields are ignored by
// encoding/json, so a richer schema than this still parses.
type rawV4State struct {
PoolManager string `json:"poolManager"`
PoolManagerU string `json:"pool_manager"`
V4PoolManager string `json:"v4PoolManager"`
Manager string `json:"manager"`
PoolsMap map[string]json.RawMessage `json:"pools"`
PoolsArr []rawV4Pool `json:"poolList"`
}
type rawV4Pool struct {
Symbol string `json:"symbol"`
Base string `json:"base"`
Quote string `json:"quote"`
Fee uint32 `json:"fee"`
TickSpacing int32 `json:"tickSpacing"`
Hooks string `json:"hooks"`
PoolID string `json:"poolId"`
}
// LoadV4State reads and parses the V4 descriptor at path. It returns (nil, nil) when
// the descriptor is not there — the canonical "V4 not yet deployed" signal — so the
// caller tolerates absence by design. A present-but-malformed file IS an error
// (fail-loud on corruption, fail-closed on absence). An explicit poolManagerOverride
// (from V4_POOL_MANAGER) wins over the file's address, so V4 can be armed by env the
// moment the address is known, before or instead of the file.
func LoadV4State(path, poolManagerOverride string) (*V4State, error) {
var raw rawV4State
haveFile := false
if strings.TrimSpace(path) != "" {
b, err := os.ReadFile(path)
switch {
case err == nil:
if uerr := json.Unmarshal(b, &raw); uerr != nil {
return nil, fmt.Errorf("v4 state %s: %w", path, uerr)
}
haveFile = true
case errors.Is(err, fs.ErrNotExist):
// absent: fall through to the env override (or nil)
default:
return nil, fmt.Errorf("v4 state %s: %w", path, err)
}
}
mgrHex := firstNonEmpty(poolManagerOverride, raw.PoolManager, raw.PoolManagerU, raw.V4PoolManager, raw.Manager)
if mgrHex == "" {
return nil, nil // no manager anywhere → V4 not deployed
}
st := &V4State{PoolManager: common.HexToAddress(mgrHex), Pools: map[string]V4Pool{}}
if !haveFile {
return st, nil // env override only: pool keys derived per-market from base/quote
}
add := func(rp rawV4Pool) {
sym := strings.TrimSpace(rp.Symbol)
if sym == "" {
return
}
p := V4Pool{
Symbol: sym,
Base: common.HexToAddress(rp.Base),
Quote: common.HexToAddress(rp.Quote),
Fee: rp.Fee, TickSpacing: rp.TickSpacing,
Hooks: common.HexToAddress(rp.Hooks),
}
if h := strings.TrimSpace(rp.PoolID); h != "" {
p.PoolID = common.HexToHash(h)
}
st.Pools[sym] = p
}
for sym, rm := range raw.PoolsMap {
var rp rawV4Pool
_ = json.Unmarshal(rm, &rp)
if rp.Symbol == "" {
rp.Symbol = sym
}
add(rp)
}
for _, rp := range raw.PoolsArr {
add(rp)
}
return st, nil
}
func firstNonEmpty(xs ...string) string {
for _, x := range xs {
if strings.TrimSpace(x) != "" {
return strings.TrimSpace(x)
}
}
return ""
}
// poolKeyFor resolves the V4 pool key for a coherence market: the descriptor's
// explicit fee/tickSpacing/hooks if published, else the canonical PoolKeyFor
// (3000/60/no-hooks) the rest of the stack uses.
func (st *V4State) poolKeyFor(m CoherenceMarket) (dex.PoolKey, common.Hash) {
if p, ok := st.Pools[m.Symbol]; ok && p.Fee != 0 {
c0, c1 := m.Base, m.Quote
if c0.Cmp(c1) > 0 {
c0, c1 = c1, c0
}
key := dex.PoolKey{
Currency0: dex.Currency{Address: c0}, Currency1: dex.Currency{Address: c1},
Fee: p.Fee, TickSpacing: p.TickSpacing, Hooks: p.Hooks,
}
id := p.PoolID
if id == (common.Hash{}) {
id = common.Hash(key.ID())
}
return key, id
}
key := PoolKeyFor(m.Base, m.Quote)
return key, common.Hash(key.ID())
}
// V4Venue reconciles a V4 pool toward the oracle over the canonical CLOB PoolManager
// ABI, routed to the V4 manager address from state.json. Reads use BestBidAskAt;
// corrections use SwapAt — but ONLY when execute is true (V4_EXECUTE), else the
// bounded trade is dry-run (computed + logged, not sent). This keeps V4 honest
// before its execution ABI is confirmed against real value.
type V4Venue struct {
state *V4State
p9 *Pool9999
execute bool
decimals int
}
// NewV4Venue builds the V4 venue from a loaded state (nil state → nil venue, i.e.
// V4 not deployed). execute gates whether corrections send or dry-run.
func NewV4Venue(state *V4State, p9 *Pool9999, execute bool, decimals int) *V4Venue {
if state == nil {
return nil
}
if decimals <= 0 {
decimals = 18
}
return &V4Venue{state: state, p9: p9, execute: execute, decimals: decimals}
}
// Name identifies the venue in logs + metrics.
func (v *V4Venue) Name() string { return "v4" }
// Spot reads the V4 pool mid from the V4 PoolManager's getBestBidAsk (best bid/ask
// average). An unreadable V4 pool errors → the reconciler skips it (never fabricated).
func (v *V4Venue) Spot(_ context.Context, m CoherenceMarket) (float64, error) {
_, id := v.state.poolKeyFor(m)
bid, ask, fromBook, err := v.p9.BestBidAskAt(v.state.PoolManager, id)
if err != nil {
return 0, err
}
if !fromBook || bid <= 0 || ask <= 0 {
return 0, fmt.Errorf("v4: no live bid/ask for %s", m.Symbol)
}
return (bid + ask) / 2, nil
}
// Correct executes (or dry-runs) one bounded corrective swap on the V4 pool via the
// canonical CLOB swap ABI (SwapAt), keyed by the V4 pool key from state.json. The
// min-out floor from the pure decision is carried; a SellBase pushes a rich pool
// down, a BuyBase pushes a cheap pool up. When execute is false, it returns a
// DryRun result (computed, logged upstream, not sent).
func (v *V4Venue) Correct(_ context.Context, acct Account, m CoherenceMarket, act ReconcileAction) (CorrectionResult, error) {
key, _ := v.state.poolKeyFor(m)
scale := math.Pow10(v.decimals)
// zeroForOne sells currency0; base==currency0 iff base sorts below quote.
sellBaseZeroFor := m.Base.Cmp(m.Quote) < 0
var zeroForOne bool
var amountInWei = big.NewInt(0)
var minOutWei uint64
switch act.Side {
case SellBase:
zeroForOne = sellBaseZeroFor
amountInWei = wholeToWei(act.BaseSize, scale)
minOutWei = wholeToWeiU64(act.MinOut, scale)
case BuyBase:
zeroForOne = !sellBaseZeroFor
amountInWei = wholeToWei(act.QuoteSize, scale)
minOutWei = wholeToWeiU64(act.MinOut, scale)
default:
return CorrectionResult{}, fmt.Errorf("v4 correct: nothing to do (hold)")
}
if amountInWei.Sign() <= 0 {
return CorrectionResult{}, fmt.Errorf("v4 correct: non-positive amountIn")
}
if !v.execute {
return CorrectionResult{DryRun: true}, nil // computed + logged, not sent
}
r, err := v.p9.SwapAt(acct, v.state.PoolManager, key, zeroForOne, amountInWei.Int64(), minOutWei)
if err != nil {
return CorrectionResult{}, err
}
return CorrectionResult{Gas: r.GasUsed, TxHash: r.TxHash}, nil
}