mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
175 lines
6.4 KiB
Go
175 lines
6.4 KiB
Go
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
|
|
}
|