mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
ONE money path: D-Chain matches, C-Chain 0x9999 SETTLES, C never matches. - 0x9999 swap() routes ONLY to SettleSwap (native C<->D atomic seam): untagged/ DI01 => Phase A intent (lock input, write C->D object, return intent id); DS01 => Phase B settlement (consume a real D->C atomic object once, credit RECORDED amount). No-receipt-no-settle: absent a real object, Phase B reverts ErrNativeNoSettlement (TestDecomplect_NoReceiptNoSettle). - Deleted the embedded matcher: swap_sync*.go, swap_custody.go (maker order book), buildRouter/evmAMMPool in swap_amm_pool.go, and the swapDeposit/withdraw/place/ cancel selectors. A live in-trie matcher forks consensus; deterministic atomic-object consumption is fork-safe inline. - 0x9010 already non-dispatched. 0x9012 (LXRouter) value selectors exactInput*/ exactOutput* now revert ErrPrecompileMoved (PRECOMPILE_MOVED); read-only quote/ route views kept. - Cleanups (dead code removal, all unreachable via any dispatched selector): deleted the legacy engine PoolManager.Swap + LXRouter.Exact*/executeV4Swap*; deleted the vestigial makerLockedVault ledger (0 callers, always 0); deleted the unwired Block-STM scaffolding (AssertWriteSetWithin/AccessCommitment/Predict*). - Activation rides the EXISTING canonical gate 1766708400 (Dec 25 2025 16:20 PST); no new timestamp. Build CGO_ENABLED=0 (pure-Go luxfi/dex path); go.sum zap re-tag checksum refreshed. 327 dex tests PASS / 0 FAIL, go vet clean. RED-reviewed (0 critical, all drain vectors safe, fund-strand cleared, negative invariant falsified-then-restored). Co-authored-by: Hanzo Dev <[email protected]>
314 lines
14 KiB
Go
314 lines
14 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
)
|
|
|
|
// settle_maker.go is the 0x9999 `modifyLiquidity` path — the MAKER / resting-order
|
|
// concern. It is PURE StateDB arithmetic with NO matching (matching lives on D;
|
|
// resting an order is a custody lock, not a trade). It composes the EXISTING
|
|
// single-concern reserve helpers from settle_custody.go (the depositor claim IS the
|
|
// maker's available balance in the 0x9999 vault) — it does NOT introduce a second
|
|
// custody ledger.
|
|
//
|
|
// THE MODEL:
|
|
// - ADD (liquidityDelta > 0): LOCK `delta` of the maker's reserve of one asset:
|
|
// available -= delta ; locked += delta
|
|
// and record a resting order. orderID = keccak(owner‖poolID‖salt‖tickLower‖tickUpper).
|
|
// - REMOVE/cancel (liquidityDelta < 0): UNLOCK |delta| (clamped to locked):
|
|
// locked -= min(|delta|, locked) ; available += min(|delta|, locked)
|
|
// and mark the order CANCELLED.
|
|
//
|
|
// CONSERVATION: locked funds are still inside the 0x9999 vault (available + locked
|
|
// is invariant across a lock/unlock); nothing is minted, nothing strands. A
|
|
// resting maker's locked balance is released by cancel (here) or consumed by a
|
|
// swap settlement that the maker is the counterparty for (the receipt path, which
|
|
// already debits/credits the vault). The withdraw path drains AVAILABLE only —
|
|
// locked funds cannot be withdrawn out from under a resting order (claim clamps to
|
|
// available, defense in depth).
|
|
//
|
|
// REENTRANCY: this is a custody-moving entrypoint, so it takes the SAME global
|
|
// non-reentrant guard the deposit/withdraw path uses (a malicious ERC-20 cannot
|
|
// re-enter modifyLiquidity from inside an observed-delta window — though the lock
|
|
// itself moves no token, the guard is a uniform discipline for every custody op).
|
|
|
|
// --- Maker side. A resting order locks exactly ONE asset; which one is the
|
|
// maker's `side`. BID rests currency0 (the maker provides token0, wanting token1);
|
|
// ASK rests currency1. The side is carried in a tiny maker envelope in hookData
|
|
// (tag "M999", one side byte) so the locked asset is UNAMBIGUOUS and deterministic;
|
|
// absent the envelope the default is BID (locks currency0). This keeps the locked
|
|
// AssetID a pure function of (key, hookData), which PredictAccesses also derives.
|
|
type MakerSide uint8
|
|
|
|
const (
|
|
MakerSideBid MakerSide = 0 // rests currency0 (the lower-address asset)
|
|
MakerSideAsk MakerSide = 1 // rests currency1 (the higher-address asset)
|
|
)
|
|
|
|
// makerEnvelopeTag marks a modifyLiquidity hookData blob as a maker instruction.
|
|
// A non-conforming hookData (or none) defaults to MakerSideBid — there is no
|
|
// matcher to fall back to; resting is a pure custody lock either way.
|
|
var makerEnvelopeTag = [4]byte{'M', '9', '9', '9'}
|
|
|
|
// decodeMakerSide reads the maker side from a hookData envelope, defaulting to BID.
|
|
func decodeMakerSide(hookData []byte) MakerSide {
|
|
if len(hookData) >= 5 && hookData[0] == makerEnvelopeTag[0] && hookData[1] == makerEnvelopeTag[1] &&
|
|
hookData[2] == makerEnvelopeTag[2] && hookData[3] == makerEnvelopeTag[3] {
|
|
if hookData[4] == byte(MakerSideAsk) {
|
|
return MakerSideAsk
|
|
}
|
|
}
|
|
return MakerSideBid
|
|
}
|
|
|
|
// lockedAssetForSide returns the injective AssetID a side rests.
|
|
func lockedAssetForSide(key PoolKey, side MakerSide) [32]byte {
|
|
if side == MakerSideAsk {
|
|
return assetID(key.Currency1)
|
|
}
|
|
return assetID(key.Currency0)
|
|
}
|
|
|
|
// --- Resting-order record + status.
|
|
type OrderStatus uint8
|
|
|
|
const (
|
|
OrderStatusNone OrderStatus = 0
|
|
OrderStatusOpen OrderStatus = 1
|
|
OrderStatusCancelled OrderStatus = 2
|
|
)
|
|
|
|
// RestingOrder is a maker's locked resting order. owner+lockedAsset+lockedAmount
|
|
// are FULL WIDTH (AccountID/AssetID never truncated). tickLower/tickUpper/salt pin
|
|
// the position; status is the lifecycle witness for idempotent cancel.
|
|
type RestingOrder struct {
|
|
Owner common.Address
|
|
PoolID [32]byte
|
|
Side MakerSide
|
|
LockedAsset [32]byte
|
|
LockedAmt *big.Int
|
|
TickLower int24
|
|
TickUpper int24
|
|
Salt [32]byte
|
|
Status OrderStatus
|
|
}
|
|
|
|
// resting-order storage. Each order is a few fine-grained slots; the per-owner
|
|
// index lets 0x9996 enumerate without scanning. No global hot write.
|
|
var (
|
|
restingOrderPrefix = []byte(settleStateNamespace + "ord") // restingOrder[orderID] -> record
|
|
ownerOrdersPrefix = []byte(settleStateNamespace + "own") // ownerOrders[owner][i] -> orderID, [count]
|
|
lockedReservePrefix = []byte(settleStateNamespace + "lck") // lockedReserve[owner][asset] -> amount
|
|
)
|
|
|
|
// order record slot suffixes (one concern per slot).
|
|
var (
|
|
orderMetaSuffix = []byte("m") // status(1)|side(1)|tickLower(int24)|tickUpper(int24)
|
|
orderOwnerSuffix = []byte("o") // owner (address)
|
|
orderPoolSuffix = []byte("p") // poolID (bytes32)
|
|
orderAssetSuffix = []byte("a") // lockedAsset (bytes32)
|
|
orderAmtSuffix = []byte("v") // lockedAmount (uint256)
|
|
orderSaltSuffix = []byte("s") // salt (bytes32)
|
|
)
|
|
|
|
// MakerOrderID derives the resting order's id: keccak(owner‖poolID‖salt‖tickLower‖tickUpper).
|
|
// Deterministic + collision-resistant: two distinct (owner,pool,salt,range) tuples
|
|
// yield distinct ids; the same tuple re-derives the SAME id (so a cancel finds the
|
|
// order placed by an earlier tx, and a re-add to the same slot is detected).
|
|
func MakerOrderID(owner common.Address, poolID [32]byte, salt [32]byte, tickLower, tickUpper int24) [32]byte {
|
|
buf := make([]byte, 0, 20+32+32+3+3)
|
|
buf = append(buf, owner.Bytes()...)
|
|
buf = append(buf, poolID[:]...)
|
|
buf = append(buf, salt[:]...)
|
|
var tl, tu [3]byte
|
|
putInt24(tl[:], tickLower)
|
|
putInt24(tu[:], tickUpper)
|
|
buf = append(buf, tl[:]...)
|
|
buf = append(buf, tu[:]...)
|
|
var id [32]byte
|
|
copy(id[:], crypto.Keccak256(buf))
|
|
return id
|
|
}
|
|
|
|
func orderSlot(orderID [32]byte, suffix []byte) common.Hash {
|
|
id := make([]byte, 0, 32+len(suffix))
|
|
id = append(id, orderID[:]...)
|
|
id = append(id, suffix...)
|
|
return makeStorageKey(restingOrderPrefix, id)
|
|
}
|
|
|
|
func lockedReserveKey(owner common.Address, assetID [32]byte) common.Hash {
|
|
id := make([]byte, 0, 20+32)
|
|
id = append(id, owner.Bytes()...)
|
|
id = append(id, assetID[:]...)
|
|
return makeStorageKey(lockedReservePrefix, id)
|
|
}
|
|
|
|
func loadLockedReserve(stateDB stateKV, owner common.Address, assetID [32]byte) *big.Int {
|
|
return new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, lockedReserveKey(owner, assetID)).Bytes())
|
|
}
|
|
|
|
func storeLockedReserve(stateDB stateKV, owner common.Address, assetID [32]byte, amount *big.Int) {
|
|
var w common.Hash
|
|
if amount != nil && amount.Sign() > 0 {
|
|
amount.FillBytes(w[:])
|
|
}
|
|
stateDB.SetState(poolManagerAddr9999, lockedReserveKey(owner, assetID), w)
|
|
}
|
|
|
|
// --- per-owner order index (ownerOrders[owner] = [orderID...], with a count).
|
|
var ownerOrdersCountSuffix = []byte("n")
|
|
|
|
func ownerOrdersCountKey(owner common.Address) common.Hash {
|
|
id := append(owner.Bytes(), ownerOrdersCountSuffix...)
|
|
return makeStorageKey(ownerOrdersPrefix, id)
|
|
}
|
|
|
|
func ownerOrdersAtKey(owner common.Address, i uint64) common.Hash {
|
|
id := make([]byte, 0, 20+8)
|
|
id = append(id, owner.Bytes()...)
|
|
var b [8]byte
|
|
putU64(b[:], i)
|
|
id = append(id, b[:]...)
|
|
return makeStorageKey(ownerOrdersPrefix, id)
|
|
}
|
|
|
|
func loadOwnerOrderCount(stateDB stateKV, owner common.Address) uint64 {
|
|
return new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, ownerOrdersCountKey(owner)).Bytes()).Uint64()
|
|
}
|
|
|
|
// orderIndexedSuffix flags whether an orderID is already present in its owner's
|
|
// index. A single SLOAD/SSTORE on this flag replaces the old O(n) presence scan, so
|
|
// appendOwnerOrder is O(1) state-touch and the cumulative cost of opening N distinct
|
|
// orders is O(N), not O(N^2). (The prior linear scan let an attacker force ~ (G/gas)^2
|
|
// validator SLOADs that the flat ADD gas did not bound — a compute-griefing vector.)
|
|
var orderIndexedSuffix = []byte("i")
|
|
|
|
func orderIndexedKey(orderID [32]byte) common.Hash {
|
|
return orderSlot(orderID, orderIndexedSuffix)
|
|
}
|
|
|
|
// appendOwnerOrder adds orderID to the owner's index iff not already present. The
|
|
// orderID is deterministic, so a re-add of the same range must NOT grow the index;
|
|
// an O(1) membership flag (orderIndexedKey) enforces idempotency without scanning.
|
|
func appendOwnerOrder(stateDB stateKV, owner common.Address, orderID [32]byte) {
|
|
if stateDB.GetState(poolManagerAddr9999, orderIndexedKey(orderID)) != (common.Hash{}) {
|
|
return // already in the index (O(1), no scan).
|
|
}
|
|
n := loadOwnerOrderCount(stateDB, owner)
|
|
stateDB.SetState(poolManagerAddr9999, ownerOrdersAtKey(owner, n), common.BytesToHash(orderID[:]))
|
|
var cnt common.Hash
|
|
new(big.Int).SetUint64(n + 1).FillBytes(cnt[:])
|
|
stateDB.SetState(poolManagerAddr9999, ownerOrdersCountKey(owner), cnt)
|
|
stateDB.SetState(poolManagerAddr9999, orderIndexedKey(orderID), common.Hash{31: 1})
|
|
}
|
|
|
|
// OwnerOrderIDs returns the orderIDs in a maker's index, in insertion order. Used
|
|
// by 0x9996 positionsOf / StateView getOpenOrders. Deterministic StateDB reads.
|
|
func OwnerOrderIDs(stateDB stateKV, owner common.Address) [][32]byte {
|
|
n := loadOwnerOrderCount(stateDB, owner)
|
|
ids := make([][32]byte, 0, n)
|
|
for i := uint64(0); i < n; i++ {
|
|
var id [32]byte
|
|
copy(id[:], stateDB.GetState(poolManagerAddr9999, ownerOrdersAtKey(owner, i)).Bytes())
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func loadRestingOrder(stateDB stateKV, orderID [32]byte) RestingOrder {
|
|
meta := stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderMetaSuffix))
|
|
var o RestingOrder
|
|
o.Status = OrderStatus(meta[0])
|
|
if o.Status == OrderStatusNone {
|
|
return o
|
|
}
|
|
o.Side = MakerSide(meta[1])
|
|
o.TickLower = decodeInt24(meta[2:5])
|
|
o.TickUpper = decodeInt24(meta[5:8])
|
|
o.Owner = common.BytesToAddress(stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderOwnerSuffix)).Bytes())
|
|
copy(o.PoolID[:], stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderPoolSuffix)).Bytes())
|
|
copy(o.LockedAsset[:], stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderAssetSuffix)).Bytes())
|
|
o.LockedAmt = new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderAmtSuffix)).Bytes())
|
|
copy(o.Salt[:], stateDB.GetState(poolManagerAddr9999, orderSlot(orderID, orderSaltSuffix)).Bytes())
|
|
return o
|
|
}
|
|
|
|
func storeRestingOrder(stateDB stateKV, orderID [32]byte, o RestingOrder) {
|
|
var meta common.Hash
|
|
meta[0] = byte(o.Status)
|
|
meta[1] = byte(o.Side)
|
|
putInt24(meta[2:5], o.TickLower)
|
|
putInt24(meta[5:8], o.TickUpper)
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderMetaSuffix), meta)
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderOwnerSuffix), common.BytesToHash(o.Owner.Bytes()))
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderPoolSuffix), common.BytesToHash(o.PoolID[:]))
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderAssetSuffix), common.BytesToHash(o.LockedAsset[:]))
|
|
var amt common.Hash
|
|
if o.LockedAmt != nil && o.LockedAmt.Sign() > 0 {
|
|
o.LockedAmt.FillBytes(amt[:])
|
|
}
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderAmtSuffix), amt)
|
|
stateDB.SetState(poolManagerAddr9999, orderSlot(orderID, orderSaltSuffix), common.BytesToHash(o.Salt[:]))
|
|
}
|
|
|
|
// maker-path errors.
|
|
var (
|
|
ErrMakerZeroDelta = errors.New("dex: modifyLiquidity delta must be non-zero")
|
|
ErrMakerNoMarket = errors.New("dex: modifyLiquidity on an unregistered market (initialize first)")
|
|
ErrMakerBadTickRange = errors.New("dex: modifyLiquidity tickLower must be < tickUpper, both in tick range")
|
|
ErrMakerNotOwner = errors.New("dex: modifyLiquidity cancel/remove: caller is not the order owner")
|
|
ErrMakerAlreadyClosed = errors.New("dex: modifyLiquidity cancel: order already cancelled")
|
|
ErrMakerAmountRange = errors.New("dex: modifyLiquidity delta exceeds uint256")
|
|
)
|
|
|
|
// runSettleModifyLiquidity MOVED to position_commit.go — the LP D-committed rail
|
|
// (ADD commits to D via a C->D object; REMOVE requests a D->C withdraw). The storage
|
|
// helpers below (RestingOrder + the owner index + the per-owner reserve accumulator)
|
|
// are the POSITION RECORD that rail composes, kept here as the single record store.
|
|
|
|
// encodeOrderResult returns (orderID bytes32, remainingLocked uint256) ABI-packed.
|
|
func encodeOrderResult(orderID [32]byte, remaining *big.Int) []byte {
|
|
out := make([]byte, 64)
|
|
copy(out[0:32], orderID[:])
|
|
if remaining != nil && remaining.Sign() > 0 {
|
|
remaining.FillBytes(out[32:64])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// enterCustodyKV / exitCustodyKV take the GLOBAL non-reentrant custody guard via the
|
|
// stateKV surface (the maker path holds a stateKV, not a *PoolManager). They read/
|
|
// write the 0x9999 namespace's custodyGuardKey9999 slot — the ONE flag across the
|
|
// 0x9999 deposit/withdraw/modifyLiquidity/collectPosition entrypoints, so a reentry
|
|
// through any of them trips it. (This is a DISTINCT slot from 0x9010's custodyGuardKey,
|
|
// which lives under 0x9010's own address; the two precompiles do not share a guard.)
|
|
func enterCustodyKV(stateDB stateKV) bool {
|
|
if stateDB.GetState(poolManagerAddr9999, custodyGuardKey9999) != (common.Hash{}) {
|
|
return false
|
|
}
|
|
stateDB.SetState(poolManagerAddr9999, custodyGuardKey9999, common.Hash{31: 1})
|
|
return true
|
|
}
|
|
|
|
func exitCustodyKV(stateDB stateKV) {
|
|
stateDB.SetState(poolManagerAddr9999, custodyGuardKey9999, common.Hash{})
|
|
}
|
|
|
|
// custodyGuardKey9999 is the 0x9999 namespace's single non-reentrant custody flag,
|
|
// stored under the 0x9999 PoolManager address (poolManagerAddr9999). Distinct from
|
|
// the 0x9010 custodyGuardKey (a different slot under 0x9010's own address); ONE slot
|
|
// for the whole 0x9999 custody entrypoint set (deposit/withdraw/modifyLiquidity/
|
|
// collectPosition).
|
|
var custodyGuardKey9999 = makeStorageKey([]byte(settleStateNamespace+"creg"), []byte{0x01})
|
|
|
|
var _ = contract.AccessibleState(nil)
|