mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
native_state.go opens by defining the pot:
custody[a] = operator seed of a
+ sum of value exported to D
- sum of value imported from D
One pot per asset is defensible exactly while that holds, because one pot means
every claim draws on the same number, and the identity is what makes that number
"value C holds because D owns it" — no more, so a claim cannot draw on backing
nobody on D is owed, and no less, so every D holder can be paid.
creditPositionFee was a fourth term. Governance delivered real value and it
raised custody, an LP record's withdrawable, and that owner's reserve, as "the
keeper's per-owner reflection of fees the D-Chain CLOB credited a maker".
The D-Chain credits no maker fees. Its settlement path has no fee engine and
says so in as many words (dex pkg/dchain/settle.go: "deliberately does NOT model
a full fee/tick/lot/minNotional engine"). The function reflected an event that
does not occur.
It could not have worked if it did. An LP collects only by consuming a D->C
claim, and D exports only what a D account holds; value credited on C never
reached D, so nothing on D could export it. The record promised a withdrawable
the rail had no way to deliver, and the real value landed in the shared pot
where it backed everyone's claims instead of that LP's. Its own test had to
hand-write a D->C object for principal+fees — value D never held. The
fabrication was the feature.
It had no ABI encoder, no keeper, and no caller anywhere outside its own tests.
It was the only writer of that fourth term and the only raiser of LockedAmt
outside a real commit. It was also the only position-moving path with no halt
gate, and it needed no atomic capability, so unlike the rest of the rail it was
live on every network today.
Deleted. The identity now holds as written and seedSeamReserve is the only way
value enters custody other than a C->D export. When D grows fees they will be
D-side balance in the maker's own account and will leave by the ordinary export,
like every other unit — the way out already exists, and it is the only one.
The two LP tests that ran a fee credit now assert what is actually true of them:
a collect is bounded by custody, and a commit plus a full collect is
value-neutral.
Co-authored-by: Hanzo Dev <[email protected]>
120 lines
4.4 KiB
Go
120 lines
4.4 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package dex
|
||
|
||
import (
|
||
"math/big"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/holiman/uint256"
|
||
"github.com/luxfi/ids"
|
||
)
|
||
|
||
// custody_identity_test.go pins the sentence native_state.go opens with:
|
||
//
|
||
// custody[a] = operator seed of a
|
||
// + Σ value exported to D (C->D claims written)
|
||
// − Σ value imported from D (D->C claims consumed)
|
||
//
|
||
// One pot per asset is only defensible while that identity holds, because a single pot
|
||
// means every claim draws on the same number. The identity is what makes the number
|
||
// exactly "value C holds because D owns it" — no more, so a claim cannot draw on
|
||
// backing nobody on D is owed, and no less, so every D holder can be paid.
|
||
//
|
||
// It had a fourth term. creditPositionFee raised custody with no D-side crossing, which
|
||
// put real value in the shared pot that no D account owned and wrote an LP a
|
||
// withdrawable the rail could not deliver.
|
||
|
||
// TestCustody_HasNoTermBesidesSeedAndCrossings walks the identity through a full LP
|
||
// cycle and asserts custody equals it at every step.
|
||
func TestCustody_HasNoTermBesidesSeedAndCrossings(t *testing.T) {
|
||
h := newSettleHarness(t)
|
||
h.registerMarket(t)
|
||
native := h.inAssetID()
|
||
db := newPoolStateAdapter(h.state)
|
||
|
||
var seed, exported, imported int64
|
||
|
||
identity := func(where string) {
|
||
t.Helper()
|
||
want := big.NewInt(seed + exported - imported)
|
||
if got := loadCustody(db, native); got.Cmp(want) != 0 {
|
||
t.Fatalf("%s: custody[native] = %s, want seed(%d) + exported(%d) - imported(%d) = %s",
|
||
where, got, seed, exported, imported, want)
|
||
}
|
||
h.vaultInvariantNative(t, where)
|
||
}
|
||
|
||
identity("genesis")
|
||
|
||
// A COMMIT is a C->D export: it debits the LP on C and writes a claim D consumes.
|
||
var salt [32]byte
|
||
salt[31] = 0x1D
|
||
recordID, _ := h.commitNativePosition(t, -60, 60, 1_000, salt)
|
||
exported += 1_000
|
||
identity("after commit")
|
||
|
||
// A COLLECT is a D->C import: it consumes a claim D exported and pays out of custody.
|
||
claim := ids.ID{0xC0, 0x1D}
|
||
h.putDtoCObject(t, h.caller, claim, [32]byte{}, 400)
|
||
if _, err := h.collectNative(claim, 400, [32]byte(recordID)); err != nil {
|
||
t.Fatalf("collect: %v", err)
|
||
}
|
||
imported += 400
|
||
identity("after collect")
|
||
|
||
// A SEED is the one other term, and it is in the identity by name.
|
||
h.fundVaultNativeOut(250)
|
||
seed += 250
|
||
identity("after seed")
|
||
|
||
// Nothing else on the whole 0x9999 surface can move custody. If a new term is ever
|
||
// added, it lands here as a mismatch rather than as an LP who cannot collect.
|
||
remaining := loadCustody(db, native).Int64()
|
||
if remaining != seed+exported-imported {
|
||
t.Fatalf("custody drifted to %d", remaining)
|
||
}
|
||
}
|
||
|
||
// TestCustody_RetiredFeeCreditIsUnreachable pins the deletion. creditPositionFee was a
|
||
// live, governance-gated selector on every network — it needed no atomic capability, so
|
||
// unlike the rest of the rail it was reachable today. Its calldata must now be an
|
||
// unknown selector rather than a path that quietly still exists.
|
||
func TestCustody_RetiredFeeCreditIsUnreachable(t *testing.T) {
|
||
h := newSettleHarness(t)
|
||
h.registerMarket(t)
|
||
|
||
var salt [32]byte
|
||
salt[31] = 0x2D
|
||
recordID, _ := h.commitNativePosition(t, -60, 60, 500, salt)
|
||
|
||
// creditPositionFee(bytes32,address,uint256) — the retired selector, called by the
|
||
// one account that was ever allowed to call it.
|
||
retired := keccak4("creditPositionFee(bytes32,address,uint256)")
|
||
data := make([]byte, 96)
|
||
copy(data[0:32], recordID[:])
|
||
big.NewInt(50).FillBytes(data[64:96])
|
||
|
||
// The operator ALSO delivers the value, so the call fails for the only reason left:
|
||
// there is no such selector. (Without this, an undelivered-value revert would let
|
||
// the test pass while the path still existed.)
|
||
h.state.stateDB.AddBalance(poolManagerAddr9999, uint256.NewInt(50))
|
||
|
||
before := loadCustody(newPoolStateAdapter(h.state), h.inAssetID())
|
||
_, _, err := h.c.Run(h.state, h.operator(), poolManagerAddr9999,
|
||
prependSelector(retired, data), 5_000_000, false)
|
||
if err == nil {
|
||
t.Fatal("the retired fee credit still executes")
|
||
}
|
||
if !strings.Contains(err.Error(), "unknown 0x9999 selector") {
|
||
t.Fatalf("the retired call failed for an incidental reason (%v); it must be an "+
|
||
"unknown selector, or the path is still there", err)
|
||
}
|
||
after := loadCustody(newPoolStateAdapter(h.state), h.inAssetID())
|
||
if after.Cmp(before) != 0 {
|
||
t.Fatalf("the retired call moved custody %s -> %s", before, after)
|
||
}
|
||
}
|