mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
wip: C-side funding rail un-braid (build green)
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+173
-933
File diff suppressed because it is too large
Load Diff
+44
-122
@@ -12,147 +12,69 @@ import (
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// native_events.go emits the ROUTING events for the native C<->D atomic seam. The
|
||||
// atomic shared-memory object alone moves value; these events only tell the keeper
|
||||
// (a relayer with NO custody authority) how to route an order into a D order, or
|
||||
// that a cancel/collect/reclaim was requested. A keeper that drops every event cannot
|
||||
// lose or steal value — the value is already locked in shared memory; the worst case
|
||||
// is a swap settlement that no one builds, which the LOCKER RECLAIMS DIRECTLY via the
|
||||
// reclaimIntent selector once the order's deadline passes (no keeper required — the
|
||||
// reclaim refunds the locked principal from the seam reserve on the locker's own tx).
|
||||
// native_events.go emits the funding rail's C-side signal. The claim in shared
|
||||
// memory is what moves value; this log only tells an indexer — and whoever delivers
|
||||
// the claim — that a crossing happened and under which id.
|
||||
//
|
||||
// Events are emitted from the 0x9999 settlement address so an indexer keyed to the
|
||||
// money path sees the full order->settlement/reclaim lifecycle in one place.
|
||||
// It carries no routing instruction, because there is nothing to route: the claim
|
||||
// names its own beneficiary, so delivering it can only credit that account. Anyone
|
||||
// who drops this event loses visibility, never value.
|
||||
|
||||
// The event names below are the deployed ABI and hash to topic0, so IntentSubmitted
|
||||
// and IntentReclaimed keep their spelling even though the value they carry is an order.
|
||||
var (
|
||||
// IntentSubmitted(bytes32 orderID, bytes32 dChainID, address account, bytes32 assetIn, uint256 amountIn, bytes32 marketID, uint256 minAmountOut, address recipient, uint64 deadline, uint8 kind, uint64 priceLimit, uint8 limitIsUpper)
|
||||
// priceLimit (CLOB quote-per-base float64 bits) + limitIsUpper let the keeper carry
|
||||
// the taker's slippage floor onto the settling relay (the bounded-MEV guard) without
|
||||
// re-deriving it from the V4 sqrt limit.
|
||||
nativeOrderEventSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"IntentSubmitted(bytes32,bytes32,address,bytes32,uint256,bytes32,uint256,address,uint64,uint8,uint64,uint8)")))
|
||||
// CancelSubmitted(bytes32 orderID, bytes32 marketID, address owner)
|
||||
nativeCancelEventSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"CancelSubmitted(bytes32,bytes32,address)")))
|
||||
// CollectSubmitted(bytes32 positionID, bytes32 marketID, address owner)
|
||||
nativeCollectEventSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"CollectSubmitted(bytes32,bytes32,address)")))
|
||||
// IntentReclaimed(bytes32 orderID, address owner, bytes32 assetIn, uint256 amount)
|
||||
nativeReclaimEventSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"IntentReclaimed(bytes32,address,bytes32,uint256)")))
|
||||
)
|
||||
// ClaimExported(bytes32 claimID, bytes32 dChainID, address owner, address beneficiary,
|
||||
// bytes32 asset, uint256 amount). The name is the deployed ABI and hashes to topic0.
|
||||
var claimExportedSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"ClaimExported(bytes32,bytes32,address,address,bytes32,uint256)")))
|
||||
|
||||
// nativeOrderKind distinguishes a taker swap order from an LP position-open
|
||||
// order so the keeper builds the right D order. Carried in the event (non-value).
|
||||
type nativeOrderKind uint8
|
||||
|
||||
const (
|
||||
orderKindSwap nativeOrderKind = 0
|
||||
orderKindPosition nativeOrderKind = 1
|
||||
)
|
||||
|
||||
// emitNativeOrderEvent logs a C->D SWAP order's routing metadata for the keeper.
|
||||
// The orderID is the indexed topic so a keeper subscribes per-order; the rest is
|
||||
// the taker order the keeper submits to D against the locked collateral. Kind is
|
||||
// fixed orderKindSwap — this is the taker rail; the LP rail uses
|
||||
// emitNativePositionCommitEvent (kind orderKindPosition).
|
||||
func emitNativeOrderEvent(stateDB StateDB, orderID, dChainID ids.ID, req OrderRequest, locked uint64) {
|
||||
emitNativeRoutingEvent(stateDB, orderID, dChainID, req, locked, orderKindSwap)
|
||||
}
|
||||
|
||||
// emitNativePositionCommitEvent logs a C->D LP POSITION-COMMIT (DL01) routing event
|
||||
// for the keeper. Identical wire to the swap order event but kind=orderKindPosition,
|
||||
// so the keeper OPENS a funded D position (range = the tick window carried in the
|
||||
// position record) against the committed collateral instead of placing a taker order.
|
||||
// The atomic commit object alone moves the value; this event only routes it into the
|
||||
// D position-open. positionID is the C->D object key (the indexed topic).
|
||||
func emitNativePositionCommitEvent(stateDB StateDB, positionID, dChainID ids.ID, req OrderRequest, locked uint64) {
|
||||
emitNativeRoutingEvent(stateDB, positionID, dChainID, req, locked, orderKindPosition)
|
||||
}
|
||||
|
||||
// emitNativeRoutingEvent is the SINGLE DRY emitter for both C->D rails: it logs the
|
||||
// (account, assetIn, locked, marketID, minAmountOut, recipient, deadline, kind)
|
||||
// routing metadata under the object id. kind selects the rail (swap vs position) so
|
||||
// the keeper builds the right D op; the value-bearing identity is in the staged
|
||||
// atomic object, never here.
|
||||
func emitNativeRoutingEvent(stateDB StateDB, objectID, dChainID ids.ID, req OrderRequest, locked uint64, kind nativeOrderKind) {
|
||||
data := make([]byte, 0, 9*32)
|
||||
data = append(data, abiEncodeAddress(req.Account)...)
|
||||
data = append(data, abiEncodeBytes32(req.AssetIn)...)
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(locked))...)
|
||||
data = append(data, abiEncodeBytes32(req.MarketID)...)
|
||||
min := req.MinAmountOut
|
||||
if min == nil {
|
||||
min = big.NewInt(0)
|
||||
}
|
||||
data = append(data, abiEncodeBigInt(min)...)
|
||||
data = append(data, abiEncodeAddress(req.Recipient)...)
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(req.Deadline))...)
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(uint64(kind)))...)
|
||||
// Slippage floor for the keeper to carry onto the settling relay (bounded-MEV guard):
|
||||
// priceLimit is the CLOB quote-per-base limit as float64 bits, limitIsUpper its side.
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(req.PriceLimit))...)
|
||||
var limitFlag uint64
|
||||
if limitIsUpper(req.Side) {
|
||||
limitFlag = 1
|
||||
}
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(limitFlag))...)
|
||||
// emitExportEvent logs a C->D crossing under its claim id (the indexed topic), from
|
||||
// the 0x9999 money path so an indexer sees custody in and custody out in one place.
|
||||
func emitExportEvent(stateDB StateDB, claimID, dChainID ids.ID, owner, beneficiary common.Address, asset [32]byte, amount uint64) {
|
||||
data := make([]byte, 0, 4*32)
|
||||
data = append(data, abiEncodeAddress(owner)...)
|
||||
data = append(data, abiEncodeAddress(beneficiary)...)
|
||||
data = append(data, abiEncodeBytes32(asset)...)
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(amount))...)
|
||||
stateDB.AddLog(ðtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
nativeOrderEventSig,
|
||||
common.BytesToHash(objectID[:]),
|
||||
claimExportedSig,
|
||||
common.BytesToHash(claimID[:]),
|
||||
common.BytesToHash(dChainID[:]),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// emitNativeCancelEvent logs a cancel request for the keeper to forward to D.
|
||||
func emitNativeCancelEvent(stateDB StateDB, orderID ids.ID, marketID [32]byte, owner common.Address) {
|
||||
data := abiEncodeAddress(owner)
|
||||
stateDB.AddLog(ðtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
nativeCancelEventSig,
|
||||
common.BytesToHash(orderID[:]),
|
||||
common.BytesToHash(marketID[:]),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
// --- position lifecycle notifications ---------------------------------------------
|
||||
//
|
||||
// A position lives on D; these two logs announce a C-side record transition so an
|
||||
// indexer (and whoever drives the D-side position) sees it. They carry no value and
|
||||
// authorize nothing — the value for a withdraw arrives as an ordinary claim on the
|
||||
// funding rail, like every other crossing.
|
||||
|
||||
// PositionClosing(bytes32 positionID, bytes32 marketID, address owner)
|
||||
var positionClosingSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"CancelSubmitted(bytes32,bytes32,address)")))
|
||||
|
||||
// PositionCollecting(bytes32 positionID, bytes32 marketID, address owner)
|
||||
var positionCollectingSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"CollectSubmitted(bytes32,bytes32,address)")))
|
||||
|
||||
func emitPositionClosing(stateDB StateDB, positionID ids.ID, marketID [32]byte, owner common.Address) {
|
||||
emitPositionEvent(stateDB, positionClosingSig, positionID, marketID, owner)
|
||||
}
|
||||
|
||||
// emitNativeCollectEvent logs a collect request for the keeper to forward to D.
|
||||
func emitNativeCollectEvent(stateDB StateDB, positionID ids.ID, marketID [32]byte, owner common.Address) {
|
||||
data := abiEncodeAddress(owner)
|
||||
func emitPositionCollecting(stateDB StateDB, positionID ids.ID, marketID [32]byte, owner common.Address) {
|
||||
emitPositionEvent(stateDB, positionCollectingSig, positionID, marketID, owner)
|
||||
}
|
||||
|
||||
func emitPositionEvent(stateDB StateDB, sig common.Hash, positionID ids.ID, marketID [32]byte, owner common.Address) {
|
||||
stateDB.AddLog(ðtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
nativeCollectEventSig,
|
||||
sig,
|
||||
common.BytesToHash(positionID[:]),
|
||||
common.BytesToHash(marketID[:]),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// emitNativeReclaimEvent logs a completed order reclaim: the locker recovered
|
||||
// `amount` of `assetIn` for the stranded `orderID` (deadline-gated). This is a
|
||||
// settled value-movement signal (the refund already happened on this tx), not a
|
||||
// keeper request — an indexer surfaces it as the order's terminal exit.
|
||||
func emitNativeReclaimEvent(stateDB StateDB, orderID ids.ID, owner common.Address, assetIn [32]byte, amount uint64) {
|
||||
data := make([]byte, 0, 3*32)
|
||||
data = append(data, abiEncodeAddress(owner)...)
|
||||
data = append(data, abiEncodeBytes32(assetIn)...)
|
||||
data = append(data, abiEncodeBigInt(new(big.Int).SetUint64(amount))...)
|
||||
stateDB.AddLog(ðtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
nativeReclaimEventSig,
|
||||
common.BytesToHash(orderID[:]),
|
||||
},
|
||||
Data: data,
|
||||
Data: abiEncodeAddress(owner),
|
||||
})
|
||||
}
|
||||
|
||||
+13
-77
@@ -4,7 +4,6 @@
|
||||
package dex
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/database"
|
||||
@@ -13,46 +12,6 @@ import (
|
||||
"github.com/luxfi/vm/chains/atomic"
|
||||
)
|
||||
|
||||
// SeamPendingTrait is the FIXED, owner-agnostic discovery trait every C->D swap
|
||||
// order carries in shared memory, so the D-Chain proposer can ENUMERATE pending
|
||||
// orders without already knowing their owners.
|
||||
//
|
||||
// WHY IT IS NEEDED. atomic.SharedMemory.Indexed can only return keys that possess
|
||||
// a trait the caller names. The per-owner trait this flush already writes is
|
||||
// useless for discovery: D would have to know the owner set to query for it. One
|
||||
// shared constant that the writer tags and the reader queries is what closes
|
||||
// that, and it is domain-separated to 32 bytes so it can never collide with a
|
||||
// 20-byte owner trait.
|
||||
//
|
||||
// CROSS-REPO CONTRACT. This value is byte-pinned against the D-Chain's own
|
||||
// constant (dex/pkg/dchain SeamPendingTrait, same domain string) the same way the
|
||||
// object wire is pinned by golden vector. The two repos cannot import each other,
|
||||
// so drift here is a silent break — D would enumerate a trait C never writes, find
|
||||
// nothing, and every swap would sit unmatched forever with no error anywhere. The
|
||||
// golden test in this package is what makes that loud.
|
||||
//
|
||||
// THE .v2 DOMAIN IS THE FLAG-DAY SWITCH. A v1 order object carried VALUE ONLY
|
||||
// (69 bytes, no operation); a v2 order carries value + operation (118 bytes), and
|
||||
// a node that cannot read the operation must never import the value — it would have
|
||||
// to invent the taker's market/side/limit. Rotating the domain makes the two
|
||||
// generations mutually invisible instead of mutually confusing:
|
||||
//
|
||||
// - a v1 D node enumerates the v1 trait, finds no v2 object, emits no import, and
|
||||
// goes on validating every non-seam block normally. Inert, never divergent.
|
||||
// - a v2 D node enumerates the v2 trait, so a stale v1 object written by a
|
||||
// not-yet-upgraded C node is never imported. Fail closed; the taker's principal
|
||||
// is reclaimable on C after the order deadline.
|
||||
//
|
||||
// So neither generation can ever execute the other's order, and the changeover
|
||||
// costs liveness on the seam alone — never a divergent state root.
|
||||
//
|
||||
// The literal is the hash input: it stays .intent.pending.v2 so both repos keep
|
||||
// deriving the same trait.
|
||||
var SeamPendingTrait = func() []byte {
|
||||
d := sha256.Sum256([]byte("lux.dex.native.intent.pending.v2"))
|
||||
return d[:]
|
||||
}()
|
||||
|
||||
// native_staging.go solves the CROSS-DOMAIN ATOMICITY problem of the native seam.
|
||||
//
|
||||
// THE PROBLEM (RED-found by self-review): a precompile's StateDB writes are covered
|
||||
@@ -74,7 +33,7 @@ var SeamPendingTrait = func() []byte {
|
||||
// commits; nothing needs it mid-block, so deferral costs nothing.)
|
||||
//
|
||||
// STAGED RECORD LAYOUTS (in StateDB, under the 0x9999 namespace):
|
||||
// - C->D PUT: stagePutPrefix | seq -> dChainID(32) | key(32) | object(69|118)
|
||||
// - C->D PUT: stagePutPrefix | seq -> dChainID(32) | key(32) | claim(60)
|
||||
// - D->C REMOVE: stageRemovePrefix | seq -> dChainID(32) | key(32)
|
||||
// - a monotonic per-block seq counter (stageSeqKey) orders them deterministically.
|
||||
//
|
||||
@@ -101,9 +60,8 @@ func setStageSeq(stateDB stateKV, n uint64) {
|
||||
|
||||
// stagePutKey / stageRemoveKey key a staged op by its sequence number. We store the
|
||||
// variable-length record across consecutive 32-byte slots (the EVM word size). A
|
||||
// staged Put record is version(1)|dChainID(32)|key(32)|object, and the object is 69
|
||||
// bytes (a value object) or 118 (a swap order) — so the record spans 4 or 6 words.
|
||||
// The length lives in slot 0, so no caller assumes a fixed word count.
|
||||
// staged Put record is version(1)|dChainID(32)|key(32)|claim(60). The length lives
|
||||
// in slot 0, so no caller assumes a fixed word count.
|
||||
func stageSlotKey(prefix []byte, seq uint64, word int) common.Hash {
|
||||
id := make([]byte, 0, 16)
|
||||
var s [8]byte
|
||||
@@ -134,9 +92,7 @@ const (
|
||||
// with any other version is malformed and FAILS block accept (never skipped).
|
||||
const stagedOpVersion byte = 1
|
||||
|
||||
// stageAtomicPut stages a C->D Put (revert-aware): version|dChainID|key|object. The
|
||||
// object is either the 69-byte value object (LP commit) or the 118-byte swap order
|
||||
// (value + the taker's operation); the flush binds whichever was staged.
|
||||
// stageAtomicPut stages a C->D Put (revert-aware): version|dChainID|key|claim.
|
||||
func stageAtomicPut(stateDB stateKV, dChainID ids.ID, key ids.ID, object []byte) {
|
||||
seq := stageSeq(stateDB)
|
||||
writeBytesToSlots(stateDB, stagePutPrefix, seq, packPut(dChainID, key, object))
|
||||
@@ -162,7 +118,7 @@ func markStageKind(stateDB stateKV, seq uint64, kind byte) {
|
||||
// leading version byte so a future layout change is an explicit new version (and an
|
||||
// unknown version fails the flush rather than silently mis-decoding value).
|
||||
//
|
||||
// Put: version(1) | dChainID(32) | key(32) | object(69 value | 118 swap order)
|
||||
// Put: version(1) | dChainID(32) | key(32) | claim(60)
|
||||
// Remove: version(1) | dChainID(32) | key(32)
|
||||
func packPut(dChainID, key ids.ID, object []byte) []byte {
|
||||
b := make([]byte, 1+32+32+len(object))
|
||||
@@ -308,44 +264,24 @@ func collectRange(stateDB stateKV, fromSeq, toSeq uint64) (map[ids.ID]*atomic.Re
|
||||
switch kindWord[31] {
|
||||
case stageKindPut:
|
||||
rec := readBytesFromSlots(stateDB, stagePutPrefix, i)
|
||||
// version(1)|dChainID(32)|key(32)|object(>=61). Reject malformed (fatal).
|
||||
if len(rec) < 1+32+32+exportedOutputSize9999 || rec[0] != stagedOpVersion {
|
||||
// version(1)|dChainID(32)|key(32)|claim(60). Reject malformed (fatal).
|
||||
if len(rec) != 1+32+32+claimSize || rec[0] != stagedOpVersion {
|
||||
return nil, ErrStagedOpMalformed
|
||||
}
|
||||
var dChainID, key ids.ID
|
||||
copy(dChainID[:], rec[1:33])
|
||||
copy(key[:], rec[33:65])
|
||||
object := rec[65:]
|
||||
// The object must be a well-formed atomic object. Its VALUE HEAD is the
|
||||
// same 69 bytes at both canonical widths (a 69-byte LP commit / D->C
|
||||
// settlement, a 118-byte C->D swap order), and the owner Trait is read
|
||||
// from that head — so the destination indexes the object by recipient
|
||||
// exactly as before, for both widths. Any other width is fatal.
|
||||
if len(object) != exportedOutputSize9999 && len(object) != orderObjectSize9999 {
|
||||
return nil, ErrStagedOpMalformed
|
||||
}
|
||||
rail, owner, _, _, _, ok := decodeObjectHead(object[:exportedOutputSize9999])
|
||||
// The object must be a well-formed claim. Its beneficiary is written as the
|
||||
// destination Trait, so the far side indexes the claim by recipient — the
|
||||
// only lookup the rail needs. A deposit is delivered by whoever holds the
|
||||
// claim id; nothing has to scan for it. Any other width is fatal.
|
||||
beneficiary, _, _, ok := decodeClaim(object)
|
||||
if !ok {
|
||||
return nil, ErrStagedOpMalformed
|
||||
}
|
||||
req := forChain(dChainID)
|
||||
traits := [][]byte{append([]byte(nil), owner[:]...)}
|
||||
// DISCOVERY TRAIT (swap rail only). The owner trait alone cannot be
|
||||
// enumerated: atomic.SharedMemory.Indexed looks up keys by a trait the
|
||||
// caller already knows, and D does not know the owner set in advance.
|
||||
// So a real Phase-A order tagged only by owner is INVISIBLE to the
|
||||
// D-Chain's autonomous import drive — it enumerates by this fixed,
|
||||
// owner-agnostic tag inside BuildBlock. Without it the drive finds
|
||||
// nothing, no order is ever imported, and no fill is ever produced,
|
||||
// which is a second reason the seam is dark that survives fixing the
|
||||
// transport.
|
||||
//
|
||||
// Swap rail only, deliberately: the LP rail's collect/withdraw
|
||||
// lifecycle is driven by the position record, not by enumeration, and
|
||||
// tagging it would put LP objects into the swap drive's queue.
|
||||
if rail == railSwap {
|
||||
traits = append(traits, append([]byte(nil), SeamPendingTrait...))
|
||||
}
|
||||
traits := [][]byte{append([]byte(nil), beneficiary[:]...)}
|
||||
req.PutRequests = append(req.PutRequests, &atomic.Element{
|
||||
Key: append([]byte(nil), key[:]...),
|
||||
Value: append([]byte(nil), object...),
|
||||
|
||||
+48
-227
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// native_state.go holds the DURABLE state of the native C<->D atomic seam: the
|
||||
// configured D-Chain target id, the C->D order replay set, and the D->C
|
||||
// settlement consumed set. All live under the 0x9999 settlement namespace
|
||||
// native_state.go holds the DURABLE state of the C<->D funding rail: the C->D
|
||||
// written-claim set, the D->C consumed-claim set, and custody. All live under the
|
||||
// 0x9999 settlement namespace
|
||||
// (dex.precompile.v1.9999.*) so the money path's state is one auditable region,
|
||||
// and all are content-addressed by the cross-chain object id so a re-executed /
|
||||
// reorged block replays identically on every validator (consensus-shared,
|
||||
@@ -26,261 +26,82 @@ import (
|
||||
// to the precompile via contract.AtomicState.DChainID(). ids.Empty (no dexvm on this
|
||||
// network) closes the on-ramp; the client refuses to move value rather than guess a peer.
|
||||
|
||||
// --- C->D order replay set: an order id is submitted at most once. Keyed by the
|
||||
// order id (== the object's shared-memory key), value = blockNumber+1 sentinel.
|
||||
// The "intent" suffix is on-disk key bytes: renaming it orphans the replay set.
|
||||
var orderSubmittedPrefix = []byte(settleStateNamespace + "intent")
|
||||
// --- C->D written set: a claim id is written at most once. Keyed by the claim id
|
||||
// (== the object's shared-memory key), value = blockNumber+1 sentinel.
|
||||
var claimWrittenPrefix = []byte(settleStateNamespace + "written")
|
||||
|
||||
func orderSubmittedKey(orderID ids.ID) common.Hash {
|
||||
return makeStorageKey(orderSubmittedPrefix, orderID[:])
|
||||
func claimWrittenKey(claimID ids.ID) common.Hash {
|
||||
return makeStorageKey(claimWrittenPrefix, claimID[:])
|
||||
}
|
||||
|
||||
func isOrderSubmitted(stateDB stateKV, orderID ids.ID) bool {
|
||||
return stateDB.GetState(poolManagerAddr9999, orderSubmittedKey(orderID)) != (common.Hash{})
|
||||
func isClaimWritten(stateDB stateKV, claimID ids.ID) bool {
|
||||
return stateDB.GetState(poolManagerAddr9999, claimWrittenKey(claimID)) != (common.Hash{})
|
||||
}
|
||||
|
||||
func markOrderSubmitted(stateDB stateKV, orderID ids.ID, blockNumber uint64) {
|
||||
func markClaimWritten(stateDB stateKV, claimID ids.ID, blockNumber uint64) {
|
||||
var v common.Hash
|
||||
uint256.NewInt(blockNumber + 1).WriteToSlice(v[:])
|
||||
stateDB.SetState(poolManagerAddr9999, orderSubmittedKey(orderID), v)
|
||||
stateDB.SetState(poolManagerAddr9999, claimWrittenKey(claimID), v)
|
||||
}
|
||||
|
||||
// --- D->C settlement consumed set: a settlement object is consumed at most once
|
||||
// (the RED H1 exactly-once property the BLS path also required, now over the
|
||||
// atomic object id). Keyed by the object id, value = blockNumber+1 sentinel.
|
||||
var settlementConsumedPrefix = []byte(settleStateNamespace + "settled")
|
||||
// --- D->C consumed set: a claim is consumed at most once. Keyed by the claim id,
|
||||
// value = blockNumber+1 sentinel. This is the exactly-once property the whole rail
|
||||
// rests on: the mark and the credit land together or neither does.
|
||||
var claimConsumedPrefix = []byte(settleStateNamespace + "consumed")
|
||||
|
||||
func settlementConsumedKey(outputID ids.ID) common.Hash {
|
||||
return makeStorageKey(settlementConsumedPrefix, outputID[:])
|
||||
func claimConsumedKey(claimID ids.ID) common.Hash {
|
||||
return makeStorageKey(claimConsumedPrefix, claimID[:])
|
||||
}
|
||||
|
||||
func isSettlementConsumed(stateDB stateKV, outputID ids.ID) bool {
|
||||
return stateDB.GetState(poolManagerAddr9999, settlementConsumedKey(outputID)) != (common.Hash{})
|
||||
func isClaimConsumed(stateDB stateKV, claimID ids.ID) bool {
|
||||
return stateDB.GetState(poolManagerAddr9999, claimConsumedKey(claimID)) != (common.Hash{})
|
||||
}
|
||||
|
||||
func markSettlementConsumed(stateDB stateKV, outputID ids.ID, blockNumber uint64) {
|
||||
func markClaimConsumed(stateDB stateKV, claimID ids.ID, blockNumber uint64) {
|
||||
var v common.Hash
|
||||
uint256.NewInt(blockNumber + 1).WriteToSlice(v[:])
|
||||
stateDB.SetState(poolManagerAddr9999, settlementConsumedKey(outputID), v)
|
||||
stateDB.SetState(poolManagerAddr9999, claimConsumedKey(claimID), v)
|
||||
}
|
||||
|
||||
// --- Per-order SWAP escrow record (the swap-rail analog of the LP RestingOrder).
|
||||
// ONE record, created at SubmitSwapOrder, drives BOTH the per-taker settlement cap
|
||||
// (MEDIUM — bound a settlement credit by this taker's OWN remaining locked principal,
|
||||
// so an over-export for taker X can never draw on other takers' pooled tokenIn) AND
|
||||
// the deadline reclaim (HIGH — refund the remaining principal to the locker once the
|
||||
// deadline passes and D has not settled). It is content-addressed by the order id
|
||||
// (the C->D object key), consensus-shared, durable, and reverted atomically with the
|
||||
// tx — exactly like every other 0x9999 native-seam record.
|
||||
//
|
||||
// FIELDS (each its own fine-grained slot under the order id; no global hot write):
|
||||
// - owner : the taker who locked the principal (the C tx caller). The sole
|
||||
// account whose settlement may draw against this order and the sole
|
||||
// reclaim payee. Equality-checked against the recorded D->C object's
|
||||
// owner so a settlement cannot bind a victim's order.
|
||||
// - assetIn : the LOCKED (input) asset — the numeraire the remaining principal is
|
||||
// measured in and the asset the reclaim refunds.
|
||||
// - remaining : the still-unsettled locked principal (in assetIn units). Starts at
|
||||
// the value actually locked; each settlement credit and the reclaim
|
||||
// decrement it. A settlement is capped by it; reclaim drains it to 0.
|
||||
// - deadline : the swap deadline (block timestamp). 0 = none. Phase-B settlement is
|
||||
// refused after it; reclaim is allowed only after it.
|
||||
// - status : Open (settlable / reclaimable) or Reclaimed (terminal — no further
|
||||
// settlement or reclaim).
|
||||
// - priceLimit : the taker's OWN worst-acceptable CLOB price (quote-per-base, float64
|
||||
// bits), derived at submit from the taker's V4 SqrtPriceLimitX96 (priceLimitToCLOB).
|
||||
// limitIsUpper says which side it bounds (true = a BUY ceiling, false = a SELL floor).
|
||||
// ImportSettlement enforces it on the PROCEEDS leg against the realized fill price
|
||||
// (out/spent) — INDEPENDENTLY of the keeper-relayed RelayOrderTx.PriceLimit — so a
|
||||
// keeper that zeroes the relay limit to sandwich the taker still produces a proceeds
|
||||
// object C refuses to credit. This is the TAKER-AUTHENTICATED MEV floor: the binding
|
||||
// authority is the taker's recorded order, not the keeper's relay. 0 = no limit.
|
||||
var (
|
||||
swapOrderPrefix = []byte(settleStateNamespace + "swint") // swapOrder[orderID][suffix]
|
||||
swapOrderReclaimedPrefix = []byte(settleStateNamespace + "swrcl") // reclaimed-set: orderID -> blockNumber+1
|
||||
)
|
||||
|
||||
// swapOrderStatus is the per-order lifecycle: an order is Open from submission
|
||||
// until either fully settled/reclaimed. Reclaimed is terminal (one-time, replay-safe).
|
||||
type swapOrderStatus uint8
|
||||
|
||||
const (
|
||||
swapOrderNone swapOrderStatus = 0 // never submitted (no record)
|
||||
swapOrderOpen swapOrderStatus = 1 // submitted; remaining principal settlable/reclaimable
|
||||
swapOrderReclaimed swapOrderStatus = 2 // principal reclaimed to the locker (terminal)
|
||||
)
|
||||
|
||||
// swapOrderRecord is the in-memory image of the per-order escrow record.
|
||||
type swapOrderRecord struct {
|
||||
Owner common.Address
|
||||
AssetIn [32]byte
|
||||
Remaining uint64
|
||||
Deadline uint64
|
||||
Status swapOrderStatus
|
||||
// PriceLimit is the taker's OWN worst-acceptable CLOB price (quote-per-base, IEEE-754
|
||||
// float64 bits) recorded at SubmitSwapOrder from the taker's V4 SqrtPriceLimitX96.
|
||||
// 0 = no limit. ImportSettlement enforces it against the realized proceeds price so the
|
||||
// floor is TAKER-authenticated, not keeper-relayed (the MEV sandwich fix).
|
||||
PriceLimit uint64
|
||||
// LimitIsUpper is the limit's side: true = a BUY ceiling (reject realized > limit),
|
||||
// false = a SELL floor (reject realized < limit). Matches priceLimitToCLOB's convention.
|
||||
LimitIsUpper bool
|
||||
}
|
||||
|
||||
// swap-order record slot suffixes (one concern per slot, mirroring the LP order).
|
||||
var (
|
||||
swapOrderOwnerSuffix = []byte("o") // owner (address)
|
||||
swapOrderAssetSuffix = []byte("a") // assetIn (bytes32)
|
||||
swapOrderMetaSuffix = []byte("m") // status(1) | limitIsUpper(1) | deadline(uint64, big-endian in low 8 bytes)
|
||||
swapOrderRemainSuffix = []byte("r") // remaining principal (uint64)
|
||||
swapOrderLimitSuffix = []byte("l") // taker's recorded CLOB price limit (float64 bits, uint64)
|
||||
)
|
||||
|
||||
func swapOrderSlot(orderID ids.ID, suffix []byte) common.Hash {
|
||||
id := make([]byte, 0, 32+len(suffix))
|
||||
id = append(id, orderID[:]...)
|
||||
id = append(id, suffix...)
|
||||
return makeStorageKey(swapOrderPrefix, id)
|
||||
}
|
||||
|
||||
// putSwapOrderRecord persists a swap-order escrow record across its slots. The meta
|
||||
// word packs status(byte 0) | limitIsUpper(byte 1) | deadline(low 8 bytes); the taker's
|
||||
// recorded price limit rides its own slot so the floor is durable, consensus-shared, and
|
||||
// reverted atomically with the submit tx — exactly like every other field of the record.
|
||||
func putSwapOrderRecord(stateDB stateKV, orderID ids.ID, r swapOrderRecord) {
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderOwnerSuffix),
|
||||
common.BytesToHash(r.Owner.Bytes()))
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderAssetSuffix),
|
||||
common.BytesToHash(r.AssetIn[:]))
|
||||
var meta common.Hash
|
||||
meta[0] = byte(r.Status)
|
||||
if r.LimitIsUpper {
|
||||
meta[1] = 1
|
||||
}
|
||||
putU64(meta[24:32], r.Deadline)
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderMetaSuffix), meta)
|
||||
var rem common.Hash
|
||||
putU64(rem[24:32], r.Remaining)
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderRemainSuffix), rem)
|
||||
var lim common.Hash
|
||||
putU64(lim[24:32], r.PriceLimit)
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderLimitSuffix), lim)
|
||||
}
|
||||
|
||||
// loadSwapOrderRecord reads a swap-order record. Status swapOrderNone means no
|
||||
// such order was ever submitted (every slot zero).
|
||||
func loadSwapOrderRecord(stateDB stateKV, orderID ids.ID) swapOrderRecord {
|
||||
meta := stateDB.GetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderMetaSuffix))
|
||||
var r swapOrderRecord
|
||||
r.Status = swapOrderStatus(meta[0])
|
||||
r.LimitIsUpper = meta[1] != 0
|
||||
r.Deadline = bytesToU64(meta[24:32])
|
||||
if r.Status == swapOrderNone {
|
||||
return r
|
||||
}
|
||||
r.Owner = common.BytesToAddress(stateDB.GetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderOwnerSuffix)).Bytes())
|
||||
copy(r.AssetIn[:], stateDB.GetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderAssetSuffix)).Bytes())
|
||||
r.Remaining = bytesToU64(stateDB.GetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderRemainSuffix)).Bytes())
|
||||
r.PriceLimit = bytesToU64(stateDB.GetState(poolManagerAddr9999, swapOrderSlot(orderID, swapOrderLimitSuffix)).Bytes())
|
||||
return r
|
||||
}
|
||||
|
||||
// --- Reclaim-set: an order's principal is reclaimed at most once. Keyed by the
|
||||
// order id, value = blockNumber+1 sentinel. The record's Status (Reclaimed) is the
|
||||
// authoritative terminal marker; this set is the explicit one-time replay guard the
|
||||
// reclaim path checks (mirroring the settlement-consumed set), so a re-submitted /
|
||||
// reorged reclaim tx is a guaranteed no-op rather than a second refund.
|
||||
func swapOrderReclaimedKey(orderID ids.ID) common.Hash {
|
||||
return makeStorageKey(swapOrderReclaimedPrefix, orderID[:])
|
||||
}
|
||||
|
||||
func isSwapOrderReclaimed(stateDB stateKV, orderID ids.ID) bool {
|
||||
return stateDB.GetState(poolManagerAddr9999, swapOrderReclaimedKey(orderID)) != (common.Hash{})
|
||||
}
|
||||
|
||||
func markSwapOrderReclaimed(stateDB stateKV, orderID ids.ID, blockNumber uint64) {
|
||||
var v common.Hash
|
||||
uint256.NewInt(blockNumber + 1).WriteToSlice(v[:])
|
||||
stateDB.SetState(poolManagerAddr9999, swapOrderReclaimedKey(orderID), v)
|
||||
}
|
||||
|
||||
// --- Seam reserve: the seam's OWN per-asset pot inside the 0x9999 vault, tracked
|
||||
// --- Custody: the funding rail's per-asset pot inside the 0x9999 vault, tracked
|
||||
// SEPARATELY from the depositor pot (settleVault) and the maker-locked pot
|
||||
// (makerLockedVault). This is the FIX-3 conservation decomplection: the seam (Phase-A
|
||||
// lock + Phase-B credit) and custody (deposit/withdraw) both move value through the
|
||||
// SAME 0x9999 vault ACCOUNT, but their CLAIMS on it are distinct values and must not
|
||||
// raid each other. So:
|
||||
// (makerLockedVault). The three are distinct CLAIMS on the same vault account and
|
||||
// must not raid each other:
|
||||
//
|
||||
// seamReserve[a] = operator seed of a + Σ Phase-A tokenIn locks of a
|
||||
// − Σ Phase-B tokenOut credits of a
|
||||
// settleVault[a] = Σ depositorClaim[*][a] (the depositor pot)
|
||||
// makerLockedVault[a] = Σ makers' locked reserve of a (the maker pot)
|
||||
// custody[a] = operator seed of a
|
||||
// + Σ value exported to D (C->D claims written)
|
||||
// − Σ value imported from D (D->C claims consumed)
|
||||
// settleVault[a] = Σ depositorClaim[*][a] (the depositor pot)
|
||||
// makerLockedVault[a] = Σ makers' locked reserve of a (the maker pot)
|
||||
//
|
||||
// THE VAULT-ACCOUNT INVARIANT (per asset a):
|
||||
//
|
||||
// GetBalance(0x9999)/ERC20 holding of a == settleVault[a] + makerLockedVault[a] + seamReserve[a]
|
||||
// GetBalance(0x9999)/ERC20 holding of a == settleVault[a] + makerLockedVault[a] + custody[a]
|
||||
//
|
||||
// A Phase-B settlement credit draws ONLY on seamReserve[a] (creditSettlementOutput),
|
||||
// so it can NEVER pay a taker out of a depositor's claim or a maker's locked reserve.
|
||||
// A withdraw draws ONLY on settleVault[a], so it can NEVER strand a backed settlement.
|
||||
// The pots are orthogonal; the blast radius of one subsystem can't reach another.
|
||||
var seamReservePrefix = []byte(settleStateNamespace + "seam") // seamReserve[asset] -> seam-owned holdings
|
||||
// An import draws ONLY on custody[a], so it can never pay out of a depositor's claim
|
||||
// or a maker's reserve; a withdraw draws ONLY on settleVault[a], so it can never
|
||||
// strand a backed import.
|
||||
//
|
||||
// THERE IS ONE CUSTODY POT, NOT ONE PER PRODUCT. It used to be two — a swap pot and
|
||||
// an LP pot — and the object carried a lane byte saying which one a claim could
|
||||
// draw. That byte was the only thing keeping the two apart, and it made the funding
|
||||
// rail know what the value was FOR. A rail that moves ownership does not: "value C
|
||||
// holds because D owns it" is one quantity per asset, so it is one number. Splitting
|
||||
// it created the cross-lane drain the byte then had to defend against.
|
||||
var custodyPrefix = []byte(settleStateNamespace + "cust") // custody[asset]
|
||||
|
||||
func seamReserveKey(assetID [32]byte) common.Hash {
|
||||
return makeStorageKey(seamReservePrefix, assetID[:])
|
||||
func custodyKey(assetID [32]byte) common.Hash {
|
||||
return makeStorageKey(custodyPrefix, assetID[:])
|
||||
}
|
||||
|
||||
func loadSeamReserve(stateDB stateKV, assetID [32]byte) *big.Int {
|
||||
return new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, seamReserveKey(assetID)).Bytes())
|
||||
func loadCustody(stateDB stateKV, assetID [32]byte) *big.Int {
|
||||
return new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, custodyKey(assetID)).Bytes())
|
||||
}
|
||||
|
||||
func storeSeamReserve(stateDB stateKV, assetID [32]byte, amount *big.Int) {
|
||||
func storeCustody(stateDB stateKV, assetID [32]byte, amount *big.Int) {
|
||||
var w common.Hash
|
||||
if amount != nil && amount.Sign() > 0 {
|
||||
amount.FillBytes(w[:])
|
||||
}
|
||||
stateDB.SetState(poolManagerAddr9999, seamReserveKey(assetID), w)
|
||||
}
|
||||
|
||||
// --- Committed-positions reserve: the LP RAIL's OWN per-asset pot inside the 0x9999
|
||||
// vault, tracked SEPARATELY from the depositor pot (settleVault), the legacy maker
|
||||
// C-side pot (makerLockedVault), and the swap-rail pot (seamReserve). This is the
|
||||
// fourth orthogonal claim on the SAME 0x9999 vault account — the FIX-3 decomplection
|
||||
// extended to the LP rail. It tracks the C-side BACKING of value COMMITTED to live D
|
||||
// positions (the DCommitted state):
|
||||
//
|
||||
// committedPositions[a] = operator seed of a (fee backing)
|
||||
// + Σ LP principal commits of a (C->D DL01 commits)
|
||||
// − Σ collect/decrease/burn credits of a (D->C imports)
|
||||
//
|
||||
// THE VAULT-ACCOUNT INVARIANT becomes (per asset a):
|
||||
//
|
||||
// realHolding(0x9999, a) == settleVault[a] + makerLockedVault[a]
|
||||
// + seamReserve[a] + committedPositions[a]
|
||||
//
|
||||
// An LP commit moves value from the caller's CSpendable EVM balance INTO
|
||||
// committedPositions (DCommitted): the unit is no longer spendable on C and a C->D
|
||||
// commit object backs a funded D position. A collect/decrease/burn draws ONLY on
|
||||
// committedPositions (creditPositionCollect), so it can NEVER pay an LP out of a
|
||||
// depositor's claim, a maker's legacy lock, or the swap rail's seam reserve — and a
|
||||
// swap settlement (which draws ONLY seamReserve) can never raid an LP's committed
|
||||
// principal. The pots are orthogonal; the blast radius of one rail can't reach
|
||||
// another. The NEVER-BOTH invariant (a unit is CSpendable XOR DCommitted) is the
|
||||
// commit debit + the committedPositions credit being a single balanced move.
|
||||
var committedPositionsPrefix = []byte(settleStateNamespace + "cpos") // committedPositions[asset]
|
||||
|
||||
func committedPositionsKey(assetID [32]byte) common.Hash {
|
||||
return makeStorageKey(committedPositionsPrefix, assetID[:])
|
||||
}
|
||||
|
||||
func loadCommittedPositions(stateDB stateKV, assetID [32]byte) *big.Int {
|
||||
return new(big.Int).SetBytes(stateDB.GetState(poolManagerAddr9999, committedPositionsKey(assetID)).Bytes())
|
||||
}
|
||||
|
||||
func storeCommittedPositions(stateDB stateKV, assetID [32]byte, amount *big.Int) {
|
||||
var w common.Hash
|
||||
if amount != nil && amount.Sign() > 0 {
|
||||
amount.FillBytes(w[:])
|
||||
}
|
||||
stateDB.SetState(poolManagerAddr9999, committedPositionsKey(assetID), w)
|
||||
stateDB.SetState(poolManagerAddr9999, custodyKey(assetID), w)
|
||||
}
|
||||
|
||||
+77
-366
@@ -11,400 +11,111 @@ import (
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// native_wire.go is the C<->D ATOMIC OBJECT wire format for the 0x9999 native
|
||||
// settlement seam. It is byte-for-byte the same shared-memory UTXO value the
|
||||
// dexvm side reads/writes (chains/dexvm/atomic.go encodeExportedOutput /
|
||||
// decodeExportedOutput): rail(1) | owner(20) | asset(32) | amount(8) | spent(8) =
|
||||
// 69 bytes, fixed width, deterministic. Keeping the wire identical is what lets the
|
||||
// D side import a C->D object NATIVELY (its executeImport binds the recorded rail/
|
||||
// owner/asset/amount) and lets C consume a D->C object the same way.
|
||||
// native_wire.go is the C<->D funding wire, and it is deliberately boring.
|
||||
//
|
||||
// THE SPENT WITNESS (trailing 8 bytes): on a D->C swap PROCEEDS leg, spent carries
|
||||
// the matched INPUT amount (in the locked/input-asset units) that produced this
|
||||
// output. It is 0 on every other leg (C->D orders/commits, same-asset refunds, LP
|
||||
// collects). ImportSettlement reads it to enforce the TAKER's OWN recorded slippage
|
||||
// limit on the proceeds leg — realized price = out/spent (SELL) or spent/out (BUY) —
|
||||
// INDEPENDENTLY of the keeper-relayed RelayOrderTx.PriceLimit. So a keeper that
|
||||
// zeroes the relay limit to let a sandwiched fill through still produces a proceeds
|
||||
// object whose realized price C refuses to credit (taker-authenticated MEV floor).
|
||||
// The two repos extend this width in LOCKSTEP; native_seam_parity_test pins the
|
||||
// golden bytes so a one-sided change is caught at test time, never in consensus.
|
||||
// MOVE ONCE, TRADE MANY. Value crosses the chain boundary exactly twice in its life
|
||||
// — in, and out. Between those, D owns it outright and every order, match and trade
|
||||
// is a local balance movement. A million trades cost zero crossings.
|
||||
//
|
||||
// THE RAIL TAG (the H1 fix): the object's FIRST byte is the RAIL discriminator —
|
||||
// the lane the value travels on, a property of the OBJECT, not of the C-side
|
||||
// selector that consumes it. A swap-fill object is railSwap; an LP-collect object
|
||||
// is railLP. Each C-side consume path accepts ONLY its own rail (ImportSettlement
|
||||
// => railSwap, ImportPositionCollect => railLP) and debits ONLY its own pot
|
||||
// (seamReserve vs committedPositions). Before the tag, a D->C object was rail-
|
||||
// agnostic and the DEBITED pot was chosen by which selector the caller invoked —
|
||||
// so an LP-collect object could drain the swap pot (H1-A) and a swap-fill object
|
||||
// could drain the LP pot (H1-B). With the tag the object is UNAMBIGUOUSLY one rail
|
||||
// and a cross-rail consume is rejected at decode-bind. A unit is CSpendable XOR
|
||||
// DCommitted across BOTH rails because a D->C object carries exactly one rail and
|
||||
// only the matching consume path (drawing only the matching pot) accepts it.
|
||||
// So this subsystem has exactly ONE job: move ownership exactly once between chains.
|
||||
// It knows nothing about orders, markets, sides, prices, sizes, executions,
|
||||
// slippage, settlement or FIX. A claim is:
|
||||
//
|
||||
// THE TWO LEGS (the ship rule made concrete):
|
||||
// - C->D ORDER/COMMIT: C writes one of these into shared memory keyed by the D
|
||||
// chain. D's executeImport consumes it and credits a funded D order/position.
|
||||
// D is funded ONLY by consuming a C->D object. The C->D object is stamped with
|
||||
// the lane that wrote it (SubmitSwapOrder => railSwap, SubmitPositionCommit
|
||||
// => railLP) so the rail round-trips: a swap lives on railSwap for BOTH legs,
|
||||
// an LP on railLP for BOTH legs.
|
||||
// - D->C SETTLEMENT/COLLECT: D writes one of these into shared memory keyed by
|
||||
// the C chain (its executeExport). C's ImportSettlement (railSwap) /
|
||||
// ImportPositionCollect (railLP) consumes it ONCE and credits C value. C is
|
||||
// credited ONLY by consuming a D->C object OF THE MATCHING RAIL.
|
||||
// beneficiary(20) | asset(32) | amount(8) = 60 bytes
|
||||
//
|
||||
// THE OPERATION SEGMENT (the seam's missing half). The 69 bytes above are a VALUE:
|
||||
// "this much of this asset belongs to this owner on this rail". A value says what
|
||||
// moves; it does not say what to DO with it. That is why the D side could consume a
|
||||
// C->D order, credit the taker's D account, and stop: it had been told nothing else.
|
||||
// Nothing placed an order, so nothing crossed, so no proceeds existed, so no D->C
|
||||
// settlement object was ever produced and the whole seam was unreachable by
|
||||
// construction.
|
||||
// and that is the whole vocabulary. The same 60 bytes travel both directions: a C->D
|
||||
// deposit and a D->C withdrawal are the same primitive pointed opposite ways, so
|
||||
// there is one encoder, one decoder, and one golden vector pinning them against
|
||||
// dex/pkg/dchain.
|
||||
//
|
||||
// A C->D SWAP ORDER therefore carries a second, orthogonal segment — the OPERATION
|
||||
// the taker authorized on C:
|
||||
// WHY EACH FIELD EARNS ITS PLACE, AND WHY NOTHING ELSE DOES.
|
||||
//
|
||||
// market(32) | side(1) | limitPrice(8) | size(8) = 49 bytes
|
||||
// - beneficiary is the account the destination chain credits. Naming it in the
|
||||
// object is what makes delivery permissionless: anyone may hand a claim to the
|
||||
// destination, because handing it over can only credit the account it already
|
||||
// names. A relayer that refuses to deliver cannot strand anyone.
|
||||
// - asset and amount are the value. Nothing else is.
|
||||
//
|
||||
// appended to the value, giving a 118-byte order object. The two segments are
|
||||
// composed, not braided: the value head is byte-for-byte the same 69 bytes every
|
||||
// other consumer already reads (the golden vectors that pin it stay valid), and the
|
||||
// op tail is a separate value qualified by its own accessor. WIDTH IS THE
|
||||
// DISCRIMINATOR — 69 and 118 are distinct fixed widths, so a decode can never be
|
||||
// steered between the two readings:
|
||||
// There is ONE party field, not two. dexprotocol.Claim (dex/pkg/dexprotocol) names
|
||||
// exactly one — Beneficiary — and PendingExport.Owner() returns claim.Beneficiary,
|
||||
// so "owner" and "beneficiary" were already one fact under two names. Carrying both
|
||||
// would store that fact twice for the two copies to be cross-checked against each
|
||||
// other.
|
||||
//
|
||||
// 69 bytes -> a VALUE object: D->C settlements, D->C LP collects, C->D LP commits.
|
||||
// Those legs instruct nothing; they only move value. decodeAtomicObject.
|
||||
// 118 bytes -> a C->D SWAP ORDER: value + the CLOB operation. decodeOrderObject.
|
||||
// The claim's SCOPE is not in its bytes because it is already in its address: the id
|
||||
// binds (source, dest, sourceTx, index), the source chain owns the partition it is
|
||||
// read from, and the destination owns the partition it is written under. Restating
|
||||
// any of that inside the value would be a second copy of the addressing.
|
||||
//
|
||||
// WHY THE OPERATION MUST BE IN THE OBJECT AND NOWHERE ELSE. The atomic object is the
|
||||
// only authenticated C->D channel. D cannot read C's logs, so the routing EVENT is
|
||||
// not a source D may bind to; anything D is not handed in the object, D would have to
|
||||
// invent — and an invented market/side/limit is an order the taker never authorized.
|
||||
// Carrying it here makes the operation as strongly bound as the value: one object,
|
||||
// one atomic consumption, one authorization.
|
||||
//
|
||||
// isMarket and limitIsUpper are NOT carried, because they are not independent facts:
|
||||
// every seam order is a LIMIT order (the limit is required, see SeamOp), and a BUY's
|
||||
// limit is by definition its ceiling while a SELL's is its floor. Deriving them keeps
|
||||
// one declaration of each fact instead of two to cross-check.
|
||||
//
|
||||
// The remaining order metadata (minAmountOut, recipient, deadline, nonce) is neither
|
||||
// value-bearing nor an instruction to the matcher; it rides in the C->D routing EVENT
|
||||
// (events.go) and is enforced on C at settlement.
|
||||
// WIDTH IS THE DISCRIMINATOR. 60 is the only canonical width; anything else is
|
||||
// refused rather than reinterpreted, so a truncated or padded record can never
|
||||
// decode into a credit.
|
||||
|
||||
// Rail is the cross-chain object's lane discriminator (wire byte 0). It is the
|
||||
// H1-closing property that makes a D->C object UNAMBIGUOUSLY one rail, so a C-side
|
||||
// consume path can refuse an object from the other rail before it touches a pot.
|
||||
type Rail uint8
|
||||
// claimSize is the fixed funding-object width: beneficiary(20) | asset(32) |
|
||||
// amount(8). IDENTICAL to dex/pkg/dchain claimSize.
|
||||
const claimSize = 20 + 32 + 8
|
||||
|
||||
const (
|
||||
// railSwap is the swap-fill / refund lane: SubmitSwapOrder writes it on C->D,
|
||||
// the dexvm's fill-settlement export (settleFromFills) writes it on D->C, and
|
||||
// ImportSettlement is the ONLY C-side path that consumes it — debiting ONLY
|
||||
// seamReserve. It is the ZERO value so an object whose lane is unstated defaults
|
||||
// to the swap rail (the dexvm's swap-fill exports omit the field), keeping the
|
||||
// non-LP call sites untouched.
|
||||
railSwap Rail = 0
|
||||
// railLP is the LP position-commit / collect lane: SubmitPositionCommit writes
|
||||
// it on C->D, the dexvm's executeWithdraw writes it on D->C, and
|
||||
// ImportPositionCollect is the ONLY C-side path that consumes it — debiting ONLY
|
||||
// committedPositions. A non-zero tag, so an LP object is never mistaken for the
|
||||
// zero-valued swap rail.
|
||||
railLP Rail = 1
|
||||
)
|
||||
|
||||
// exportedOutputSize is the fixed shared-memory object width: rail(1) | owner(20) |
|
||||
// asset(32) | amount(8) | spent(8). IDENTICAL to chains/dexvm/atomic.go
|
||||
// exportedOutputSize. The trailing spent(8) is the matched-input witness on a swap
|
||||
// proceeds leg (0 elsewhere); both repos carry it in lockstep.
|
||||
const exportedOutputSize9999 = 1 + 20 + 32 + 8 + 8
|
||||
|
||||
// encodeAtomicObject serializes a cross-chain value object as the shared-memory
|
||||
// value, byte-identical with the dexvm side: rail(1) | owner(20) | asset(32) |
|
||||
// amount(8) | spent(8). rail is the lane the value travels (railSwap / railLP);
|
||||
// owner is the account (EVM address / dexvm ShortID — both 20 bytes); asset is the
|
||||
// full injective AssetID (assetID(Currency); native = all-zero); amount is the
|
||||
// integer asset unit count.
|
||||
//
|
||||
// The precompile only ever writes C->D ORDER/COMMIT objects, which carry NO matched
|
||||
// input (the match happens later on D), so spent is ALWAYS 0 here — written as the
|
||||
// trailing zero word. The D side fills spent on the D->C proceeds leg (settleFromFills);
|
||||
// the precompile DECODES it in ImportSettlement. Keeping spent=0 on the C->D leg makes
|
||||
// the C->D object byte-identical with what the dexvm's executeImport expects (its
|
||||
// decodeExportedOutput requires exactly this width and ignores spent on an import).
|
||||
func encodeAtomicObject(rail Rail, owner common.Address, asset [32]byte, amount uint64) []byte {
|
||||
// A C->D order/commit object carries no matched input, so spent is always 0 here.
|
||||
return encodeAtomicObjectSpent(rail, owner, asset, amount, 0)
|
||||
}
|
||||
|
||||
// encodeAtomicObjectSpent is the full encoder including the trailing spent witness. The
|
||||
// production C->D path uses spent=0 (encodeAtomicObject); the D side (chains
|
||||
// settleFromFills) sets spent>0 on a D->C PROCEEDS leg. This variant exists so the seam
|
||||
// surface (and the parity/redteam tests) can construct the EXACT bytes the dexvm exports,
|
||||
// byte-identical with chains/dexvm/atomic.go encodeExportedOutput.
|
||||
func encodeAtomicObjectSpent(rail Rail, owner common.Address, asset [32]byte, amount, spent uint64) []byte {
|
||||
v := make([]byte, exportedOutputSize9999)
|
||||
v[0] = byte(rail)
|
||||
copy(v[1:21], owner[:])
|
||||
copy(v[21:53], asset[:])
|
||||
binary.BigEndian.PutUint64(v[53:61], amount)
|
||||
binary.BigEndian.PutUint64(v[61:69], spent)
|
||||
// encodeClaim serializes a funding object. beneficiary is the destination account
|
||||
// (EVM address / D-Chain 20-byte owner); asset is the full injective AssetID (native
|
||||
// = all-zero); amount is the integer asset unit count. Fixed width, big-endian, no
|
||||
// padding — so two encoders agree byte-for-byte or the golden vector fails.
|
||||
func encodeClaim(beneficiary common.Address, asset [32]byte, amount uint64) []byte {
|
||||
v := make([]byte, claimSize)
|
||||
copy(v[0:20], beneficiary[:])
|
||||
copy(v[20:52], asset[:])
|
||||
binary.BigEndian.PutUint64(v[52:60], amount)
|
||||
return v
|
||||
}
|
||||
|
||||
// --- the OPERATION segment (C->D swap orders only) -----------------------------
|
||||
|
||||
// Side is the CLOB side the taker's operation takes. The wire bytes are pinned to
|
||||
// the matcher's own constants (dex/pkg/zapwire, dex/pkg/dchain sideBuy/sideSell), so
|
||||
// the byte D reads out of the object is the byte D's order book consumes.
|
||||
const (
|
||||
seamSideBuy uint8 = 0
|
||||
seamSideSell uint8 = 1
|
||||
)
|
||||
|
||||
// seamOpSize is the fixed OPERATION segment width: market(32) | side(1) |
|
||||
// limitPrice(8) | size(8) = 49 bytes. IDENTICAL to dex/pkg/dchain seamOpSize.
|
||||
const seamOpSize = 32 + 1 + 8 + 8
|
||||
|
||||
// orderObjectSize9999 is the fixed C->D SWAP ORDER object width: the 69-byte value
|
||||
// head plus the 49-byte operation tail = 118 bytes. IDENTICAL to dex/pkg/dchain
|
||||
// seamOrderObjectSize. A width that is neither 69 nor 118 is not an object of ours.
|
||||
const orderObjectSize9999 = exportedOutputSize9999 + seamOpSize
|
||||
|
||||
// SeamOp is the CLOB operation a C->D swap order instructs. Every field is a value
|
||||
// the taker fixed on C when they signed the swap:
|
||||
//
|
||||
// - Market : the D market (poolId) the order targets.
|
||||
// - Side : seamSideBuy / seamSideSell.
|
||||
// - LimitPrice : the worst acceptable price on the PriceInt grid (quote-per-base
|
||||
// x 1e8). REQUIRED, non-zero. Every seam order is a LIMIT IOC order:
|
||||
// a cross-chain taker cannot re-price mid-flight and their order
|
||||
// executes in a block they do not control, so an unbounded market
|
||||
// order across the seam is exactly the unbounded-MEV case the spent
|
||||
// witness exists to prevent. C refuses a zero limit at submission and
|
||||
// D refuses a zero-limit op at import — the same rule, both ends.
|
||||
// - Size : the base-unit quantity to trade.
|
||||
type SeamOp struct {
|
||||
Market [32]byte
|
||||
Side uint8
|
||||
LimitPrice uint64
|
||||
Size uint64
|
||||
}
|
||||
|
||||
// limitIsUpper reports which end of the price range the taker's limit bounds: a BUY's
|
||||
// limit is a CEILING (reject a realized price above it), a SELL's is a FLOOR. It is a
|
||||
// pure function of Side, so the bound is never stored alongside the side as a second
|
||||
// copy of one fact.
|
||||
func limitIsUpper(side uint8) bool { return side == seamSideBuy }
|
||||
|
||||
// encodeSeamOp serializes the operation segment. Fixed width, big-endian, no padding
|
||||
// — the same discipline as the value head.
|
||||
func encodeSeamOp(op SeamOp) []byte {
|
||||
b := make([]byte, seamOpSize)
|
||||
copy(b[0:32], op.Market[:])
|
||||
b[32] = op.Side
|
||||
binary.BigEndian.PutUint64(b[33:41], op.LimitPrice)
|
||||
binary.BigEndian.PutUint64(b[41:49], op.Size)
|
||||
return b
|
||||
}
|
||||
|
||||
// decodeSeamOp is the inverse over an EXACTLY seamOpSize-wide tail.
|
||||
func decodeSeamOp(b []byte) (SeamOp, bool) {
|
||||
if len(b) != seamOpSize {
|
||||
return SeamOp{}, false
|
||||
// decodeClaim is the exact inverse. ok=false for any width but the canonical one, so
|
||||
// a corrupt record is never reinterpreted into a credit. The consumer binds the
|
||||
// credit to THIS recorded value, never to what the calling transaction declares.
|
||||
func decodeClaim(v []byte) (beneficiary common.Address, asset [32]byte, amount uint64, ok bool) {
|
||||
if len(v) != claimSize {
|
||||
return common.Address{}, [32]byte{}, 0, false
|
||||
}
|
||||
var op SeamOp
|
||||
copy(op.Market[:], b[0:32])
|
||||
op.Side = b[32]
|
||||
op.LimitPrice = binary.BigEndian.Uint64(b[33:41])
|
||||
op.Size = binary.BigEndian.Uint64(b[41:49])
|
||||
return op, true
|
||||
copy(beneficiary[:], v[0:20])
|
||||
copy(asset[:], v[20:52])
|
||||
amount = binary.BigEndian.Uint64(v[52:60])
|
||||
return beneficiary, asset, amount, true
|
||||
}
|
||||
|
||||
// encodeOrderObject serializes a C->D SWAP ORDER: the canonical value head with
|
||||
// spent=0 (a C->D leg has matched nothing yet), followed by the operation tail. This
|
||||
// is the ONLY object the swap rail writes C->D, and the ONLY one D's import consumes.
|
||||
func encodeOrderObject(owner common.Address, asset [32]byte, amount uint64, op SeamOp) []byte {
|
||||
v := make([]byte, 0, orderObjectSize9999)
|
||||
v = append(v, encodeAtomicObjectSpent(railSwap, owner, asset, amount, 0)...)
|
||||
return append(v, encodeSeamOp(op)...)
|
||||
}
|
||||
|
||||
// decodeOrderObject reads back a C->D swap order. ok=false for any width other than
|
||||
// the canonical 118 — so a 69-byte VALUE object (an old, op-less order, or a
|
||||
// settlement object misrouted here) is REFUSED rather than imported with an invented
|
||||
// operation. Fail closed: no operation, no import.
|
||||
func decodeOrderObject(v []byte) (owner common.Address, asset [32]byte, amount uint64, op SeamOp, ok bool) {
|
||||
if len(v) != orderObjectSize9999 {
|
||||
return common.Address{}, [32]byte{}, 0, SeamOp{}, false
|
||||
}
|
||||
rail, owner, asset, amount, _, headOK := decodeObjectHead(v[:exportedOutputSize9999])
|
||||
if !headOK || rail != railSwap {
|
||||
return common.Address{}, [32]byte{}, 0, SeamOp{}, false
|
||||
}
|
||||
op, opOK := decodeSeamOp(v[exportedOutputSize9999:])
|
||||
if !opOK {
|
||||
return common.Address{}, [32]byte{}, 0, SeamOp{}, false
|
||||
}
|
||||
return owner, asset, amount, op, true
|
||||
}
|
||||
|
||||
// decodeObjectHead reads the 69-byte VALUE head. It is the single definition of the
|
||||
// head field offsets, shared by the value-object decoder and the order decoder, so
|
||||
// the two readings of those bytes can never drift apart.
|
||||
func decodeObjectHead(v []byte) (rail Rail, owner common.Address, asset [32]byte, amount, spent uint64, ok bool) {
|
||||
if len(v) != exportedOutputSize9999 {
|
||||
return 0, common.Address{}, [32]byte{}, 0, 0, false
|
||||
}
|
||||
rail = Rail(v[0])
|
||||
copy(owner[:], v[1:21])
|
||||
copy(asset[:], v[21:53])
|
||||
amount = binary.BigEndian.Uint64(v[53:61])
|
||||
spent = binary.BigEndian.Uint64(v[61:69])
|
||||
return rail, owner, asset, amount, spent, true
|
||||
}
|
||||
|
||||
// decodeAtomicObject is the inverse: it reads back the (rail, owner, asset, amount,
|
||||
// spent) a consumed cross-chain object RECORDED in shared memory. ok=false for any
|
||||
// value that is not EXACTLY the canonical width, so a corrupt/garbage record is never
|
||||
// reinterpreted into a credit — the same defense the dexvm decodeExportedOutput
|
||||
// applies. The consumer binds the credited rail/owner/asset/amount to THIS recorded
|
||||
// value, never to what the calling tx merely declares — so the rail (and therefore
|
||||
// which pot may be debited) is the OBJECT's, not the caller's. spent is meaningful
|
||||
// ONLY on a D->C swap proceeds leg (the MEV-floor witness ImportSettlement checks);
|
||||
// the LP-collect and staging consumers ignore it.
|
||||
func decodeAtomicObject(v []byte) (rail Rail, owner common.Address, asset [32]byte, amount, spent uint64, ok bool) {
|
||||
return decodeObjectHead(v)
|
||||
}
|
||||
|
||||
// DeriveOrderID computes the deterministic id of a C->D atomic order object.
|
||||
// It is the shared-memory UTXO key the object is PUT under (and that D's import
|
||||
// consumes), and it is INJECTIVE over the full identity so two distinct orders
|
||||
// never collide:
|
||||
// DeriveClaimID computes a funding claim's deterministic id — the shared-memory key
|
||||
// the object is written under, and the key the destination consumes:
|
||||
//
|
||||
// SHA-256( domain | networkID | cChainID | dChainID |
|
||||
// account | assetIn | amountIn | marketID | nonce )
|
||||
// SHA-256( claimDomain | source | dest | sourceTx | index )
|
||||
//
|
||||
// Every component is fixed width and length-stable (no concatenation ambiguity).
|
||||
// (networkID, cChainID, dChainID) scope the object to exactly one rail;
|
||||
// account/asset/amount/market bind the economic payload; nonce is the taker's
|
||||
// own disambiguator so two otherwise-identical swaps get distinct ids.
|
||||
// ONE derivation, BOTH directions, BOTH repos. dex/pkg/dchain reproduces it
|
||||
// byte-for-byte; the golden vector pins them together.
|
||||
//
|
||||
// CHAIN-OBSERVABLE BY CONSTRUCTION (the watch-correlation fix). The id derives ONLY
|
||||
// from values the off-chain client knows BEFORE it signs — there is NO txID here. The
|
||||
// nonce is carried in the swap's own DI01 hookData, so the precompile (on-chain) and the
|
||||
// keeper/SDK (off-chain) read the IDENTICAL nonce from the IDENTICAL calldata and derive
|
||||
// the IDENTICAL id. Previously the off-chain side substituted a ZERO txID (the real txID
|
||||
// is unknown pre-landing) while the chain used the REAL txID, so the two ids never
|
||||
// matched and the keeper's watch could not correlate against a live chain. Binding the id
|
||||
// to a chain-observable nonce (not the post-landing txID) closes that.
|
||||
// It is injective because a transaction id is unique on its chain and the index is
|
||||
// unique within that transaction — so two claims minted by one transaction differ by
|
||||
// index, and two minted by different transactions differ by sourceTx. (source, dest)
|
||||
// scope the claim to one corridor, so an object minted for C->D can never be replayed
|
||||
// as D->C. Every component is fixed width, so there is no concatenation ambiguity.
|
||||
//
|
||||
// REPLAY/INJECTIVITY. Two distinct takers differ by account; the same taker's two
|
||||
// DIFFERENT swaps differ by asset/amount/market; the same taker's two IDENTICAL swaps
|
||||
// must use DISTINCT nonces (a reused tuple derives the same id and the second submit is
|
||||
// replay-rejected by the on-chain isOrderSubmitted guard — idempotent, never a second
|
||||
// debit). The nonce is the user's order-nonce: it disambiguates repeats AND two swaps in
|
||||
// one tx (the caller supplies distinct nonces), replacing the former callIndex axis.
|
||||
func DeriveOrderID(
|
||||
networkID uint32,
|
||||
cChainID, dChainID ids.ID,
|
||||
account common.Address,
|
||||
assetIn [32]byte,
|
||||
amountIn uint64,
|
||||
marketID [32]byte,
|
||||
nonce uint64,
|
||||
) ids.ID {
|
||||
// The VALUE is deliberately not folded in. The id names WHICH claim; the object
|
||||
// carries WHAT it moves, and the destination binds the credit to the object's bytes,
|
||||
// which the block-level import rule proves are the recorded ones. Hashing the value
|
||||
// into the id too would be a second copy of it, and a copy the consumer would then
|
||||
// have to decide whether to trust over the first.
|
||||
func DeriveClaimID(source, dest, sourceTx ids.ID, index uint32) ids.ID {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(nativeOrderDomain))
|
||||
h.Write([]byte(claimDomain))
|
||||
h.Write(source[:])
|
||||
h.Write(dest[:])
|
||||
h.Write(sourceTx[:])
|
||||
var u4 [4]byte
|
||||
binary.BigEndian.PutUint32(u4[:], networkID)
|
||||
binary.BigEndian.PutUint32(u4[:], index)
|
||||
h.Write(u4[:])
|
||||
h.Write(cChainID[:])
|
||||
h.Write(dChainID[:])
|
||||
h.Write(account[:])
|
||||
h.Write(assetIn[:])
|
||||
var u8 [8]byte
|
||||
binary.BigEndian.PutUint64(u8[:], amountIn)
|
||||
h.Write(u8[:])
|
||||
h.Write(marketID[:])
|
||||
binary.BigEndian.PutUint64(u8[:], nonce)
|
||||
h.Write(u8[:])
|
||||
var out [32]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return ids.ID(out)
|
||||
}
|
||||
|
||||
// nativeOrderDomain scopes the order-id derivation so an id minted for the DEX
|
||||
// C->D atomic seam can never be confused with any other shared-memory object id.
|
||||
// claimDomain scopes the claim-id derivation so a funding-rail id can never collide
|
||||
// with any other 32-byte shared-memory key.
|
||||
//
|
||||
// The literal is the hash input, not a label, so it does not track this identifier:
|
||||
// changing a byte of it moves every order id the seam has ever derived.
|
||||
const nativeOrderDomain = "lux.dex.native.intent.v1"
|
||||
|
||||
// DerivePositionCommitID computes the deterministic id of a C->D LP POSITION-COMMIT
|
||||
// atomic object (order kind DL01) — the C->D leg that FUNDS a D position. It is the
|
||||
// shared-memory UTXO key the commit object is PUT under (and that D's import
|
||||
// consumes). It is derived with its OWN domain (positionCommitDomain) so a
|
||||
// position-commit id and a swap-order id (DeriveOrderID, nativeOrderDomain) live
|
||||
// in DISJOINT id spaces — a swap order object can NEVER be consumed as a position
|
||||
// commit, nor vice-versa, even with the same (account, asset, amount, market). The
|
||||
// 69-byte object wire (rail|owner|asset|amount|spent) is IDENTICAL so D imports both
|
||||
// natively; only the id DOMAIN (and the routing event KIND) distinguish the two rails.
|
||||
//
|
||||
// SHA-256( positionCommitDomain | networkID | cChainID | dChainID | txID |
|
||||
// callIndex | account | assetIn | amountIn | poolID )
|
||||
//
|
||||
// Every component is fixed width and length-stable; callIndex disambiguates two
|
||||
// commits in one tx; (networkID, cChainID, dChainID) scope the object to one rail;
|
||||
// account/asset/amount/pool bind the economic payload so the id cannot be reused.
|
||||
//
|
||||
// DELIBERATE ASYMMETRY vs DeriveOrderID (txID+callIndex here, user NONCE there — do NOT
|
||||
// "align" them into a consensus break). The SWAP id is chain-observable (derived from the
|
||||
// taker's DI01 nonce, no txID) because an OFF-CHAIN keeper must re-derive it from calldata
|
||||
// to correlate its fill watch. The LP commit id has NO such off-chain re-derivation: it is
|
||||
// only ever READ from shared memory by the dexvm's executeImport (the keeper opens the D
|
||||
// position from the routing EVENT, which already carries the id), so it does not need to be
|
||||
// computable from calldata alone. txID+callIndex is therefore the right choice for the LP
|
||||
// rail — injective, and callIndex (not a user-supplied nonce the LP path doesn't carry)
|
||||
// disambiguates two commits in one tx. Switching the LP rail to a nonce would add a
|
||||
// user-managed field the modifyLiquidity ABI doesn't have and change a consensus-shared key
|
||||
// for no correlation benefit.
|
||||
func DerivePositionCommitID(
|
||||
networkID uint32,
|
||||
cChainID, dChainID ids.ID,
|
||||
txID ids.ID,
|
||||
callIndex uint32,
|
||||
account common.Address,
|
||||
assetIn [32]byte,
|
||||
amountIn uint64,
|
||||
poolID [32]byte,
|
||||
) ids.ID {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(positionCommitDomain))
|
||||
var u4 [4]byte
|
||||
binary.BigEndian.PutUint32(u4[:], networkID)
|
||||
h.Write(u4[:])
|
||||
h.Write(cChainID[:])
|
||||
h.Write(dChainID[:])
|
||||
h.Write(txID[:])
|
||||
binary.BigEndian.PutUint32(u4[:], callIndex)
|
||||
h.Write(u4[:])
|
||||
h.Write(account[:])
|
||||
h.Write(assetIn[:])
|
||||
var u8 [8]byte
|
||||
binary.BigEndian.PutUint64(u8[:], amountIn)
|
||||
h.Write(u8[:])
|
||||
h.Write(poolID[:])
|
||||
var out [32]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return ids.ID(out)
|
||||
}
|
||||
|
||||
// positionCommitDomain scopes the position-commit-id derivation so a DL01 LP commit
|
||||
// object id can never collide with a DI01 swap-order object id (nativeOrderDomain)
|
||||
// or any other shared-memory object id. This is the id-space half of the rail
|
||||
// separation (the routing-event KIND is the other half).
|
||||
const positionCommitDomain = "lux.dex.native.poscommit.v1"
|
||||
// changing a byte of it moves every claim id the rail has ever derived.
|
||||
const claimDomain = "lux.dex.claim.v1"
|
||||
|
||||
+27
-82
@@ -121,14 +121,13 @@ func moveNativeIntoVault(stateDB StateDB, caller common.Address, amount uint64)
|
||||
}
|
||||
|
||||
// moveNativeOutOfVault releases `amount` native LUX from the 0x9999 vault to `recipient`.
|
||||
// The caller MUST have already decremented the asset's reserve accounting (recordSeamRelease
|
||||
// / recordCommittedRelease), which bounds amount to the vault's real holding; the underflow
|
||||
// The caller MUST have already decremented the asset's reserve accounting (recordCustodyOut), which bounds amount to the vault's real holding; the underflow
|
||||
// guard here is defense-in-depth (SubBalance is uint256-modular from a precompile and would
|
||||
// otherwise wrap rather than revert). Phase-A op (no nested call).
|
||||
func moveNativeOutOfVault(stateDB StateDB, recipient common.Address, amount uint64) error {
|
||||
u := uint256.NewInt(amount)
|
||||
if stateDB.GetBalance(poolManagerAddr9999).Cmp(u) < 0 {
|
||||
return ErrNativeSettleUnbacked
|
||||
return ErrCustodyUnbacked
|
||||
}
|
||||
stateDB.SubBalance(poolManagerAddr9999, u)
|
||||
stateDB.AddBalance(recipient, u)
|
||||
@@ -139,41 +138,25 @@ func moveNativeOutOfVault(stateDB StateDB, recipient common.Address, amount uint
|
||||
// Pure 0x9999 storage writes (never nested calls). The matching value movement is a
|
||||
// separate Phase-A (native) or Phase-B (ERC-20) settle.
|
||||
|
||||
// recordSeamLock credits seamReserve[asset] by amount (the swap-rail input lock).
|
||||
func recordSeamLock(stateDB stateKV, assetID [32]byte, amount uint64) {
|
||||
before := loadSeamReserve(stateDB, assetID)
|
||||
storeSeamReserve(stateDB, assetID, new(big.Int).Add(before, new(big.Int).SetUint64(amount)))
|
||||
// recordCustodyIn credits custody[asset] by amount — value entering the funding
|
||||
// rail's pot on its way to D.
|
||||
func recordCustodyIn(stateDB stateKV, assetID [32]byte, amount uint64) {
|
||||
before := loadCustody(stateDB, assetID)
|
||||
storeCustody(stateDB, assetID, new(big.Int).Add(before, new(big.Int).SetUint64(amount)))
|
||||
}
|
||||
|
||||
// recordSeamRelease debits seamReserve[asset] by amount, refusing (NO MINT, NO RAID) if the
|
||||
// seam's own pot cannot back it. Returns ErrNativeSettleUnbacked when the seam reserve is
|
||||
// short — exactly the conservation gate creditSettlementOutput enforced, now callable on its
|
||||
// own so a handler can record the release BEFORE the terminal payout.
|
||||
func recordSeamRelease(stateDB stateKV, assetID [32]byte, amount uint64) error {
|
||||
held := loadSeamReserve(stateDB, assetID)
|
||||
// recordCustodyOut debits custody[asset] by amount, refusing (NO MINT, NO RAID) if
|
||||
// the rail's own pot cannot back it. This is the C-side conservation floor: a credit
|
||||
// can only ever pay out value C is already holding on D's behalf, never a depositor's
|
||||
// claim or a maker's reserve. Callable on its own so a handler records the release
|
||||
// BEFORE the terminal payout.
|
||||
func recordCustodyOut(stateDB stateKV, assetID [32]byte, amount uint64) error {
|
||||
held := loadCustody(stateDB, assetID)
|
||||
amt := new(big.Int).SetUint64(amount)
|
||||
if held.Cmp(amt) < 0 {
|
||||
return ErrNativeSettleUnbacked
|
||||
return ErrCustodyUnbacked
|
||||
}
|
||||
storeSeamReserve(stateDB, assetID, new(big.Int).Sub(held, amt))
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordCommittedLock / recordCommittedRelease are the LP-rail analogs on the
|
||||
// committedPositions pot — orthogonal to seamReserve (a swap settle and an LP collect can
|
||||
// never raid each other). Same Phase-A discipline.
|
||||
func recordCommittedLock(stateDB stateKV, assetID [32]byte, amount uint64) {
|
||||
before := loadCommittedPositions(stateDB, assetID)
|
||||
storeCommittedPositions(stateDB, assetID, new(big.Int).Add(before, new(big.Int).SetUint64(amount)))
|
||||
}
|
||||
|
||||
func recordCommittedRelease(stateDB stateKV, assetID [32]byte, amount uint64) error {
|
||||
held := loadCommittedPositions(stateDB, assetID)
|
||||
amt := new(big.Int).SetUint64(amount)
|
||||
if held.Cmp(amt) < 0 {
|
||||
return ErrLPCollectUnbacked
|
||||
}
|
||||
storeCommittedPositions(stateDB, assetID, new(big.Int).Sub(held, amt))
|
||||
storeCustody(stateDB, assetID, new(big.Int).Sub(held, amt))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -202,7 +185,7 @@ func pullERC20Terminal(vault erc20Vault, token, from common.Address, amount *big
|
||||
|
||||
// pushERC20Terminal moves `amount` of `token` from the 0x9999 vault to `to` as a TERMINAL
|
||||
// frame effect (OpenZeppelin SafeERC20 semantics). The caller MUST have already debited the
|
||||
// asset's reserve accounting (recordSeamRelease / recordCommittedRelease) so this can never
|
||||
// asset's reserve accounting (recordCustodyOut) so this can never
|
||||
// pay out unbacked value. Zero 0x9999 storage writes.
|
||||
func pushERC20Terminal(vault erc20Vault, token, to common.Address, amount *big.Int) error {
|
||||
return safeTransferTokenTo(vault, token, to, amount)
|
||||
@@ -210,17 +193,17 @@ func pushERC20Terminal(vault erc20Vault, token, to common.Address, amount *big.I
|
||||
|
||||
// ───────────────────── composed two-phase input-lock helpers ────────────────────────
|
||||
|
||||
// lockAssetIn is the Phase-A-then-Phase-B input lock for the swap-rail deposit / order
|
||||
// paths. It (A) credits seamReserve and runs `record` — the caller's OWN 0x9999 accounting
|
||||
// (dexcore ledger credit, order record, atomic staging, event) — so EVERY 0x9999 write is
|
||||
// done BEFORE any nested call, then (B) settles the asset IN: native via balance ops, ERC-20
|
||||
// via the single TERMINAL transferFrom. Returns the amount locked (== requested; an ERC-20
|
||||
// under-delivery reverts the whole frame, `record` included). `record` may be nil and runs
|
||||
// before the terminal pull, so its writes are never dropped.
|
||||
// lockAsset is the Phase-A-then-Phase-B input lock for the funding rail's export
|
||||
// leg. It (A) credits custody and runs `record` — the caller's OWN 0x9999 accounting
|
||||
// (claim staging, event) — so EVERY 0x9999 write is done BEFORE any nested call, then
|
||||
// (B) settles the asset IN: native via balance ops, ERC-20 via the single TERMINAL
|
||||
// transferFrom. Returns the amount locked (== requested; an ERC-20 under-delivery
|
||||
// reverts the whole frame, `record` included). `record` may be nil and runs before
|
||||
// the terminal pull, so its writes are never dropped.
|
||||
//
|
||||
// This is the ONE structural fix for the deposit/lock family: the transferFrom is the frame's
|
||||
// This is the ONE structural fix for the lock family: the transferFrom is the frame's
|
||||
// terminal external effect with ALL 0x9999 accounting written before it.
|
||||
func lockAssetIn(stateDB StateDB, caller common.Address, assetID [32]byte, assetAddr common.Address, amount uint64, record func() error) (uint64, error) {
|
||||
func lockAsset(stateDB StateDB, caller common.Address, assetID [32]byte, assetAddr common.Address, amount uint64, record func() error) (uint64, error) {
|
||||
if amount == 0 {
|
||||
return 0, ErrNativeBadAmount
|
||||
}
|
||||
@@ -232,7 +215,7 @@ func lockAssetIn(stateDB StateDB, caller common.Address, assetID [32]byte, asset
|
||||
return 0, ErrNativeFundsShort
|
||||
}
|
||||
// PHASE A — accounting (no nested calls).
|
||||
recordSeamLock(stateDB, assetID, amount)
|
||||
recordCustodyIn(stateDB, assetID, amount)
|
||||
if record != nil {
|
||||
if err := record(); err != nil {
|
||||
return 0, err
|
||||
@@ -255,41 +238,3 @@ func lockAssetIn(stateDB StateDB, caller common.Address, assetID [32]byte, asset
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
// commitAssetIn is the LP-rail analog of lockAssetIn on the committedPositions pot: it
|
||||
// records the committed lock + runs `record` in Phase A, then settles the asset IN
|
||||
// terminally in Phase B. Returns the amount committed (== requested; ERC-20 under-delivery
|
||||
// reverts). The orthogonal-pot rule holds: an LP commit can never appear in the swap rail's
|
||||
// seam reserve.
|
||||
func commitAssetIn(stateDB StateDB, caller common.Address, assetID [32]byte, assetAddr common.Address, amount uint64, record func() error) (uint64, error) {
|
||||
if amount == 0 {
|
||||
return 0, ErrNativeBadAmount
|
||||
}
|
||||
// Native fail-fast (see lockAssetIn): refuse an underfunded commit BEFORE any 0x9999 write so
|
||||
// no committed-positions accounting and no C->D commit object is staged for an unbacked commit.
|
||||
if isNativeAsset(assetID) && stateDB.GetBalance(caller).Cmp(uint256.NewInt(amount)) < 0 {
|
||||
return 0, ErrNativeFundsShort
|
||||
}
|
||||
// PHASE A — accounting (no nested calls).
|
||||
recordCommittedLock(stateDB, assetID, amount)
|
||||
if record != nil {
|
||||
if err := record(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if isNativeAsset(assetID) {
|
||||
if err := moveNativeIntoVault(stateDB, caller, amount); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
// PHASE B — terminal ERC-20 pull.
|
||||
vault, ok := stateDBERC20(stateDB)
|
||||
if !ok {
|
||||
return 0, ErrNativeERC20Vault
|
||||
}
|
||||
if err := pullERC20Terminal(vault, assetAddr, caller, new(big.Int).SetUint64(amount)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
+31
-31
@@ -163,19 +163,17 @@ func (s *SettleContract) commitPosition(
|
||||
return nil, gasLeft, ErrMakerAmountRange
|
||||
}
|
||||
|
||||
// COMMIT to D: SubmitPositionCommit debits the caller's CSpendable balance into
|
||||
// committedPositions, stages the C->D DL01 object, and emits the position-open
|
||||
// routing event. Returns the commit object id (the shared-memory key D imports).
|
||||
req := OrderRequest{
|
||||
Account: caller,
|
||||
AssetIn: lockedAsset,
|
||||
AmountIn: delta.Uint64(),
|
||||
AssetInAddr: assetAddress(lockedAsset),
|
||||
MarketID: key.ID(),
|
||||
Recipient: caller,
|
||||
Deadline: 0,
|
||||
}
|
||||
commitObjID, committed, cerr := nativeClient.SubmitPositionCommit(state, atomicState, req)
|
||||
// FUND D: Export debits the caller's C balance into custody and stages a C->D
|
||||
// claim naming them as beneficiary. Returns the claim id (the shared-memory key D
|
||||
// imports). The POSITION is opened on D against the funded account; the crossing
|
||||
// itself opens nothing.
|
||||
commitObjID, committed, cerr := nativeClient.Export(state, atomicState, Transfer{
|
||||
Owner: caller,
|
||||
Beneficiary: caller,
|
||||
Asset: lockedAsset,
|
||||
AssetAddr: assetAddress(lockedAsset),
|
||||
Amount: delta.Uint64(),
|
||||
})
|
||||
if cerr != nil {
|
||||
return nil, gasLeft, cerr
|
||||
}
|
||||
@@ -246,7 +244,7 @@ func (s *SettleContract) requestPositionWithdraw(
|
||||
// the withdraw request for the keeper; the funds return via collectPosition.
|
||||
order.Status = OrderStatusClosing
|
||||
storeRestingOrder(stateDB, orderID, order)
|
||||
emitNativeCancelEvent(stateDB, ids.ID(orderID), order.PoolID, caller)
|
||||
emitPositionClosing(stateDB, ids.ID(orderID), order.PoolID, caller)
|
||||
return encodeOrderResult(orderID, order.LockedAmt), gasLeft, nil
|
||||
}
|
||||
|
||||
@@ -288,15 +286,15 @@ func (s *SettleContract) runSettleCollectPosition(
|
||||
if !ok || atomicState.AtomicMemory() == nil {
|
||||
return nil, gasLeft, ErrLPNoAtomicState
|
||||
}
|
||||
// FIXED width: outputID(32) | asset(32) | positionID(32) | object(69). The object
|
||||
// FIXED width: claimID(32) | asset(32) | positionID(32) | claim(60). The object
|
||||
// rides in the CALLDATA for the same reason the swap rail's does — execution must
|
||||
// never read shared memory, or a node can never re-execute this block. There is no
|
||||
// separate `amount` word: the amount IS the object's amount.
|
||||
if len(input) != collectPositionInputLen {
|
||||
return nil, gasLeft, ErrLPBadCollectInput
|
||||
}
|
||||
var outputID ids.ID
|
||||
copy(outputID[:], input[0:32])
|
||||
var claimID ids.ID
|
||||
copy(claimID[:], input[0:32])
|
||||
var asset [32]byte
|
||||
copy(asset[:], input[32:64])
|
||||
var positionID [32]byte
|
||||
@@ -313,15 +311,17 @@ func (s *SettleContract) runSettleCollectPosition(
|
||||
}
|
||||
defer exitCustodyKV(stateDB)
|
||||
|
||||
claim := SettlementClaim{
|
||||
OutputID: outputID,
|
||||
Asset: asset,
|
||||
AssetAddr: assetAddress(asset), // routing only; the transfer token is derived from the object's asset (FIX-5).
|
||||
Recipient: caller, // day-1: no delegation; recipient is the caller.
|
||||
PositionID: positionID,
|
||||
Object: object,
|
||||
}
|
||||
credited, ierr := nativeClient.ImportPositionCollect(state, atomicState, claim)
|
||||
// The position record is C-side bookkeeping for a D-side position; the VALUE comes
|
||||
// through the one funding rail, like every other crossing. positionID identifies
|
||||
// which record this collect settles, and is not consulted to authorize the credit
|
||||
// — the claim's own beneficiary is.
|
||||
_ = positionID
|
||||
credited, ierr := nativeClient.Import(state, atomicState, Claim{
|
||||
ID: claimID,
|
||||
Asset: asset,
|
||||
Beneficiary: caller, // day-1: no delegation; the beneficiary is the caller.
|
||||
Object: object,
|
||||
})
|
||||
if ierr != nil {
|
||||
return nil, gasLeft, ierr
|
||||
}
|
||||
@@ -330,12 +330,12 @@ func (s *SettleContract) runSettleCollectPosition(
|
||||
return out, gasLeft, nil
|
||||
}
|
||||
|
||||
// EncodeCollectPositionInput builds collectPosition calldata (outputID, asset,
|
||||
// EncodeCollectPositionInput builds collectPosition calldata (claimID, asset,
|
||||
// amount, positionID) for the keeper's collect-tx builder and tests. The inverse of
|
||||
// the decode in runSettleCollectPosition.
|
||||
func EncodeCollectPositionInput(outputID ids.ID, asset [32]byte, positionID [32]byte, object []byte) []byte {
|
||||
func EncodeCollectPositionInput(claimID ids.ID, asset [32]byte, positionID [32]byte, object []byte) []byte {
|
||||
out := make([]byte, collectPositionInputLen)
|
||||
copy(out[0:32], outputID[:])
|
||||
copy(out[0:32], claimID[:])
|
||||
copy(out[32:64], asset[:])
|
||||
copy(out[64:96], positionID[:])
|
||||
copy(out[96:collectPositionInputLen], object)
|
||||
@@ -343,5 +343,5 @@ func EncodeCollectPositionInput(outputID ids.ID, asset [32]byte, positionID [32]
|
||||
}
|
||||
|
||||
// collectPositionInputLen is the fixed collectPosition calldata width:
|
||||
// outputID(32) | asset(32) | positionID(32) | object(69).
|
||||
const collectPositionInputLen = 32 + 32 + 32 + exportedOutputSize9999
|
||||
// claimID(32) | asset(32) | positionID(32) | claim(60).
|
||||
const collectPositionInputLen = 32 + 32 + 32 + claimSize
|
||||
|
||||
@@ -308,7 +308,7 @@ func (p *PositionManagerContract) routeCollectRequest(
|
||||
}
|
||||
// Emit the collect routing event (reuse the native collect event — the keeper
|
||||
// forwards it to D to export the maker's withdrawable balance for this position).
|
||||
emitNativeCollectEvent(stateDB, ids.ID(orderID), poolID, caller)
|
||||
emitPositionCollecting(stateDB, ids.ID(orderID), poolID, caller)
|
||||
out := make([]byte, 32)
|
||||
copy(out, orderID[:])
|
||||
return out, gasLeft, nil
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// seam_pending_trait_test.go pins the C->D order DISCOVERY trait against the
|
||||
// D-Chain's own constant, and pins that the flush actually attaches it.
|
||||
//
|
||||
// WHY A GOLDEN. The D-Chain proposer enumerates pending orders with
|
||||
// atomic.SharedMemory.Indexed under a trait it derives itself (dex/pkg/dchain
|
||||
// drive.go SeamPendingTrait). The two repos cannot import each other — the same
|
||||
// module-cycle constraint that already forced a golden vector for the 69-byte
|
||||
// object wire — so nothing but a shared vector keeps them equal.
|
||||
//
|
||||
// Drift here is silent in the worst way. D would query a trait C never writes,
|
||||
// enumerate nothing, and every swap order would sit unmatched forever with no
|
||||
// error logged on either side. That is indistinguishable from "nobody is
|
||||
// trading", which is precisely how it went unnoticed: the constant existed on
|
||||
// the D side with a comment naming this exact companion change, and the C side
|
||||
// never made it.
|
||||
//
|
||||
// The golden is sha256("lux.dex.native.intent.pending.v2") — the same domain
|
||||
// string dex/pkg/dchain hashes.
|
||||
//
|
||||
// WHY .v2. A v1 order object carried VALUE ONLY (69 bytes); a v2 order carries
|
||||
// value + the CLOB OPERATION (118 bytes) so D can place the taker's order instead
|
||||
// of crediting an account and stopping. A node that cannot read the operation must
|
||||
// never import the value — it would have to invent the market/side/limit. Rotating
|
||||
// the domain makes the two generations mutually INVISIBLE rather than mutually
|
||||
// confusing: a v1 D node enumerates v1, sees no v2 object, and emits no import; a
|
||||
// v2 D node enumerates v2 and never touches a stale op-less v1 object. Neither can
|
||||
// execute the other's order, so the changeover costs seam liveness alone — never a
|
||||
// divergent state root.
|
||||
const seamPendingTraitGoldenHex = "5f10f9ad8c53b78043df24cb69f8a5acb745351107470cb7e19f44748fe8bd5b"
|
||||
|
||||
func TestSeamPendingTrait_MatchesDChainGolden(t *testing.T) {
|
||||
want, err := hex.DecodeString(seamPendingTraitGoldenHex)
|
||||
if err != nil {
|
||||
t.Fatalf("bad golden hex: %v", err)
|
||||
}
|
||||
if len(SeamPendingTrait) != 32 {
|
||||
t.Fatalf("trait width %d, want 32 — a 20-byte trait could collide with an owner trait", len(SeamPendingTrait))
|
||||
}
|
||||
if !bytes.Equal(SeamPendingTrait, want) {
|
||||
t.Fatalf("SeamPendingTrait = %x\nwant %x\n"+
|
||||
"the discovery trait drifted from the D-Chain's constant: D would enumerate "+
|
||||
"nothing and every swap would sit unmatched with no error anywhere",
|
||||
SeamPendingTrait, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectStaged_SwapOrderCarriesDiscoveryTrait proves the FLUSH attaches
|
||||
// the trait to a real staged swap-rail export. The constant existing is worth
|
||||
// nothing if collectRange does not write it — that omission is the actual defect.
|
||||
func TestCollectStaged_SwapOrderCarriesDiscoveryTrait(t *testing.T) {
|
||||
db := NewMockStateDB()
|
||||
dChainID := ids.ID{0xD1}
|
||||
key := ids.ID{0x1A}
|
||||
owner := parityOwner()
|
||||
obj := encodeAtomicObject(railSwap, owner, parityAsset(), 100)
|
||||
|
||||
stageAtomicPut(db, dChainID, key, obj)
|
||||
|
||||
reqs, err := collectRange(db, 0, stageSeq(db))
|
||||
if err != nil {
|
||||
t.Fatalf("collectRange: %v", err)
|
||||
}
|
||||
ops := reqs[dChainID]
|
||||
if ops == nil || len(ops.PutRequests) != 1 {
|
||||
t.Fatalf("puts for D = %v, want exactly 1", ops)
|
||||
}
|
||||
|
||||
var haveOwner, haveDiscovery bool
|
||||
for _, tr := range ops.PutRequests[0].Traits {
|
||||
if bytes.Equal(tr, owner[:]) {
|
||||
haveOwner = true
|
||||
}
|
||||
if bytes.Equal(tr, SeamPendingTrait) {
|
||||
haveDiscovery = true
|
||||
}
|
||||
}
|
||||
if !haveOwner {
|
||||
t.Error("owner trait missing — the per-recipient index broke")
|
||||
}
|
||||
if !haveDiscovery {
|
||||
t.Error("discovery trait missing — the D-Chain import drive cannot enumerate this order, so it is never matched and no fill is ever produced")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectStaged_LPObjectHasNoDiscoveryTrait: the LP rail's lifecycle is
|
||||
// driven by its position record, not by enumeration. Tagging it would inject LP
|
||||
// objects into the swap import drive's queue.
|
||||
func TestCollectStaged_LPObjectHasNoDiscoveryTrait(t *testing.T) {
|
||||
db := NewMockStateDB()
|
||||
dChainID := ids.ID{0xD1}
|
||||
key := ids.ID{0x2B}
|
||||
obj := encodeAtomicObject(railLP, parityOwner(), parityAsset(), 100)
|
||||
|
||||
stageAtomicPut(db, dChainID, key, obj)
|
||||
|
||||
reqs, err := collectRange(db, 0, stageSeq(db))
|
||||
if err != nil {
|
||||
t.Fatalf("collectRange: %v", err)
|
||||
}
|
||||
for _, tr := range reqs[dChainID].PutRequests[0].Traits {
|
||||
if bytes.Equal(tr, SeamPendingTrait) {
|
||||
t.Fatal("LP-rail object carries the swap discovery trait — it would enter the swap import drive")
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-148
@@ -198,9 +198,8 @@ func SettleSwap(
|
||||
defer exitCustodyKV(stateDB)
|
||||
|
||||
blockNumber := state.GetBlockContext().Number().Uint64()
|
||||
blockTimestamp := state.GetBlockContext().Timestamp()
|
||||
|
||||
phase, body, taggedOrder, perr := decodeSwapPhase(hookData)
|
||||
phase, body, _, perr := decodeSwapPhase(hookData)
|
||||
if perr != nil {
|
||||
return nil, suppliedGas, perr
|
||||
}
|
||||
@@ -220,7 +219,7 @@ func SettleSwap(
|
||||
if derr != nil {
|
||||
return nil, gasLeft, derr
|
||||
}
|
||||
credited, ierr := nativeClient.ImportSettlement(state, atomicState, claim)
|
||||
credited, ierr := nativeClient.Import(state, atomicState, claim)
|
||||
if ierr != nil {
|
||||
return nil, gasLeft, ierr
|
||||
}
|
||||
@@ -239,7 +238,10 @@ func SettleSwap(
|
||||
return PackBalanceDelta(delta.Amount0, delta.Amount1), gasLeft, nil
|
||||
|
||||
default:
|
||||
// PHASE A — lock input on C and create a C->D atomic order.
|
||||
// PHASE A — cross value C->D. It funds the caller's D account and creates NO
|
||||
// order: what they trade, at what price and in what size is a D-local decision
|
||||
// they take with their own D transactions, against money that is already
|
||||
// theirs. That is what makes a million trades cost zero further crossings.
|
||||
if suppliedGas < GasNativeOrder {
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
@@ -248,114 +250,43 @@ func SettleSwap(
|
||||
if herr := checkHalt(stateDB, key, params); herr != nil {
|
||||
return nil, gasLeft, herr
|
||||
}
|
||||
// The OPTIONAL deadline + nonce ride in an EXPLICIT DI01 order body (the V4
|
||||
// SwapParams tuple is unchanged). They are read ONLY for a DI01-tagged order; an
|
||||
// untagged / opaque hook body carries neither (taggedOrder=false => body ignored),
|
||||
// so a hook contract's arbitrary bytes are never mis-parsed. The deadline gates late
|
||||
// settlement + reclaim; the NONCE is folded into the order id so it is
|
||||
// chain-observable (the off-chain keeper derives the SAME id from the SAME calldata —
|
||||
// the watch-correlation fix) and disambiguates otherwise-identical swaps.
|
||||
var deadline, nonce uint64
|
||||
if taggedOrder {
|
||||
d, n, derr := decodeOrderBody(body)
|
||||
if derr != nil {
|
||||
return nil, gasLeft, derr
|
||||
}
|
||||
deadline, nonce = d, n
|
||||
}
|
||||
// FIX (do-not-ship fund-lock): a plain swap() — and any order that did not name an
|
||||
// explicit deadline — MUST still carry a FINITE reclaim horizon. Without it the taker's
|
||||
// full input is locked under SubmitSwapOrder with Deadline==0, and reclaimIntent then
|
||||
// PERMANENTLY refuses (ErrReclaimNoDeadline) => the principal can never exit if D never
|
||||
// settles. We default a missing deadline to block.timestamp + maxOrderTTL so EVERY
|
||||
// value-path order is reclaimable after a bounded wait. An explicit deadline (DI01) is
|
||||
// honored as-is. This is the structural guarantee that swap() can never strand funds.
|
||||
if deadline == 0 {
|
||||
deadline = defaultedReclaimDeadline(blockTimestamp)
|
||||
}
|
||||
req, berr := buildOrderRequest(key, params, caller, deadline, nonce)
|
||||
t, berr := buildTransfer(key, params, caller)
|
||||
if berr != nil {
|
||||
return nil, gasLeft, berr
|
||||
}
|
||||
orderID, serr := nativeClient.SubmitSwapOrder(state, atomicState, req)
|
||||
claimID, _, serr := nativeClient.Export(state, atomicState, t)
|
||||
if serr != nil {
|
||||
return nil, gasLeft, serr
|
||||
}
|
||||
// Return the order id (32 bytes) so the caller / keeper can track the
|
||||
// async D->C settlement. NOT a fill — Phase A returns no output value.
|
||||
// Return the claim id (32 bytes) so the caller can track the crossing and
|
||||
// deliver it on D. NOT a fill — a crossing produces no output value.
|
||||
out := make([]byte, 32)
|
||||
copy(out, orderID[:])
|
||||
copy(out, claimID[:])
|
||||
return out, gasLeft, nil
|
||||
}
|
||||
}
|
||||
|
||||
// buildOrderRequest derives the C->D order from the V4 swap: the taker is the
|
||||
// caller, the input asset/amount come from the swap direction + amountSpecified
|
||||
// (exact-input magnitude), the market is the pool id, the recipient defaults to the
|
||||
// caller, and the deadline is the (optional) value parsed from the Phase-A hookData
|
||||
// body. A non-zero deadline is persisted in the per-order record so Phase-B refuses
|
||||
// a late settlement and the locker can reclaim the principal past it.
|
||||
func buildOrderRequest(key PoolKey, params SwapParams, caller common.Address, deadline, nonce uint64) (OrderRequest, error) {
|
||||
// buildTransfer derives the C->D crossing from the V4 swap: the owner and the
|
||||
// beneficiary are the caller, and the asset/amount come from the swap direction plus
|
||||
// amountSpecified (exact-input magnitude). Nothing else is read, because nothing else
|
||||
// crosses.
|
||||
func buildTransfer(key PoolKey, params SwapParams, caller common.Address) (Transfer, error) {
|
||||
in, _ := swapAssetDirection(key, params)
|
||||
// Exact-input: AmountSpecified < 0, magnitude is the input. Exact-output is a P4
|
||||
// router concern; the native order locks the exact input the taker commits.
|
||||
// Exact-input: AmountSpecified < 0, magnitude is the input. Exact-output is a
|
||||
// router concern; a crossing moves the exact amount the caller commits.
|
||||
if params.AmountSpecified == nil || params.AmountSpecified.Sign() == 0 {
|
||||
return OrderRequest{}, ErrInvalidAmount
|
||||
return Transfer{}, ErrInvalidAmount
|
||||
}
|
||||
mag := new(big.Int).Abs(params.AmountSpecified)
|
||||
if !mag.IsUint64() || mag.Sign() <= 0 {
|
||||
return OrderRequest{}, ErrInvalidAmount
|
||||
return Transfer{}, ErrInvalidAmount
|
||||
}
|
||||
// THE OPERATION the C->D object carries. The CLOB convention on a V4 pool is
|
||||
// base = currency0, quote = currency1 (priceLimitToCLOB computes quote-per-base as
|
||||
// currency1 per currency0), so the swap direction IS the side: ZeroForOne spends
|
||||
// currency0 — the base — and is a SELL; the other direction spends quote and is a
|
||||
// BUY. That is one derivation from one fact, not a second copy of it.
|
||||
priceLimit, _ := priceLimitToCLOB(params)
|
||||
side := seamSideBuy
|
||||
if params.ZeroForOne {
|
||||
side = seamSideSell
|
||||
}
|
||||
// A LIMIT is REQUIRED across the seam. The taker's order executes on D, in a block
|
||||
// they do not control, and they cannot re-price mid-flight; an unbounded market
|
||||
// order there is precisely the sandwich the spent-witness floor exists to refuse,
|
||||
// and with priceLimit == 0 that floor is vacuous. Refuse at submission rather than
|
||||
// lock the taker's principal behind a check that cannot protect them.
|
||||
if priceLimit == 0 {
|
||||
return OrderRequest{}, ErrNativeOrderNoPriceLimit
|
||||
}
|
||||
// SIZE in base units. A SELL spends the base it locked, so the locked amount IS the
|
||||
// size. A BUY spends quote, so the size is the most base that quote can buy at the
|
||||
// taker's own limit: floor(amountIn * priceScale / limit) — exact integer division
|
||||
// on the PriceInt grid, so orderLock's ceil(size*limit/priceScale) can never exceed
|
||||
// what was locked. A size that floors to zero is a swap too small to execute at the
|
||||
// taker's own limit; refuse it rather than lock principal for a guaranteed no-op.
|
||||
amountIn := mag.Uint64()
|
||||
size := amountIn
|
||||
if side == seamSideBuy {
|
||||
q := new(big.Int).Mul(mag, big.NewInt(priceScale))
|
||||
q.Quo(q, new(big.Int).SetUint64(priceLimit))
|
||||
if !q.IsUint64() {
|
||||
return OrderRequest{}, ErrInvalidAmount
|
||||
}
|
||||
size = q.Uint64()
|
||||
}
|
||||
if size == 0 {
|
||||
return OrderRequest{}, ErrNativeOrderDustSize
|
||||
}
|
||||
return OrderRequest{
|
||||
Account: caller,
|
||||
AssetIn: in,
|
||||
AmountIn: amountIn,
|
||||
AssetInAddr: assetAddress(in),
|
||||
MarketID: key.ID(),
|
||||
MinAmountOut: minAmountOut(params),
|
||||
PriceLimit: priceLimit,
|
||||
Side: side,
|
||||
Size: size,
|
||||
Recipient: caller,
|
||||
Deadline: deadline,
|
||||
Nonce: nonce,
|
||||
return Transfer{
|
||||
Owner: caller,
|
||||
Beneficiary: caller,
|
||||
Asset: in,
|
||||
AssetAddr: assetAddress(in),
|
||||
Amount: mag.Uint64(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -422,59 +353,6 @@ func priceLimitToCLOB(params SwapParams) (priceLimitUnits uint64, limitIsUpper b
|
||||
return units.Uint64(), limitIsUpper
|
||||
}
|
||||
|
||||
// runSettleReclaimOrder is the reclaimIntent(bytes32 orderID) handler: the swap
|
||||
// rail's deadline-reclaim. It decodes the order id, takes the shared custody
|
||||
// non-reentrant guard (the refund moves value through the seam reserve, and an ERC-20
|
||||
// transfer can re-enter), and calls ReclaimOrder which refunds the remaining locked
|
||||
// principal to the caller (the locker) once the deadline has passed. Returns the
|
||||
// refunded amount. NOT gated by the swap halt — funds must always be able to exit.
|
||||
func (s *SettleContract) runSettleReclaimOrder(
|
||||
state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, gas, errors.New("dex: cannot reclaimIntent in read-only mode")
|
||||
}
|
||||
if gas < GasNativeSettlement {
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
gasLeft := gas - GasNativeSettlement
|
||||
|
||||
atomicState, ok := state.(contract.AtomicState)
|
||||
if !ok || atomicState.AtomicMemory() == nil {
|
||||
return nil, gasLeft, ErrSettleNoAtomicState
|
||||
}
|
||||
if len(data) < 32 {
|
||||
return nil, gasLeft, ErrReclaimBadInput
|
||||
}
|
||||
var orderID ids.ID
|
||||
copy(orderID[:], data[0:32])
|
||||
|
||||
stateDB := newPoolStateAdapter(state)
|
||||
if !enterCustodyKV(stateDB) {
|
||||
return nil, gasLeft, ErrCustodyReentrant
|
||||
}
|
||||
defer exitCustodyKV(stateDB)
|
||||
|
||||
refunded, rerr := nativeClient.ReclaimOrder(state, atomicState, caller, orderID)
|
||||
if rerr != nil {
|
||||
return nil, gasLeft, rerr
|
||||
}
|
||||
out := make([]byte, 32)
|
||||
new(big.Int).SetUint64(refunded).FillBytes(out)
|
||||
return out, gasLeft, nil
|
||||
}
|
||||
|
||||
// EncodeReclaimOrderInput builds reclaimIntent(bytes32) calldata for the locker's
|
||||
// reclaim tx and tests.
|
||||
func EncodeReclaimOrderInput(orderID ids.ID) []byte {
|
||||
out := make([]byte, 32)
|
||||
copy(out, orderID[:])
|
||||
return out
|
||||
}
|
||||
|
||||
// ErrReclaimBadInput is returned when reclaimIntent calldata is too short to carry an
|
||||
// order id.
|
||||
var ErrReclaimBadInput = errors.New("dex: reclaimIntent input too short (need bytes32 orderID)")
|
||||
|
||||
// balanceDeltaForOutput maps a credited output amount to the V4 BalanceDelta
|
||||
// convention (output paid out to taker = negative on the output side; the input
|
||||
|
||||
+23
-30
@@ -143,46 +143,39 @@ func decodeSwapPhase(hookData []byte) (phase swapPhase, body []byte, taggedOrder
|
||||
return swapPhaseOrder, nil, false, nil
|
||||
}
|
||||
|
||||
// settlementBodyLen is the fixed Phase-B body width: outputID(32) | orderID(32) |
|
||||
// object(69).
|
||||
const settlementBodyLen = 32 + 32 + exportedOutputSize9999
|
||||
// settlementBodyLen is the fixed Phase-B body width: claimID(32) | claim(60).
|
||||
const settlementBodyLen = 32 + claimSize
|
||||
|
||||
// decodeSettlementBody parses a Phase-B body into a SettlementClaim, DERIVING the
|
||||
// asset from the swap output direction and the recipient from the caller, and
|
||||
// carrying the SUPPLIED object bytes through untouched. The claim is bound against
|
||||
// those bytes in ImportSettlement (which the host proves are the recorded object at
|
||||
// Verify), and its credit is capped by the named order record's remaining principal.
|
||||
func decodeSettlementBody(body []byte, key PoolKey, params SwapParams, caller common.Address) (SettlementClaim, error) {
|
||||
// decodeSettlementBody parses a Phase-B body into a Claim, DERIVING the asset from
|
||||
// the swap output direction and the beneficiary from the caller, and carrying the
|
||||
// SUPPLIED object bytes through untouched. The claim is bound against those bytes in
|
||||
// Import (which the host proves are the recorded object at Verify).
|
||||
func decodeSettlementBody(body []byte, key PoolKey, params SwapParams, caller common.Address) (Claim, error) {
|
||||
if len(body) != settlementBodyLen {
|
||||
return SettlementClaim{}, ErrSettleBodyMalformed
|
||||
return Claim{}, ErrSettleBodyMalformed
|
||||
}
|
||||
var outputID ids.ID
|
||||
copy(outputID[:], body[0:32])
|
||||
var orderID [32]byte
|
||||
copy(orderID[:], body[32:64])
|
||||
var claimID ids.ID
|
||||
copy(claimID[:], body[0:32])
|
||||
// Output asset = the pool's output side for this swap direction (the asset the
|
||||
// taker receives). Derived, not wire-supplied, so a claim cannot name a foreign
|
||||
// asset; ImportSettlement still equality-checks it against the object.
|
||||
// caller receives). Derived, not wire-supplied, so a transaction cannot name a
|
||||
// foreign asset; Import still equality-checks it against the object.
|
||||
_, outAsset := swapAssetDirection(key, params)
|
||||
return SettlementClaim{
|
||||
OutputID: outputID,
|
||||
Asset: outAsset,
|
||||
AssetAddr: assetAddress(outAsset),
|
||||
Recipient: caller, // day-1: no delegation; recipient is the caller.
|
||||
OrderID: orderID,
|
||||
Object: append([]byte(nil), body[64:settlementBodyLen]...),
|
||||
return Claim{
|
||||
ID: claimID,
|
||||
Asset: outAsset,
|
||||
Beneficiary: caller, // day-1: no delegation; the beneficiary is the caller.
|
||||
Object: append([]byte(nil), body[32:settlementBodyLen]...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncodeSettlementHookData builds a Phase-B hookData for tests and the keeper's
|
||||
// settle-tx builder: tag + outputID + orderID + object. The inverse of
|
||||
// decodeSettlementBody (asset/recipient are derived at decode, not encoded; the
|
||||
// amount is inside the object and is never restated).
|
||||
func EncodeSettlementHookData(outputID ids.ID, orderID ids.ID, object []byte) []byte {
|
||||
// EncodeSettlementHookData builds a Phase-B hookData for tests and a client's
|
||||
// import-tx builder: tag + claimID + claim. The inverse of decodeSettlementBody
|
||||
// (asset/beneficiary are derived at decode, not encoded; the amount is inside the
|
||||
// object and is never restated).
|
||||
func EncodeSettlementHookData(claimID ids.ID, object []byte) []byte {
|
||||
out := make([]byte, 0, 4+settlementBodyLen)
|
||||
out = append(out, settlementPhaseTag[:]...)
|
||||
out = append(out, outputID[:]...)
|
||||
out = append(out, orderID[:]...)
|
||||
out = append(out, claimID[:]...)
|
||||
out = append(out, object...)
|
||||
return out
|
||||
}
|
||||
|
||||
+12
-17
@@ -12,8 +12,7 @@ import (
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// settle_import.go is THE ONE PLACE a D->C atomic object enters C, for BOTH rails
|
||||
// (railSwap via ImportSettlement, railLP via ImportPositionCollect).
|
||||
// settle_import.go is THE ONE PLACE a D->C claim enters C.
|
||||
//
|
||||
// WHY IT EXISTS — EXECUTION MUST BE A PURE FUNCTION OF THE BLOCK.
|
||||
//
|
||||
@@ -156,26 +155,22 @@ var ErrImportObjectMalformed = errors.New("dex: supplied cross-chain object is m
|
||||
// It deliberately does NOT read shared memory. The value a consumer credits is the
|
||||
// value in these bytes; whether these bytes are the real recorded object is proven
|
||||
// one level up, on the block. Every C-side consume path goes through here, so the
|
||||
// declaration can never be omitted and the two rails can never drift apart.
|
||||
// declaration can never be omitted.
|
||||
func bindImportObject(
|
||||
stateDB StateDB,
|
||||
sourceChainID, outputID ids.ID,
|
||||
sourceChainID, claimID ids.ID,
|
||||
object []byte,
|
||||
) (rail Rail, owner common.Address, asset [32]byte, amount, spent uint64, err error) {
|
||||
rail, owner, asset, amount, spent, ok := decodeAtomicObject(object)
|
||||
) (beneficiary common.Address, asset [32]byte, amount uint64, err error) {
|
||||
beneficiary, asset, amount, ok := decodeClaim(object)
|
||||
if !ok {
|
||||
return 0, common.Address{}, [32]byte{}, 0, 0, ErrImportObjectMalformed
|
||||
return common.Address{}, [32]byte{}, 0, ErrImportObjectMalformed
|
||||
}
|
||||
// A ZERO-AMOUNT object is not a value object, and refusing it HERE rather than in
|
||||
// each rail matters for a specific reason: an absent object that some caller
|
||||
// zero-pads to the canonical width decodes as a well-formed all-zero object on
|
||||
// rail 0. Caught at this single binding point it is always the same clear refusal;
|
||||
// left to the rails it surfaced as whichever per-rail check happened to run first
|
||||
// (the LP rail reported a cross-rail violation for what was really "no object").
|
||||
// One check, one meaning, both rails.
|
||||
// A ZERO-AMOUNT object is not a value transfer, and refusing it HERE is what makes
|
||||
// "no object" a single clear refusal: an absent object that some caller zero-pads
|
||||
// to the canonical width decodes as a well-formed all-zero claim.
|
||||
if amount == 0 {
|
||||
return 0, common.Address{}, [32]byte{}, 0, 0, ErrNativeSettleAmount
|
||||
return common.Address{}, [32]byte{}, 0, ErrImportAmount
|
||||
}
|
||||
emitSettleImportDeclaration(stateDB, sourceChainID, outputID, object)
|
||||
return rail, owner, asset, amount, spent, nil
|
||||
emitSettleImportDeclaration(stateDB, sourceChainID, claimID, object)
|
||||
return beneficiary, asset, amount, nil
|
||||
}
|
||||
|
||||
@@ -89,13 +89,6 @@ var (
|
||||
// through here). Distinct selector, same orthogonal money-path discipline.
|
||||
SelectorCollectPosition uint32 // collectPosition(bytes32,address,uint256,bytes32)
|
||||
|
||||
// SelectorReclaimOrder is the SWAP rail's deadline-reclaim liveness path:
|
||||
// reclaimIntent(bytes32 orderID) refunds the remaining locked principal of a
|
||||
// stranded swap order (one D never settled) from seamReserve to the original
|
||||
// locker, once the order's deadline has passed. It is the swap-rail analog of the
|
||||
// LP collect path's exit guarantee — so locked swap input "can always exit" exactly
|
||||
// like deposit/withdraw, never stranded by a non-settling D.
|
||||
SelectorReclaimOrder uint32 // reclaimIntent(bytes32)
|
||||
)
|
||||
|
||||
// SettleModule registers the 0x9999 precompile as ALWAYS-ON: it is active on every
|
||||
@@ -132,9 +125,6 @@ func init() {
|
||||
SelectorSetHaltMarket = keccak4("setHaltMarket(bytes32,bool)")
|
||||
SelectorSetHaltAsset = keccak4("setHaltAsset(bytes32,bool)")
|
||||
SelectorCollectPosition = keccak4("collectPosition(bytes32,address,uint256,bytes32)")
|
||||
// reclaimIntent is the deployed ABI name and the signature is what hashes, so the
|
||||
// string keeps its spelling while the identifier takes the current vocabulary.
|
||||
SelectorReclaimOrder = keccak4("reclaimIntent(bytes32)")
|
||||
SelectorSeedSeamReserve = keccak4("seedSeamReserve(address,uint256)")
|
||||
SelectorCreditPositionFee = keccak4("creditPositionFee(bytes32,address,uint256)")
|
||||
|
||||
@@ -260,8 +250,6 @@ func (s *SettleContract) Run(
|
||||
// settled. The swap-rail analog of collectPosition's exit guarantee — locked swap
|
||||
// input can always exit. NOT gated by the swap halt (funds must exit even when
|
||||
// halted), exactly like deposit/withdraw.
|
||||
case SelectorReclaimOrder:
|
||||
return s.runSettleReclaimOrder(accessibleState, caller, data, suppliedGas, readOnly)
|
||||
|
||||
// donate is explicitly UNSUPPORTED in the receipt model (settle_views9999.go):
|
||||
// LP fee growth is D-authoritative, so a safe C-only donate cannot exist.
|
||||
|
||||
+4
-4
@@ -98,7 +98,7 @@ func (s *SettleContract) runSeedSeamReserve(
|
||||
// PHASE A (record) credits seamReserve BEFORE the terminal ERC-20 pull, so the seed's
|
||||
// accounting is never dropped by the geth nested-call host bug.
|
||||
delivered, derr := receiveOperatorValue(stateDB, caller, asset, aid, amount, func(amt *big.Int) error {
|
||||
storeSeamReserve(stateDB, aid, new(big.Int).Add(loadSeamReserve(stateDB, aid), amt))
|
||||
storeCustody(stateDB, aid, new(big.Int).Add(loadCustody(stateDB, aid), amt))
|
||||
return nil
|
||||
})
|
||||
if derr != nil {
|
||||
@@ -165,7 +165,7 @@ func (s *SettleContract) runCreditPositionFee(
|
||||
// before the transfer is the geth nested-call fund-loss fix (the prior order dropped these
|
||||
// writes for an ERC-20 fee credit).
|
||||
delivered, derr := receiveOperatorValue(stateDB, caller, asset, aid, amount, func(amt *big.Int) error {
|
||||
storeCommittedPositions(stateDB, aid, new(big.Int).Add(loadCommittedPositions(stateDB, aid), amt))
|
||||
storeCustody(stateDB, aid, new(big.Int).Add(loadCustody(stateDB, aid), amt))
|
||||
order.LockedAmt = new(big.Int).Add(order.LockedAmt, amt)
|
||||
storeRestingOrder(stateDB, positionID, order)
|
||||
storeLockedReserve(stateDB, order.Owner, aid,
|
||||
@@ -205,8 +205,8 @@ func receiveOperatorValue(stateDB StateDB, caller common.Address, asset Currency
|
||||
// with the vault's real native balance.
|
||||
realBal := stateDB.GetBalance(poolManagerAddr9999).ToBig()
|
||||
tracked := new(big.Int).Set(loadSettleVault(stateDB, aid))
|
||||
tracked.Add(tracked, loadSeamReserve(stateDB, aid))
|
||||
tracked.Add(tracked, loadCommittedPositions(stateDB, aid))
|
||||
tracked.Add(tracked, loadCustody(stateDB, aid))
|
||||
tracked.Add(tracked, loadCustody(stateDB, aid))
|
||||
delivered := new(big.Int).Sub(realBal, tracked)
|
||||
if delivered.Sign() < 0 || delivered.Cmp(amount) < 0 {
|
||||
return nil, ErrSeedUndelivered
|
||||
|
||||
@@ -273,7 +273,7 @@ func (v *StateViewContract) Run(
|
||||
var rid ids.ID
|
||||
copy(rid[:], data[:32])
|
||||
out := make([]byte, 32)
|
||||
if isSettlementConsumed(rv, rid) {
|
||||
if isClaimConsumed(rv, rid) {
|
||||
out[31] = 1
|
||||
}
|
||||
return out, gasLeft, nil
|
||||
|
||||
Reference in New Issue
Block a user