refactor(lx): rip compliance/KYC from permissionless DEX engine

The public permissionless DEX must be provably KYC-free. Compliance/KYC
belongs in lux-private (dex/compliance/sovereign), never in luxfi/dex.

- delete compliance.go + compliance_types.go: EligibilityChecker.CanTrade
  pre-trade gate, StandardEligibilityChecker (KYC/AML/accreditation/
  jurisdiction enforcement), InvestorProfile/InvestorStatus, PEPStatus,
  TransferRestriction, ExemptionType, ComplianceError + codes.
- delete market_config.go: the SecuritiesMarket wrapper existed solely to
  gate AddOrder on EligibilityChecker.CanTrade. Zero production consumers —
  the permissionless engine uses AdvancedOrderBook directly.
- funds.go: remove preCheck compliance hook + AML/sanctions/compliance
  fields + ComplianceGates; permissionless default is FundsStatusApproved.
- reconciliation.go: relocate neutral AssetClass + ReconciliationMode types
  here (cross-venue metadata, not gates); drop CEXComplianceID /
  ReportedToRegulator / RegulatoryReportID fields.
- tests updated to match.

Green (CGO off): go build ./..., go vet + go test ./pkg/lx ./pkg/dchain.
This commit is contained in:
2026-06-26 13:20:02 -07:00
parent 1722b23b89
commit 7f87251b7f
8 changed files with 94 additions and 1441 deletions
-174
View File
@@ -1,174 +0,0 @@
package lx
import "fmt"
// EligibilityChecker is called before every order placement to verify that
// the trader is allowed to trade the given security. ATS operators implement
// this interface with their own KYC/AML/accreditation backend.
type EligibilityChecker interface {
// CanTrade returns nil if the user is eligible, or an error describing
// why the trade is rejected.
CanTrade(userID string, symbol string, side Side, size float64, price float64) error
}
// SettlementHandler is called after each trade execution. ATS operators
// implement this to notify transfer agents, update cap tables, trigger
// on-chain token transfers, or enqueue escrow workflows.
type SettlementHandler interface {
// OnTrade is called for each executed trade. Returning an error does not
// reverse the match — settlement failures are handled asynchronously.
OnTrade(trade *Trade, market *MarketConfig) error
}
// ComplianceErrorCode identifies the category of a compliance rejection.
type ComplianceErrorCode uint8
const (
ErrNotAccredited ComplianceErrorCode = iota + 1 // Investor not accredited
ErrJurisdiction // Investor jurisdiction blocked
ErrHoldingPeriod // Holding period not elapsed
ErrMaxHolders // Would exceed max holder count
ErrKYCRequired // KYC verification missing
ErrAMLRequired // AML clearance missing
ErrMinNotional // Below minimum notional value
ErrMinSize // Below minimum order size
ErrMaxSize // Exceeds maximum order size
ErrROFRPending // Right of first refusal pending
ErrBoardApproval // Board approval required
ErrTransferRestricted // Generic transfer restriction
ErrPEPReviewRequired // PEP requires enhanced due diligence
ErrSourceOfFundsRequired // Source of funds verification needed
ErrAdverseMedia // Adverse media screening flagged
ErrHighRiskCountry // FATF high-risk jurisdiction
ErrInstitutionalOnly // Restricted to institutional investors
ErrLockupPeriod // Asset is within lockup period
ErrCollateralInsufficient // Insufficient collateral ratio
)
// ComplianceError is returned when a pre-trade check fails.
type ComplianceError struct {
Code ComplianceErrorCode
Message string
Detail interface{} // Optional context (threshold value, expiry date, etc.)
}
func (e *ComplianceError) Error() string {
if e.Detail != nil {
return fmt.Sprintf("compliance: %s (detail: %v)", e.Message, e.Detail)
}
return fmt.Sprintf("compliance: %s", e.Message)
}
// StandardEligibilityChecker provides a reusable implementation that enforces
// TransferRestriction rules using an InvestorProfile lookup function.
// ATS operators can use this directly or wrap it with additional checks.
type StandardEligibilityChecker struct {
// GetProfile returns the investor profile for a user ID.
// This is the only function the operator must provide.
GetProfile func(userID string) (*InvestorProfile, error)
// GetHolderCount returns the current number of distinct holders for a symbol.
GetHolderCount func(symbol string) (int, error)
// Restrictions are the transfer restrictions for the market.
// Usually copied from MarketConfig.Restrictions.
Restrictions *TransferRestriction
}
// CanTrade checks all configured restrictions against the investor profile.
func (c *StandardEligibilityChecker) CanTrade(userID string, symbol string, side Side, size float64, price float64) error {
if c.Restrictions == nil {
return nil
}
profile, err := c.GetProfile(userID)
if err != nil {
return &ComplianceError{Code: ErrTransferRestricted, Message: "unable to verify investor profile"}
}
if profile == nil {
return &ComplianceError{Code: ErrKYCRequired, Message: "investor profile not found"}
}
// KYC/AML
if !profile.KYCVerified {
return &ComplianceError{Code: ErrKYCRequired, Message: "KYC verification required"}
}
if !profile.AMLCleared {
return &ComplianceError{Code: ErrAMLRequired, Message: "AML clearance required"}
}
r := c.Restrictions
// Investor status check (accreditation)
if profile.Status < r.MinInvestorStatus {
return &ComplianceError{
Code: ErrNotAccredited,
Message: "investor does not meet minimum status requirement",
Detail: r.MinInvestorStatus,
}
}
// Jurisdiction whitelist
if len(r.JurisdictionWhitelist) > 0 {
allowed := false
for _, j := range r.JurisdictionWhitelist {
if j == profile.Jurisdiction {
allowed = true
break
}
}
if !allowed {
return &ComplianceError{
Code: ErrJurisdiction,
Message: "investor jurisdiction not in whitelist",
Detail: profile.Jurisdiction,
}
}
}
// Jurisdiction blacklist
for _, j := range r.JurisdictionBlacklist {
if j == profile.Jurisdiction {
return &ComplianceError{
Code: ErrJurisdiction,
Message: "investor jurisdiction is restricted",
Detail: profile.Jurisdiction,
}
}
}
// Max holders (only check for buy side — new holders)
if side == Buy && r.MaxHolders > 0 && c.GetHolderCount != nil {
// Only check if this would be a NEW holder
currentHolding := profile.Holdings[symbol]
if currentHolding == 0 {
count, err := c.GetHolderCount(symbol)
if err == nil && count >= r.MaxHolders {
return &ComplianceError{
Code: ErrMaxHolders,
Message: "maximum holder count reached",
Detail: r.MaxHolders,
}
}
}
}
// ROFR / board approval (these would typically be checked via an async
// workflow, but we reject synchronously if the flag is set and no
// pre-approval exists. The operator's GetProfile should set these
// flags based on prior approval status.)
if side == Buy && r.ROFRRequired {
return &ComplianceError{
Code: ErrROFRPending,
Message: "right of first refusal review required",
}
}
if r.BoardApprovalRequired {
return &ComplianceError{
Code: ErrBoardApproval,
Message: "board approval required for transfer",
}
}
return nil
}
-515
View File
@@ -1,515 +0,0 @@
package lx
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- helpers ---
func accreditedProfile(userID string) *InvestorProfile {
return &InvestorProfile{
UserID: userID,
Status: InvestorAccredited,
Jurisdiction: "US",
AccreditedAt: time.Now().Add(-365 * 24 * time.Hour),
KYCVerified: true,
AMLCleared: true,
Holdings: map[string]float64{},
}
}
func retailProfile(userID string) *InvestorProfile {
return &InvestorProfile{
UserID: userID,
Status: InvestorRetail,
Jurisdiction: "US",
KYCVerified: true,
AMLCleared: true,
Holdings: map[string]float64{},
}
}
func regD506cMarket() MarketConfig {
return MarketConfig{
Symbol: "ACME-USD",
AssetClass: AssetClassEquity,
Exemption: ExemptionRegD506c,
ShareClass: "Series A Preferred",
TickSize: 0.01,
LotSize: 1.0,
MinNotional: 25000.0,
Restrictions: &TransferRestriction{
HoldingPeriod: 365 * 24 * time.Hour,
MaxHolders: 2000,
MinInvestorStatus: InvestorAccredited,
},
}
}
// --- StandardEligibilityChecker tests ---
func TestAccreditedInvestorCanTrade(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
GetHolderCount: func(symbol string) (int, error) { return 10, nil },
Restrictions: &TransferRestriction{
MinInvestorStatus: InvestorAccredited,
MaxHolders: 2000,
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 100, 50.0)
assert.NoError(t, err)
}
func TestRetailInvestorRejectedForRegD(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return retailProfile(userID), nil
},
Restrictions: &TransferRestriction{
MinInvestorStatus: InvestorAccredited,
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 100, 50.0)
require.Error(t, err)
ce, ok := err.(*ComplianceError)
require.True(t, ok)
assert.Equal(t, ErrNotAccredited, ce.Code)
}
func TestKYCRequired(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
p := accreditedProfile(userID)
p.KYCVerified = false
return p, nil
},
Restrictions: &TransferRestriction{},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrKYCRequired, err.(*ComplianceError).Code)
}
func TestAMLRequired(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
p := accreditedProfile(userID)
p.AMLCleared = false
return p, nil
},
Restrictions: &TransferRestriction{},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrAMLRequired, err.(*ComplianceError).Code)
}
func TestJurisdictionWhitelist(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
p := accreditedProfile(userID)
p.Jurisdiction = "CN"
return p, nil
},
Restrictions: &TransferRestriction{
JurisdictionWhitelist: []string{"US", "CA", "GB"},
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrJurisdiction, err.(*ComplianceError).Code)
}
func TestJurisdictionWhitelistAllowed(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
Restrictions: &TransferRestriction{
JurisdictionWhitelist: []string{"US", "CA", "GB"},
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
assert.NoError(t, err)
}
func TestJurisdictionBlacklist(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
p := accreditedProfile(userID)
p.Jurisdiction = "KP"
return p, nil
},
Restrictions: &TransferRestriction{
JurisdictionBlacklist: []string{"KP", "IR", "CU"},
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrJurisdiction, err.(*ComplianceError).Code)
}
func TestMaxHoldersBuyRejected(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil // Holdings empty = new holder
},
GetHolderCount: func(symbol string) (int, error) { return 2000, nil },
Restrictions: &TransferRestriction{
MaxHolders: 2000,
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrMaxHolders, err.(*ComplianceError).Code)
}
func TestMaxHoldersExistingHolderOK(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
p := accreditedProfile(userID)
p.Holdings["ACME-USD"] = 50.0 // Already a holder
return p, nil
},
GetHolderCount: func(symbol string) (int, error) { return 2000, nil },
Restrictions: &TransferRestriction{
MaxHolders: 2000,
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
assert.NoError(t, err)
}
func TestMaxHoldersSellAlwaysOK(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
GetHolderCount: func(symbol string) (int, error) { return 2000, nil },
Restrictions: &TransferRestriction{
MaxHolders: 2000,
},
}
// Sell side should never be blocked by max holders
err := checker.CanTrade("user1", "ACME-USD", Sell, 10, 100.0)
assert.NoError(t, err)
}
func TestROFRRequired(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
Restrictions: &TransferRestriction{
ROFRRequired: true,
},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrROFRPending, err.(*ComplianceError).Code)
// Sell side should also work (ROFR only blocks buy)
// Actually, ROFR is on buy side in our impl
err = checker.CanTrade("user1", "ACME-USD", Sell, 10, 100.0)
assert.NoError(t, err)
}
func TestNoRestrictionsPassesAll(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
Restrictions: nil,
}
err := checker.CanTrade("user1", "BTC-USD", Buy, 1, 50000.0)
assert.NoError(t, err)
}
func TestProfileLookupError(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return nil, fmt.Errorf("database unavailable")
},
Restrictions: &TransferRestriction{},
}
err := checker.CanTrade("user1", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrTransferRestricted, err.(*ComplianceError).Code)
}
func TestProfileNotFound(t *testing.T) {
checker := &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return nil, nil
},
Restrictions: &TransferRestriction{},
}
err := checker.CanTrade("unknown", "ACME-USD", Buy, 10, 100.0)
require.Error(t, err)
assert.Equal(t, ErrKYCRequired, err.(*ComplianceError).Code)
}
// --- SecuritiesMarket tests ---
func TestSecuritiesMarketMinNotional(t *testing.T) {
config := regD506cMarket()
config.EligibilityChecker = &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
GetHolderCount: func(symbol string) (int, error) { return 10, nil },
Restrictions: config.Restrictions,
}
sm := NewSecuritiesMarket(config)
// Order notional = 10 * 100 = $1,000 < $25,000 minimum
order := &AdvancedOrder{
ID: 1,
UserID: "user1",
Symbol: "ACME-USD",
Side: Buy,
Type: Limit,
Price: 100.0,
Size: 10.0,
}
_, err := sm.AddOrder(order)
require.Error(t, err)
ce, ok := err.(*ComplianceError)
require.True(t, ok)
assert.Equal(t, ErrMinNotional, ce.Code)
}
func TestSecuritiesMarketMinNotionalPasses(t *testing.T) {
config := regD506cMarket()
config.EligibilityChecker = &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
GetHolderCount: func(symbol string) (int, error) { return 10, nil },
Restrictions: config.Restrictions,
}
sm := NewSecuritiesMarket(config)
// Order notional = 500 * 100 = $50,000 >= $25,000 minimum
order := &AdvancedOrder{
ID: 1,
UserID: "user1",
Symbol: "ACME-USD",
Side: Buy,
Type: Limit,
Price: 100.0,
Size: 500.0,
}
_, err := sm.AddOrder(order)
assert.NoError(t, err)
}
func TestSecuritiesMarketRejectsRetailInvestor(t *testing.T) {
config := regD506cMarket()
config.EligibilityChecker = &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return retailProfile(userID), nil
},
Restrictions: config.Restrictions,
}
sm := NewSecuritiesMarket(config)
order := &AdvancedOrder{
ID: 1,
UserID: "retail_user",
Symbol: "ACME-USD",
Side: Buy,
Type: Limit,
Price: 100.0,
Size: 500.0,
}
_, err := sm.AddOrder(order)
require.Error(t, err)
assert.Equal(t, ErrNotAccredited, err.(*ComplianceError).Code)
}
func TestSecuritiesMarketMatchWithSettlement(t *testing.T) {
var settledTrades []Trade
config := regD506cMarket()
config.EligibilityChecker = &StandardEligibilityChecker{
GetProfile: func(userID string) (*InvestorProfile, error) {
return accreditedProfile(userID), nil
},
GetHolderCount: func(symbol string) (int, error) { return 10, nil },
Restrictions: config.Restrictions,
}
config.SettlementHandler = &testSettlementHandler{
onTrade: func(trade *Trade, mc *MarketConfig) error {
settledTrades = append(settledTrades, *trade)
return nil
},
}
sm := NewSecuritiesMarket(config)
// Place a sell order
sell := &AdvancedOrder{
ID: 1,
UserID: "seller1",
Symbol: "ACME-USD",
Side: Sell,
Type: Limit,
Price: 100.0,
Size: 500.0,
}
_, err := sm.AddOrder(sell)
require.NoError(t, err)
// Place a matching buy order
buy := &AdvancedOrder{
ID: 2,
UserID: "buyer1",
Symbol: "ACME-USD",
Side: Buy,
Type: Limit,
Price: 100.0,
Size: 500.0,
}
trades, err := sm.AddOrder(buy)
require.NoError(t, err)
require.Len(t, trades, 1)
// Verify settlement was called
require.Len(t, settledTrades, 1)
assert.Equal(t, 100.0, settledTrades[0].Price)
assert.Equal(t, 500.0, settledTrades[0].Size)
}
func TestSecuritiesMarketSingleMatchMode(t *testing.T) {
config := regD506cMarket()
config.SingleMatchMode = true
config.MinNotional = 0 // Disable for this test
config.EligibilityChecker = nil
config.Restrictions = nil
sm := NewSecuritiesMarket(config)
// Place one sell order for 50 shares only
sell1 := &AdvancedOrder{
ID: 1, UserID: "s1", Symbol: "ACME-USD",
Side: Sell, Type: Limit, Price: 100.0, Size: 50.0,
}
sm.AdvancedOrderBook.AddOrder(sell1)
// Buy 200 shares — only 50 available, so 150 remain after match.
// In single-match mode, remainder should be canceled.
buy := &AdvancedOrder{
ID: 2, UserID: "b1", Symbol: "ACME-USD",
Side: Buy, Type: Limit, Price: 100.0, Size: 200.0,
}
trades, err := sm.AddOrder(buy)
require.NoError(t, err)
assert.Len(t, trades, 1)
assert.Equal(t, 50.0, trades[0].Size)
// Single-match mode: remaining 150 should be canceled
assert.Equal(t, StatusCanceled, buy.Status)
assert.Equal(t, 0.0, buy.RemainingSize)
}
func TestSecuritiesMarketMaxSize(t *testing.T) {
config := DefaultMarketConfig()
config.Symbol = "TEST-USD"
config.MaxSize = 1000.0
sm := NewSecuritiesMarket(config)
order := &AdvancedOrder{
ID: 1, UserID: "user1", Symbol: "TEST-USD",
Side: Buy, Type: Limit, Price: 10.0, Size: 2000.0,
}
_, err := sm.AddOrder(order)
require.Error(t, err)
assert.Equal(t, ErrMaxSize, err.(*ComplianceError).Code)
}
func TestSecuritiesMarketMinSize(t *testing.T) {
config := DefaultMarketConfig()
config.Symbol = "TEST-USD"
config.MinSize = 100.0
config.LotSize = 1.0
sm := NewSecuritiesMarket(config)
order := &AdvancedOrder{
ID: 1, UserID: "user1", Symbol: "TEST-USD",
Side: Buy, Type: Limit, Price: 10.0, Size: 50.0,
}
_, err := sm.AddOrder(order)
require.Error(t, err)
assert.Equal(t, ErrMinSize, err.(*ComplianceError).Code)
}
func TestDefaultMarketConfigIsUnregulated(t *testing.T) {
config := DefaultMarketConfig()
assert.False(t, config.IsRegulated())
assert.False(t, config.HasRestrictions())
}
func TestRegDMarketConfigIsRegulated(t *testing.T) {
config := regD506cMarket()
assert.True(t, config.IsRegulated())
assert.True(t, config.HasRestrictions())
}
func TestComplianceErrorMessage(t *testing.T) {
e := &ComplianceError{
Code: ErrNotAccredited,
Message: "investor not accredited",
Detail: InvestorAccredited,
}
assert.Contains(t, e.Error(), "investor not accredited")
assert.Contains(t, e.Error(), "compliance:")
e2 := &ComplianceError{
Code: ErrKYCRequired,
Message: "KYC required",
}
assert.Contains(t, e2.Error(), "KYC required")
assert.NotContains(t, e2.Error(), "detail")
}
// --- test helpers ---
type testSettlementHandler struct {
onTrade func(trade *Trade, mc *MarketConfig) error
}
func (h *testSettlementHandler) OnTrade(trade *Trade, mc *MarketConfig) error {
return h.onTrade(trade, mc)
}
-172
View File
@@ -1,172 +0,0 @@
package lx
import "time"
// AssetClass represents the classification of a traded asset.
type AssetClass uint8
const (
AssetClassCrypto AssetClass = iota // Unregulated digital asset
AssetClassEquity // Common/preferred stock (domestic)
AssetClassDebt // Bonds, notes, convertible notes
AssetClassFund // LP interests, fund shares
AssetClassSAFE // Simple Agreement for Future Equity
AssetClassCommodity // Physical or digital commodity
AssetClassDerivative // Options, futures, swaps
// Equities — granular
AssetClassIntlEquity // International equity (non-US)
// FX
AssetClassForex // Foreign exchange pairs
// Derivatives — granular
AssetClassOptions // Options contracts
AssetClassFutures // Futures contracts
// Fixed income / credit
AssetClassFixedIncome // Bonds, notes, treasuries
AssetClassMunicipal // Municipal bonds
AssetClassStructured // ABS, MBS, CDO, CLO
AssetClassCorporateDebt // Corporate loans, credit facilities
AssetClassConsumerDebt // Personal loans, credit card ABS
// Real assets
AssetClassRealEstate // REIT, fractional RE, RE tokens
AssetClassPreciousMetals // XAU, XAG, XPT, XPD spot
// Alternative investments
AssetClassPrivateEquity // PE fund tokens, SPV shares
AssetClassVenture // VC fund shares, SAFE tokens
AssetClassPrivateCredit // Direct lending, mezzanine
// DeFi-native (bridged)
AssetClassCDP // Collateralized debt positions
AssetClassLP // Liquidity pool tokens
)
// ExemptionType identifies the registration exemption or regulatory framework
// under which a security is offered and traded.
type ExemptionType uint8
const (
ExemptionNone ExemptionType = iota // No exemption (registered or crypto)
// United States (SEC)
ExemptionRegD506b // Reg D Rule 506(b) — up to 35 non-accredited
ExemptionRegD506c // Reg D Rule 506(c) — accredited only, general solicitation OK
ExemptionRegA1 // Reg A Tier 1 — up to $20M/year
ExemptionRegA2 // Reg A Tier 2 — up to $75M/year
ExemptionRegCF // Regulation Crowdfunding — up to $5M/year
ExemptionRegS // Regulation S — offshore only
ExemptionRule144 // Rule 144 resale of restricted securities
ExemptionSection4a7 // Section 4(a)(7) — private resales to accredited
// United Kingdom (FCA)
ExemptionFCAPrivate // FCA private placement (no prospectus required)
ExemptionFCAProspectus // FCA prospectus-exempt (qualified investors only)
// Singapore (MAS)
ExemptionMASSIP // MAS Small Offers (S$5M/12 months, <=50 persons)
ExemptionMASPrivate // MAS Private Placement (<=50 persons in 12 months)
ExemptionMASInstitutional // MAS Institutional Investor exemption
// European Union (MiFID II / Prospectus Regulation)
ExemptionEUQualifiedInvestor // Qualified investor exemption (Art 1(4)(a))
ExemptionEUSmallOffering // < EUR 8M (Art 3(2))
ExemptionEUCrowdfunding // ECSP Regulation
// Hong Kong (SFO)
ExemptionHKProfessionalInvestor // Professional investor (Cap 571D)
ExemptionHKSmallOffering // Offers to <50 persons
// Japan (FIEA)
ExemptionJPQII // Qualified Institutional Investor
// Australia (Corporations Act)
ExemptionAUSophisticated // Sophisticated investor (s708(8))
ExemptionAUSmallScale // Small scale offering (s708(1))
// UAE (DFSA / ADGM)
ExemptionUAEExemptOffer // Exempt offer under DFSA Rules
ExemptionUAEQualifiedInvestor // Qualified investor under ADGM
// Switzerland (FinSA)
ExemptionCHQualifiedInvestor // Qualified investor exemption
)
// InvestorStatus represents the regulatory classification of an investor.
// Values are ordered by privilege level — higher values have more access.
type InvestorStatus uint8
const (
InvestorRetail InvestorStatus = iota // Non-accredited retail
InvestorAccredited // US: SEC accredited investor
InvestorSophisticated // UK: FCA certified sophisticated investor
InvestorProfessional // UK: FCA professional client / SG: MAS accredited
InvestorQualifiedPurchaser // US: Qualified purchaser ($5M+ investments)
InvestorQualifiedClient // US: Qualified client ($1.1M+ net worth)
InvestorInstitutional // Institutional investor (bank, fund, etc.)
)
// TransferRestriction defines a constraint on the transferability of a security.
type TransferRestriction struct {
// HoldingPeriod is the minimum duration a buyer must hold before resale.
// Zero means no holding period restriction.
HoldingPeriod time.Duration
// MaxHolders is the maximum number of beneficial owners allowed.
// Zero means unlimited.
MaxHolders int
// MinInvestorStatus is the minimum investor classification required to trade.
MinInvestorStatus InvestorStatus
// JurisdictionWhitelist limits trading to investors in these jurisdictions.
// Empty means all jurisdictions allowed.
JurisdictionWhitelist []string
// JurisdictionBlacklist prohibits trading by investors in these jurisdictions.
JurisdictionBlacklist []string
// ROFRRequired indicates the company has a right of first refusal on transfers.
ROFRRequired bool
// BoardApprovalRequired indicates transfers require board/GP approval.
BoardApprovalRequired bool
}
// PEPStatus indicates politically exposed person classification.
type PEPStatus uint8
const (
PEPNone PEPStatus = iota // Not a PEP
PEPDirect // Directly a PEP (head of state, senior official, etc.)
PEPRelated // Close associate or family member of a PEP
PEPFormer // Former PEP (cooling-off period)
)
// InvestorProfile holds the compliance-relevant attributes of a trader.
// Provided by the EligibilityChecker implementation, not stored in the DEX.
type InvestorProfile struct {
UserID string
Status InvestorStatus
Jurisdiction string // ISO 3166-1 alpha-2 country code
AccreditedAt time.Time
KYCVerified bool
AMLCleared bool
// Holdings tracks current share count per symbol for max-holder checks.
// The checker implementation is responsible for populating this.
Holdings map[string]float64
// PEP / EDD (Enhanced Due Diligence)
PEP PEPStatus // Politically exposed person status
EDDCompleted bool // Enhanced due diligence completed
SourceOfFunds string // Declared source of funds
SOFVerified bool // Source of funds verified
AdverseMedia bool // Adverse media screening flagged
HighRiskCountry bool // Jurisdiction is FATF high-risk
TaxResidency string // Tax residency country code
EntityID string // Legal entity identifier (LEI for institutions)
}
+10 -48
View File
@@ -9,7 +9,7 @@ import (
// VenueKind distinguishes the fundamental type of trading venue.
// CEX and DEX have completely different deposit/withdrawal mechanics,
// risk profiles, compliance gates, and settlement times.
// risk profiles, and settlement times.
type VenueKind uint8
const (
@@ -44,15 +44,13 @@ type FundsStatus uint8
const (
FundsStatusPending FundsStatus = iota // Created, awaiting processing
FundsStatusCompliance // Undergoing compliance review
FundsStatusApproved // Compliance approved, ready to execute
FundsStatusApproved // Validated, ready to execute
FundsStatusProcessing // Execution in progress
FundsStatusConfirming // Awaiting on-chain confirmations (DEX)
FundsStatusSettling // Settlement in progress (CEX T+1/T+2)
FundsStatusCompleted // Fully settled
FundsStatusFailed // Failed (insufficient funds, compliance, etc.)
FundsStatusFailed // Failed (insufficient funds, limit exceeded, etc.)
FundsStatusCancelled // Cancelled by user or system
FundsStatusFrozen // Frozen by compliance (AML hold)
)
// RiskLevel classifies the risk of a funds movement.
@@ -61,8 +59,8 @@ type RiskLevel uint8
const (
RiskLow RiskLevel = iota // Internal, small amounts, known counterparty
RiskMedium // Cross-venue, moderate amounts
RiskHigh // Large amounts, new counterparty, high-risk jurisdiction
RiskCritical // Requires manual review (PEP, sanctions, etc.)
RiskHigh // Large amounts, cross-venue/external
RiskCritical // Requires manual review
)
// FundsMovement is the universal representation of any deposit, withdrawal,
@@ -95,13 +93,6 @@ type FundsMovement struct {
UpdatedAt time.Time
CompletedAt *time.Time
// Compliance
ComplianceCheckID string // Pre-movement compliance check
ComplianceStatus string // approved, pending_review, rejected, frozen
AMLFlag bool // Flagged by AML screening
SanctionsFlag bool // Flagged by sanctions check
ReviewerID string // Manual reviewer (if required)
// On-chain (DEX movements)
SourceTxHash string // Source chain transaction
DestTxHash string // Destination chain transaction
@@ -127,7 +118,6 @@ type RiskProfile struct {
CustodialRisk bool // True for CEX movements
BridgeRisk bool // True for cross-chain
SettlementTime string // "instant", "~15min", "T+1", "T+2", "1-5 business days"
ComplianceGates []string
TypicalFees string // Human-readable fee description
}
@@ -138,7 +128,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CounterpartyRisk: "none",
SmartContractRisk: true,
SettlementTime: "instant",
ComplianceGates: []string{},
TypicalFees: "gas only",
},
RouteCEXInternal: {
@@ -146,7 +135,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CounterpartyRisk: "custodial",
CustodialRisk: true,
SettlementTime: "instant",
ComplianceGates: []string{"account_verification"},
TypicalFees: "none",
},
RouteDEXToDEX: {
@@ -155,7 +143,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
SmartContractRisk: true,
BridgeRisk: true,
SettlementTime: "~15min (bridge confirmations)",
ComplianceGates: []string{},
TypicalFees: "bridge fee (0.1-0.3%) + gas",
},
RouteCEXToCEX: {
@@ -163,7 +150,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CounterpartyRisk: "custodial",
CustodialRisk: true,
SettlementTime: "T+1 to T+2",
ComplianceGates: []string{"kyc", "aml", "withdrawal_limit"},
TypicalFees: "broker transfer fee",
},
RouteDEXToCEX: {
@@ -172,7 +158,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
SmartContractRisk: true,
CustodialRisk: true,
SettlementTime: "~30min (on-chain confirm + CEX credit)",
ComplianceGates: []string{"kyc", "aml", "source_verification", "address_whitelist"},
TypicalFees: "gas + CEX deposit fee (often free)",
},
RouteCEXToDEX: {
@@ -181,7 +166,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CustodialRisk: true,
SmartContractRisk: true,
SettlementTime: "~15min (CEX processing + on-chain confirm)",
ComplianceGates: []string{"kyc", "aml", "withdrawal_limit", "address_whitelist", "2fa"},
TypicalFees: "CEX withdrawal fee + gas",
},
RouteExternalIn: {
@@ -189,7 +173,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CounterpartyRisk: "banking",
CustodialRisk: true,
SettlementTime: "1-5 business days (wire/ACH)",
ComplianceGates: []string{"kyc", "aml", "source_of_funds", "bank_verification"},
TypicalFees: "wire fee ($25-50) or free (ACH)",
},
RouteExternalOut: {
@@ -197,7 +180,6 @@ var routeRiskProfiles = map[FundsRoute]RiskProfile{
CounterpartyRisk: "banking",
CustodialRisk: true,
SettlementTime: "1-5 business days (wire/ACH)",
ComplianceGates: []string{"kyc", "aml", "withdrawal_limit", "bank_verification", "travel_rule"},
TypicalFees: "wire fee ($25-50) or free (ACH)",
},
}
@@ -223,8 +205,7 @@ type FundsManager struct {
lastReset time.Time
// Hooks
preCheck func(m *FundsMovement) error // Pre-movement compliance check
onComplete func(m *FundsMovement) // Post-settlement callback
onComplete func(m *FundsMovement) // Post-settlement callback
onFail func(m *FundsMovement, reason string)
// Bridge for cross-chain movements
@@ -245,13 +226,11 @@ func NewFundsManager(bridge *EnhancedBridge) *FundsManager {
// SetHooks sets lifecycle callbacks.
func (fm *FundsManager) SetHooks(
preCheck func(*FundsMovement) error,
onComplete func(*FundsMovement),
onFail func(*FundsMovement, string),
) {
fm.mu.Lock()
defer fm.mu.Unlock()
fm.preCheck = preCheck
fm.onComplete = onComplete
fm.onFail = onFail
}
@@ -306,17 +285,9 @@ func (fm *FundsManager) Initiate(m *FundsMovement) (*FundsMovement, error) {
}
}
// Run pre-check hook (compliance)
if fm.preCheck != nil {
m.Status = FundsStatusCompliance
if err := fm.preCheck(m); err != nil {
m.Status = FundsStatusFailed
m.FailureReason = err.Error()
fm.movements[m.ID] = m
return m, err
}
}
// Permissionless: a validated movement is approved. There is no
// eligibility/KYC gate — only the funds checks above (positive amount,
// asset/source present, daily withdrawal limit) can reject a movement.
m.Status = FundsStatusApproved
// Store
@@ -418,11 +389,6 @@ func (fm *FundsManager) GetAccountMovements(accountID string) []*FundsMovement {
// --- Risk classification ---
func (fm *FundsManager) classifyRisk(m *FundsMovement) RiskLevel {
// Sanctions or AML flag = critical
if m.SanctionsFlag || m.AMLFlag {
return RiskCritical
}
// Large amounts = high risk (threshold: 10,000 USD equivalent)
threshold := new(big.Int).Mul(big.NewInt(10000), big.NewInt(1e6)) // 10K in 6-decimal units
if m.Amount.Cmp(threshold) > 0 {
@@ -514,18 +480,16 @@ func (fm *FundsManager) executeDEXToCEX(m *FundsMovement) error {
// 2. Send to CEX deposit address
// 3. Wait for CEX to credit (confirmations required)
// Risk: smart contract (step 1) + custodial (step 3)
// Compliance: CEX requires address whitelisting, AML check on deposit
// Settlement: ~30 min (on-chain confirms + CEX processing)
return nil
}
// CEX→DEX: custodial withdrawal + on-chain deposit — mixed risk, ~15 min.
func (fm *FundsManager) executeCEXToDEX(m *FundsMovement) error {
// 1. CEX processes withdrawal (compliance checks, 2FA, withdrawal limits)
// 1. CEX processes withdrawal (2FA, withdrawal limits)
// 2. CEX sends on-chain transaction
// 3. DEX contract receives deposit
// Risk: custodial (step 1) + smart contract (step 3)
// Compliance: CEX withdrawal compliance (KYC, AML, travel rule, limits)
// Settlement: ~15 min
return nil
}
@@ -534,7 +498,6 @@ func (fm *FundsManager) executeCEXToDEX(m *FundsMovement) error {
func (fm *FundsManager) executeExternalIn(m *FundsMovement) error {
// Fiat deposit via wire transfer, ACH, or card
// Risk: banking counterparty
// Compliance: source of funds verification, bank account verification
// Settlement: 1-5 business days (wire), 3-5 days (ACH), instant (card, with limits)
return nil
}
@@ -543,7 +506,6 @@ func (fm *FundsManager) executeExternalIn(m *FundsMovement) error {
func (fm *FundsManager) executeExternalOut(m *FundsMovement) error {
// Fiat withdrawal via wire transfer or ACH
// Risk: banking counterparty
// Compliance: KYC, AML, travel rule, withdrawal limits, tax reporting
// Settlement: 1-5 business days
return nil
}
+7 -79
View File
@@ -17,7 +17,6 @@ func TestRiskProfileDEXInternal(t *testing.T) {
assert.False(t, rp.CustodialRisk)
assert.False(t, rp.BridgeRisk)
assert.Equal(t, "instant", rp.SettlementTime)
assert.Empty(t, rp.ComplianceGates)
}
func TestRiskProfileCEXInternal(t *testing.T) {
@@ -39,37 +38,28 @@ func TestRiskProfileCEXToCEX(t *testing.T) {
rp := GetRiskProfile(RouteCEXToCEX)
assert.True(t, rp.CustodialRisk)
assert.Contains(t, rp.SettlementTime, "T+1")
assert.Contains(t, rp.ComplianceGates, "kyc")
assert.Contains(t, rp.ComplianceGates, "aml")
}
func TestRiskProfileDEXToCEX(t *testing.T) {
rp := GetRiskProfile(RouteDEXToCEX)
assert.True(t, rp.SmartContractRisk)
assert.True(t, rp.CustodialRisk)
assert.Contains(t, rp.ComplianceGates, "source_verification")
assert.Contains(t, rp.ComplianceGates, "address_whitelist")
}
func TestRiskProfileCEXToDEX(t *testing.T) {
rp := GetRiskProfile(RouteCEXToDEX)
assert.True(t, rp.CustodialRisk)
assert.True(t, rp.SmartContractRisk)
assert.Contains(t, rp.ComplianceGates, "2fa")
assert.Contains(t, rp.ComplianceGates, "withdrawal_limit")
}
func TestRiskProfileExternalIn(t *testing.T) {
rp := GetRiskProfile(RouteExternalIn)
assert.Equal(t, "banking", rp.CounterpartyRisk)
assert.Contains(t, rp.ComplianceGates, "source_of_funds")
assert.Contains(t, rp.ComplianceGates, "bank_verification")
}
func TestRiskProfileExternalOut(t *testing.T) {
rp := GetRiskProfile(RouteExternalOut)
assert.Equal(t, "banking", rp.CounterpartyRisk)
assert.Contains(t, rp.ComplianceGates, "travel_rule")
}
// --- FundsManager tests ---
@@ -191,18 +181,11 @@ func TestFundsManagerNoLimitNoRestriction(t *testing.T) {
require.NoError(t, err)
}
func TestFundsManagerPreCheckHook(t *testing.T) {
func TestFundsManagerInitiateApprovesWithoutGate(t *testing.T) {
// Permissionless: every validated movement is approved — there is no
// eligibility/KYC pre-check hook to reject it.
fm := NewFundsManager(nil)
complianceChecked := false
fm.SetHooks(
func(m *FundsMovement) error {
complianceChecked = true
return nil
},
nil, nil,
)
m := &FundsMovement{
Direction: FundsDeposit,
Route: RouteDEXInternal,
@@ -210,41 +193,16 @@ func TestFundsManagerPreCheckHook(t *testing.T) {
Asset: "ETH",
Amount: big.NewInt(1000),
}
_, err := fm.Initiate(m)
result, err := fm.Initiate(m)
require.NoError(t, err)
assert.True(t, complianceChecked)
}
func TestFundsManagerPreCheckRejection(t *testing.T) {
fm := NewFundsManager(nil)
fm.SetHooks(
func(m *FundsMovement) error {
if m.AMLFlag {
return assert.AnError
}
return nil
},
nil, nil,
)
m := &FundsMovement{
Direction: FundsDeposit,
Route: RouteDEXInternal,
SourceID: "0xabc",
Asset: "ETH",
Amount: big.NewInt(1000),
AMLFlag: true,
}
_, err := fm.Initiate(m)
require.Error(t, err)
assert.Equal(t, FundsStatusApproved, result.Status)
}
func TestFundsManagerExecute(t *testing.T) {
fm := NewFundsManager(nil)
var completed bool
fm.SetHooks(nil,
fm.SetHooks(
func(m *FundsMovement) { completed = true },
nil,
)
@@ -302,7 +260,7 @@ func TestFundsManagerDEXToDEXRequiresBridge(t *testing.T) {
fm := NewFundsManager(nil) // nil bridge
var failReason string
fm.SetHooks(nil, nil,
fm.SetHooks(nil,
func(m *FundsMovement, reason string) { failReason = reason },
)
@@ -344,36 +302,6 @@ func TestFundsManagerGetAccountMovements(t *testing.T) {
// --- Risk classification tests ---
func TestRiskClassificationSanctionsIsCritical(t *testing.T) {
fm := NewFundsManager(nil)
m := &FundsMovement{
Direction: FundsDeposit,
Route: RouteDEXInternal,
SourceID: "0xabc",
Asset: "USDC",
Amount: big.NewInt(100),
SanctionsFlag: true,
}
result, _ := fm.Initiate(m)
assert.Equal(t, RiskCritical, result.Risk)
}
func TestRiskClassificationAMLIsCritical(t *testing.T) {
fm := NewFundsManager(nil)
m := &FundsMovement{
Direction: FundsDeposit,
Route: RouteDEXInternal,
SourceID: "0xabc",
Asset: "USDC",
Amount: big.NewInt(100),
AMLFlag: true,
}
result, _ := fm.Initiate(m)
assert.Equal(t, RiskCritical, result.Risk)
}
func TestRiskClassificationLargeAmountCrossVenue(t *testing.T) {
fm := NewFundsManager(nil)
-285
View File
@@ -1,285 +0,0 @@
package lx
// MarketConfig defines the trading rules and regulatory constraints for a market.
// For unregulated crypto markets, use DefaultMarketConfig().
type MarketConfig struct {
Symbol string
// Asset classification
AssetClass AssetClass
Exemption ExemptionType
ShareClass string // e.g. "Series A Preferred", "Class B Common"
// Transfer restrictions (nil = no restrictions)
Restrictions *TransferRestriction
// Trading constraints
MinNotional float64 // Minimum trade notional value (price * size)
MinSize float64 // Minimum order size in base units
MaxSize float64 // Maximum order size (0 = unlimited)
TickSize float64 // Price tick size
LotSize float64 // Size lot size
// Matching behavior
SingleMatchMode bool // If true, remove both sides after first match (private secondaries)
// Compliance hooks (nil = no checks)
EligibilityChecker EligibilityChecker
SettlementHandler SettlementHandler
// Offering / Issuer metadata — populated for exempt securities
OfferingType string // reg_cf, reg_d_506c, reg_a, reg_s, public
IssuerID string
IssuerName string
// Structured product metadata — credit, real estate, private offerings
ProductType string // abs, mbs, cdo, clo, reit, cdp, pe_fund, vc_fund
CreditRating string // AAA, AA, A, BBB, BB, B, CCC, NR
CollateralType string // residential, commercial, auto, student, mixed
MaturityDate string // ISO 8601 date
CouponRate float64 // Annual coupon rate as decimal
YieldToMaturity float64 // Current YTM
Leverage float64 // Max leverage for the product
CollateralRatio float64 // Min collateral ratio (e.g. 1.50 = 150%)
LiquidationRatio float64 // Ratio below which liquidation triggers
MinInvestment float64 // Minimum investment amount
LockupPeriod string // e.g. "180d", "1y"
AccreditedOnly bool // Requires accredited/professional investor
InstitutionalOnly bool // Institutions only (QIB, etc.)
// CEX reconciliation — links this DEX market to a CEX venue
CEXSymbol string // Corresponding CEX symbol (if reconciled)
CEXVenue string // CEX venue identifier (e.g. "lux-cex")
ReconciliationMode ReconciliationMode // How cross-venue settlement works
}
// ReconciliationMode determines how assets are settled across CEX and DEX.
type ReconciliationMode uint8
const (
ReconciliationNone ReconciliationMode = iota // No cross-venue reconciliation
ReconciliationAtomicSwap // On-chain atomic swap between venues
ReconciliationBridge // Bridge-based transfer (lock/mint)
ReconciliationCustodial // Custodial settlement via broker
ReconciliationHybrid // On-chain proof + off-chain settlement
)
// DefaultMarketConfig returns a config for an unregulated crypto market.
func DefaultMarketConfig() MarketConfig {
return MarketConfig{
AssetClass: AssetClassCrypto,
Exemption: ExemptionNone,
TickSize: 0.01,
LotSize: 0.001,
}
}
// FixedIncomeMarketConfig returns a config for bond/note markets.
func FixedIncomeMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassFixedIncome,
TickSize: 0.001,
LotSize: 1000, // Standard bond lot
MinNotional: 1000,
}
}
// StructuredProductMarketConfig returns a config for ABS/MBS/CDO markets.
func StructuredProductMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassStructured,
TickSize: 0.01,
LotSize: 10000,
MinNotional: 25000,
AccreditedOnly: true,
Restrictions: &TransferRestriction{
MinInvestorStatus: InvestorAccredited,
},
}
}
// PreciousMetalsMarketConfig returns a config for XAU/XAG/XPT/XPD spot.
func PreciousMetalsMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassPreciousMetals,
TickSize: 0.01,
LotSize: 0.001,
}
}
// RealEstateMarketConfig returns a config for tokenized real estate.
func RealEstateMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassRealEstate,
TickSize: 0.01,
LotSize: 1,
AccreditedOnly: true,
Restrictions: &TransferRestriction{
MinInvestorStatus: InvestorAccredited,
HoldingPeriod: 365 * 24 * 3600000000000, // 1 year in nanoseconds
},
}
}
// PrivateEquityMarketConfig returns a config for PE/VC fund tokens.
func PrivateEquityMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassPrivateEquity,
TickSize: 0.01,
LotSize: 1,
AccreditedOnly: true,
InstitutionalOnly: false,
SingleMatchMode: true, // Private secondaries
Restrictions: &TransferRestriction{
MinInvestorStatus: InvestorQualifiedPurchaser,
ROFRRequired: true,
BoardApprovalRequired: true,
},
}
}
// CDPMarketConfig returns a config for collateralized debt positions.
func CDPMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassCDP,
TickSize: 0.01,
LotSize: 0.001,
}
}
// LPTokenMarketConfig returns a config for liquidity pool tokens.
func LPTokenMarketConfig(symbol string) MarketConfig {
return MarketConfig{
Symbol: symbol,
AssetClass: AssetClassLP,
TickSize: 0.000001,
LotSize: 0.000001,
}
}
// IsRegulated returns true if this market trades a regulated security.
func (mc *MarketConfig) IsRegulated() bool {
return mc.Exemption != ExemptionNone
}
// HasRestrictions returns true if transfer restrictions are configured.
func (mc *MarketConfig) HasRestrictions() bool {
return mc.Restrictions != nil
}
// SecuritiesMarket is an AdvancedOrderBook with regulatory configuration.
type SecuritiesMarket struct {
*AdvancedOrderBook
Config MarketConfig
}
// NewSecuritiesMarket creates a market with full securities compliance support.
func NewSecuritiesMarket(config MarketConfig) *SecuritiesMarket {
book := NewAdvancedOrderBook(config.Symbol)
book.tickSize = config.TickSize
book.lotSize = config.LotSize
if config.LotSize == 0 {
book.lotSize = 0.001
}
if config.TickSize == 0 {
book.tickSize = 0.01
}
// Always enable self-trade prevention for regulated markets
if config.IsRegulated() {
book.enableSelfTrade = true
}
return &SecuritiesMarket{
AdvancedOrderBook: book,
Config: config,
}
}
// AddOrder validates compliance before placing an order.
func (sm *SecuritiesMarket) AddOrder(order *AdvancedOrder) ([]Trade, error) {
// Pre-trade compliance check
if sm.Config.EligibilityChecker != nil {
if err := sm.Config.EligibilityChecker.CanTrade(
order.UserID,
sm.Config.Symbol,
order.Side,
order.Size,
order.Price,
); err != nil {
order.Status = StatusRejected
return nil, err
}
}
// Min notional check
if sm.Config.MinNotional > 0 && order.Type == Limit {
notional := order.Price * order.Size
if notional < sm.Config.MinNotional {
order.Status = StatusRejected
return nil, &ComplianceError{
Code: ErrMinNotional,
Message: "order notional below minimum",
Detail: sm.Config.MinNotional,
}
}
}
// Max size check
if sm.Config.MaxSize > 0 && order.Size > sm.Config.MaxSize {
order.Status = StatusRejected
return nil, &ComplianceError{
Code: ErrMaxSize,
Message: "order size exceeds maximum",
Detail: sm.Config.MaxSize,
}
}
// Min size check
if sm.Config.MinSize > 0 && order.Size < sm.Config.MinSize {
order.Status = StatusRejected
return nil, &ComplianceError{
Code: ErrMinSize,
Message: "order size below minimum",
Detail: sm.Config.MinSize,
}
}
// Single-match mode: use IOC so unmatched qty is auto-cancelled
if sm.Config.SingleMatchMode && order.TimeInForce != TIF_FOK {
order.TimeInForce = TIF_IOC
}
// Delegate to the underlying order book
trades, err := sm.AdvancedOrderBook.AddOrder(order)
if err != nil {
return nil, err
}
// Post-trade settlement
if sm.Config.SettlementHandler != nil && len(trades) > 0 {
for i := range trades {
if sErr := sm.Config.SettlementHandler.OnTrade(&trades[i], &sm.Config); sErr != nil {
// Log but don't fail the match — trade already happened.
// Settlement errors are handled asynchronously.
_ = sErr
}
}
}
// Single-match mode: IOC already cancelled the remainder via the engine.
// Ensure status reflects cancellation if there was a partial fill.
if sm.Config.SingleMatchMode && len(trades) > 0 && order.ExecutedSize < order.Size {
order.Status = StatusCanceled
order.RemainingSize = 0
}
return trades, nil
}
+67 -14
View File
@@ -6,6 +6,63 @@ import (
"time"
)
// AssetClass represents the classification of a traded asset. It is metadata
// used to map a cross-venue trade between the DEX and a counterpart CEX venue;
// it is NOT a trading gate — the permissionless engine matches every class.
type AssetClass uint8
const (
AssetClassCrypto AssetClass = iota // Digital asset
AssetClassEquity // Common/preferred stock (domestic)
AssetClassDebt // Bonds, notes, convertible notes
AssetClassFund // LP interests, fund shares
AssetClassSAFE // Simple Agreement for Future Equity
AssetClassCommodity // Physical or digital commodity
AssetClassDerivative // Options, futures, swaps
// Equities — granular
AssetClassIntlEquity // International equity (non-US)
// FX
AssetClassForex // Foreign exchange pairs
// Derivatives — granular
AssetClassOptions // Options contracts
AssetClassFutures // Futures contracts
// Fixed income / credit
AssetClassFixedIncome // Bonds, notes, treasuries
AssetClassMunicipal // Municipal bonds
AssetClassStructured // ABS, MBS, CDO, CLO
AssetClassCorporateDebt // Corporate loans, credit facilities
AssetClassConsumerDebt // Personal loans, credit card ABS
// Real assets
AssetClassRealEstate // REIT, fractional RE, RE tokens
AssetClassPreciousMetals // XAU, XAG, XPT, XPD spot
// Alternative investments
AssetClassPrivateEquity // PE fund tokens, SPV shares
AssetClassVenture // VC fund shares, SAFE tokens
AssetClassPrivateCredit // Direct lending, mezzanine
// DeFi-native (bridged)
AssetClassCDP // Collateralized debt positions
AssetClassLP // Liquidity pool tokens
)
// ReconciliationMode determines how a matched cross-venue trade is settled
// between the DEX and a counterpart CEX.
type ReconciliationMode uint8
const (
ReconciliationNone ReconciliationMode = iota // No cross-venue reconciliation
ReconciliationAtomicSwap // On-chain atomic swap between venues
ReconciliationBridge // Bridge-based transfer (lock/mint)
ReconciliationCustodial // Custodial settlement via broker
ReconciliationHybrid // On-chain proof + off-chain settlement
)
// VenueType identifies where a trade was executed.
type VenueType uint8
@@ -40,11 +97,10 @@ type CrossVenueTrade struct {
DEXTxHash string // on-chain transaction hash
// CEX side
CEXOrderID string // CEX uses string UUIDs
CEXTradeID string
CEXSymbol string
CEXVenue string // execution venue on CEX
CEXComplianceID string
CEXOrderID string // CEX uses string UUIDs
CEXTradeID string
CEXSymbol string
CEXVenue string // execution venue on CEX
// Common fields
AssetClass AssetClass
@@ -60,10 +116,8 @@ type CrossVenueTrade struct {
SettledAt *time.Time
FailureReason string
// Regulatory
Jurisdiction string // Governing jurisdiction
ReportedToRegulator bool
RegulatoryReportID string
// Governing jurisdiction of the counterpart CEX venue (metadata only).
Jurisdiction string
}
// dexToCEXMap maps DEX asset classes to CEX string equivalents.
@@ -274,11 +328,10 @@ func (r *Reconciler) mergeTrades(dex, cex *CrossVenueTrade) *CrossVenueTrade {
DEXTxHash: dex.DEXTxHash,
// CEX side
CEXOrderID: cex.CEXOrderID,
CEXTradeID: cex.CEXTradeID,
CEXSymbol: cex.CEXSymbol,
CEXVenue: cex.CEXVenue,
CEXComplianceID: cex.CEXComplianceID,
CEXOrderID: cex.CEXOrderID,
CEXTradeID: cex.CEXTradeID,
CEXSymbol: cex.CEXSymbol,
CEXVenue: cex.CEXVenue,
// Common
AssetClass: dex.AssetClass,
+10 -154
View File
@@ -120,17 +120,16 @@ func TestReconcilerMatchDEXFirst(t *testing.T) {
// Now submit matching CEX trade
cexTrade := &CrossVenueTrade{
CEXOrderID: "uuid-1",
CEXTradeID: "uuid-2",
CEXSymbol: "BTC-USD",
CEXVenue: "lux-cex",
CEXComplianceID: "comp-1",
DEXSymbol: "BTC-USD",
Side: Buy,
Price: 50000.0,
Size: 1.0,
Jurisdiction: "US",
Timestamp: time.Now(),
CEXOrderID: "uuid-1",
CEXTradeID: "uuid-2",
CEXSymbol: "BTC-USD",
CEXVenue: "lux-cex",
DEXSymbol: "BTC-USD",
Side: Buy,
Price: 50000.0,
Size: 1.0,
Jurisdiction: "US",
Timestamp: time.Now(),
}
result, err = recon.SubmitCEXTrade(cexTrade)
@@ -382,115 +381,6 @@ func TestNewAssetClassesAreDefined(t *testing.T) {
assert.Equal(t, 23, len(seen), "expected 23 unique asset classes")
}
func TestNewExemptionTypesAreDefined(t *testing.T) {
// Verify new jurisdiction exemptions are distinct
exemptions := []ExemptionType{
ExemptionNone,
ExemptionRegD506b, ExemptionRegD506c, ExemptionRegA1, ExemptionRegA2,
ExemptionRegCF, ExemptionRegS, ExemptionRule144, ExemptionSection4a7,
ExemptionFCAPrivate, ExemptionFCAProspectus,
ExemptionMASSIP, ExemptionMASPrivate, ExemptionMASInstitutional,
ExemptionEUQualifiedInvestor, ExemptionEUSmallOffering, ExemptionEUCrowdfunding,
ExemptionHKProfessionalInvestor, ExemptionHKSmallOffering,
ExemptionJPQII,
ExemptionAUSophisticated, ExemptionAUSmallScale,
ExemptionUAEExemptOffer, ExemptionUAEQualifiedInvestor,
ExemptionCHQualifiedInvestor,
}
seen := make(map[ExemptionType]bool)
for _, e := range exemptions {
assert.False(t, seen[e], "duplicate exemption value: %d", e)
seen[e] = true
}
assert.Equal(t, 25, len(seen))
}
// --- Market config factory tests ---
func TestFixedIncomeMarketConfig(t *testing.T) {
mc := FixedIncomeMarketConfig("UST-10Y")
assert.Equal(t, AssetClassFixedIncome, mc.AssetClass)
assert.Equal(t, float64(1000), mc.LotSize)
assert.Equal(t, "UST-10Y", mc.Symbol)
}
func TestStructuredProductMarketConfig(t *testing.T) {
mc := StructuredProductMarketConfig("MBS-POOL-A")
assert.Equal(t, AssetClassStructured, mc.AssetClass)
assert.True(t, mc.AccreditedOnly)
assert.NotNil(t, mc.Restrictions)
assert.Equal(t, InvestorAccredited, mc.Restrictions.MinInvestorStatus)
}
func TestPreciousMetalsMarketConfig(t *testing.T) {
mc := PreciousMetalsMarketConfig("XAU-USD")
assert.Equal(t, AssetClassPreciousMetals, mc.AssetClass)
assert.Equal(t, 0.001, mc.LotSize)
}
func TestRealEstateMarketConfig(t *testing.T) {
mc := RealEstateMarketConfig("REIT-ALPHA")
assert.Equal(t, AssetClassRealEstate, mc.AssetClass)
assert.True(t, mc.AccreditedOnly)
assert.NotNil(t, mc.Restrictions)
}
func TestPrivateEquityMarketConfig(t *testing.T) {
mc := PrivateEquityMarketConfig("PE-FUND-I")
assert.Equal(t, AssetClassPrivateEquity, mc.AssetClass)
assert.True(t, mc.SingleMatchMode)
assert.True(t, mc.AccreditedOnly)
assert.True(t, mc.Restrictions.ROFRRequired)
assert.True(t, mc.Restrictions.BoardApprovalRequired)
assert.Equal(t, InvestorQualifiedPurchaser, mc.Restrictions.MinInvestorStatus)
}
func TestCDPMarketConfig(t *testing.T) {
mc := CDPMarketConfig("DAI-CDP")
assert.Equal(t, AssetClassCDP, mc.AssetClass)
}
func TestLPTokenMarketConfig(t *testing.T) {
mc := LPTokenMarketConfig("UNI-V3-ETH-USDC")
assert.Equal(t, AssetClassLP, mc.AssetClass)
assert.Equal(t, 0.000001, mc.LotSize)
}
// --- PEP / EDD tests ---
func TestPEPStatusValues(t *testing.T) {
assert.Equal(t, PEPStatus(0), PEPNone)
assert.Equal(t, PEPStatus(1), PEPDirect)
assert.Equal(t, PEPStatus(2), PEPRelated)
assert.Equal(t, PEPStatus(3), PEPFormer)
}
func TestInvestorProfilePEPFields(t *testing.T) {
profile := &InvestorProfile{
UserID: "pep-user",
Status: InvestorAccredited,
Jurisdiction: "US",
KYCVerified: true,
AMLCleared: true,
Holdings: map[string]float64{},
PEP: PEPDirect,
EDDCompleted: true,
SourceOfFunds: "salary",
SOFVerified: true,
AdverseMedia: false,
HighRiskCountry: false,
TaxResidency: "US",
EntityID: "LEI-12345",
}
assert.Equal(t, PEPDirect, profile.PEP)
assert.True(t, profile.EDDCompleted)
assert.Equal(t, "salary", profile.SourceOfFunds)
assert.True(t, profile.SOFVerified)
assert.Equal(t, "LEI-12345", profile.EntityID)
}
// --- Reconciliation mode tests ---
func TestReconciliationModeValues(t *testing.T) {
@@ -500,37 +390,3 @@ func TestReconciliationModeValues(t *testing.T) {
assert.Equal(t, ReconciliationMode(3), ReconciliationCustodial)
assert.Equal(t, ReconciliationMode(4), ReconciliationHybrid)
}
func TestMarketConfigWithReconciliation(t *testing.T) {
mc := MarketConfig{
Symbol: "BTC-USD",
AssetClass: AssetClassCrypto,
CEXSymbol: "BTC-USD",
CEXVenue: "lux-cex",
ReconciliationMode: ReconciliationAtomicSwap,
}
assert.Equal(t, "lux-cex", mc.CEXVenue)
assert.Equal(t, ReconciliationAtomicSwap, mc.ReconciliationMode)
}
// --- New compliance error codes ---
func TestNewComplianceErrorCodes(t *testing.T) {
codes := []ComplianceErrorCode{
ErrPEPReviewRequired,
ErrSourceOfFundsRequired,
ErrAdverseMedia,
ErrHighRiskCountry,
ErrInstitutionalOnly,
ErrLockupPeriod,
ErrCollateralInsufficient,
}
seen := make(map[ComplianceErrorCode]bool)
for _, c := range codes {
assert.False(t, seen[c], "duplicate compliance error code: %d", c)
seen[c] = true
assert.True(t, c > ErrTransferRestricted, "new codes should be after ErrTransferRestricted")
}
}