mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
big.Int.Uint64 and .Int64 are documented "if x cannot be represented, the result is undefined". On calldata that is not a lossy conversion to reason about, it is a value substitution, and the substitution happens twice in series: Uint64 drops everything above bit 63, and the Go conversion that follows it -- uint24(x), which is uint32(x) because uint24 is an alias -- drops everything above bit 31. A reader who checks only the first concludes the site is safe. contract.Unsigned and contract.Signed refuse a word that does not fit its declared width, once, where the bytes become a value. Six production sites use them; nothing downstream has to remember which of its arguments were bounded. DecodePoolKey read fee and tickSpacing as 24-bit fields and enforced neither. Above 2^32 the narrowing substituted: a tickSpacing word of 2^32+6 arrived as 6, and 2^32+60 produced the same pool id as 60. Between 2^24 and 2^32 the field held the caller's value faithfully while EncodePoolKeyABI wrote only its low three bytes, so Fee 3000 and Fee 2^24+3000 hash to the SAME id -- PoolKey.ID is not injective over its own Go type, and the decoder handed back a key whose fields are not the ones its id was derived from. The encoder is unchanged; changing it renames every pool that exists. What changed is that no such key can be built from calldata, which empties the collision class at the only boundary that produces PoolKeys from untrusted bytes. Same bound on the two tick fields in DecodeModifyLiquidityInput and decodePMLifecycle. Three engines narrowed INSIDE the comparison meant to bound the value, so the check did not merely miss, it inverted. A margin removal to 2^64x leverage read as leverage zero and passed; a perp open at 2^64 in hundredths did the same; increasePosition wrapped the quotient into a uint32 field that then set the liquidation price. Leverage is now compared at full width. CalculateFee clamped AFTER narrowing, so a violent enough move came out cheaper than a calm one; it clamps in 256 bits and narrows once the clamp proves it fits. GetTimeToRepayment saturates rather than wrapping a horizon into a small number of blocks. fhe declares euint128/160/256 and loads them through EncryptUint64, whose bit loop reads value>>i off a uint64 -- zero for every i above 63. The container is genuinely wider (tfheRandom fills all 256 bits), so the missing bits were not rounded away, they were encrypted AS ZERO: measured, asEuint256(2^200) decrypted to 0, and asEaddress on two addresses sharing their last eight bytes produced the same plaintext, so a homomorphic equality over encrypted addresses answered true for two different accounts. Casts to a narrower type still truncate, matching Solidity and the existing asEuint4/8/16/32 masks; what is refused is a plaintext wider than the loader, which is not a cast but a value the encryptor cannot carry. Widening it needs an EncryptBig upstream -- BitCiphertext's fields are unexported and it has no constructor from bits. Same bound on the scalar ops, once ahead of the switch rather than five times inside it. Consensus-visible: words that decoded now refuse. Fee or tickSpacing wider than 24 bits, a tick wider than int24, an FHE plaintext or scalar wider than 64 bits. Every one of them previously resolved to a different value than the caller sent. Four prior lanes had pinned three of these as characterisation tests naming themselves DEFECT, each instructing that the test be inverted when the bound arrived; they are inverted here rather than deleted, so the history of why stays legible. 24 of 24 mutations caught, no survivors. contract 100.0%, dex 99.1 -> 99.2. Both CGO settings, -count=1, -count=2 and -race.
586 lines
16 KiB
Go
586 lines
16 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// Additional errors for perpetuals
|
|
var (
|
|
ErrExcessiveLeverage = errors.New("leverage exceeds maximum allowed")
|
|
)
|
|
|
|
// PerpetualEngine manages perpetual futures trading
|
|
// Address: 0x0420
|
|
type PerpetualEngine struct {
|
|
Markets map[[32]byte]*PerpMarket // Market ID -> Market
|
|
Positions map[common.Address]map[[32]byte]*PerpPosition // User -> Market -> Position
|
|
InsuranceFund *big.Int // Global insurance fund
|
|
|
|
// Funding state per market
|
|
FundingStates map[[32]byte]*FundingState
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewPerpetualEngine creates a new perpetual futures engine
|
|
func NewPerpetualEngine() *PerpetualEngine {
|
|
return &PerpetualEngine{
|
|
Markets: make(map[[32]byte]*PerpMarket),
|
|
Positions: make(map[common.Address]map[[32]byte]*PerpPosition),
|
|
InsuranceFund: big.NewInt(0),
|
|
FundingStates: make(map[[32]byte]*FundingState),
|
|
}
|
|
}
|
|
|
|
// CreateMarket creates a new perpetual futures market
|
|
func (pe *PerpetualEngine) CreateMarket(
|
|
baseAsset, quoteAsset Currency,
|
|
initialPrice *big.Int,
|
|
maxLeverage uint32,
|
|
maintenanceMargin *big.Int,
|
|
) ([32]byte, error) {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
// Validate leverage
|
|
if maxLeverage == 0 || maxLeverage > MaxLeverage {
|
|
maxLeverage = MaxLeverage
|
|
}
|
|
|
|
// Generate market ID
|
|
marketID := generateMarketID(baseAsset, quoteAsset)
|
|
|
|
if _, exists := pe.Markets[marketID]; exists {
|
|
return [32]byte{}, ErrPoolExists
|
|
}
|
|
|
|
market := &PerpMarket{
|
|
BaseAsset: baseAsset,
|
|
QuoteAsset: quoteAsset,
|
|
MarkPrice: new(big.Int).Set(initialPrice),
|
|
IndexPrice: new(big.Int).Set(initialPrice),
|
|
OpenInterestLong: big.NewInt(0),
|
|
OpenInterestShort: big.NewInt(0),
|
|
FundingRate: big.NewInt(0),
|
|
LastFundingTime: time.Now().Unix(),
|
|
MaxLeverage: maxLeverage,
|
|
MaintenanceMargin: maintenanceMargin,
|
|
InsuranceFund: big.NewInt(0),
|
|
}
|
|
|
|
pe.Markets[marketID] = market
|
|
pe.FundingStates[marketID] = &FundingState{
|
|
CumulativeFunding: big.NewInt(0),
|
|
LastUpdateTime: time.Now().Unix(),
|
|
PremiumEMA: big.NewInt(0),
|
|
TWAPWindow: 8 * 3600, // 8 hours
|
|
}
|
|
|
|
return marketID, nil
|
|
}
|
|
|
|
// OpenPosition opens or increases a perpetual position
|
|
func (pe *PerpetualEngine) OpenPosition(
|
|
owner common.Address,
|
|
marketID [32]byte,
|
|
size *big.Int, // Positive = long, negative = short
|
|
margin *big.Int,
|
|
isIsolated bool,
|
|
) (*PerpPosition, error) {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
|
|
// Calculate effective leverage
|
|
notionalValue := new(big.Int).Abs(size)
|
|
notionalValue.Mul(notionalValue, market.MarkPrice)
|
|
notionalValue.Div(notionalValue, Q96)
|
|
|
|
leverage := new(big.Int).Mul(notionalValue, big.NewInt(100))
|
|
leverage.Div(leverage, margin)
|
|
|
|
// Compared as a 256-bit value, for the reason margin.go gives at the same
|
|
// check: a leverage past uint64 narrows to its low word, and the low word of
|
|
// an unpayable position is usually small.
|
|
if leverage.Cmp(new(big.Int).SetUint64(uint64(market.MaxLeverage)*100)) > 0 {
|
|
return nil, ErrExcessiveLeverage
|
|
}
|
|
|
|
// Get or create position
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
userPositions = make(map[[32]byte]*PerpPosition)
|
|
pe.Positions[owner] = userPositions
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
fundingState := pe.FundingStates[marketID]
|
|
|
|
if position == nil {
|
|
// New position
|
|
position = &PerpPosition{
|
|
Owner: owner,
|
|
Market: marketID,
|
|
Size: new(big.Int).Set(size),
|
|
EntryPrice: new(big.Int).Set(market.MarkPrice),
|
|
Margin: new(big.Int).Set(margin),
|
|
LastFundingIndex: new(big.Int).Set(fundingState.CumulativeFunding),
|
|
IsIsolated: isIsolated,
|
|
}
|
|
userPositions[marketID] = position
|
|
} else {
|
|
// Settle funding before modifying
|
|
pe.settleFundingForPosition(position, fundingState)
|
|
|
|
// Increase position
|
|
oldNotional := new(big.Int).Mul(position.Size, position.EntryPrice)
|
|
newNotional := new(big.Int).Mul(size, market.MarkPrice)
|
|
totalNotional := new(big.Int).Add(oldNotional, newNotional)
|
|
|
|
newSize := new(big.Int).Add(position.Size, size)
|
|
if newSize.Sign() == 0 {
|
|
// Position closed
|
|
delete(userPositions, marketID)
|
|
return nil, nil
|
|
}
|
|
|
|
// Calculate new average entry price
|
|
position.EntryPrice.Div(totalNotional, newSize)
|
|
position.Size = newSize
|
|
position.Margin.Add(position.Margin, margin)
|
|
}
|
|
|
|
// Update open interest
|
|
if size.Sign() > 0 {
|
|
market.OpenInterestLong.Add(market.OpenInterestLong, new(big.Int).Abs(size))
|
|
} else {
|
|
market.OpenInterestShort.Add(market.OpenInterestShort, new(big.Int).Abs(size))
|
|
}
|
|
|
|
return position, nil
|
|
}
|
|
|
|
// ClosePosition closes a perpetual position
|
|
func (pe *PerpetualEngine) ClosePosition(
|
|
owner common.Address,
|
|
marketID [32]byte,
|
|
sizeToClose *big.Int, // Amount to close (absolute value)
|
|
) (*big.Int, error) { // Returns realized PnL
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
// Settle funding first
|
|
fundingState := pe.FundingStates[marketID]
|
|
fundingPnL := pe.settleFundingForPosition(position, fundingState)
|
|
|
|
// Calculate PnL
|
|
positionSize := new(big.Int).Abs(position.Size)
|
|
if sizeToClose.Cmp(positionSize) > 0 {
|
|
sizeToClose = positionSize
|
|
}
|
|
|
|
// Price difference
|
|
priceDiff := new(big.Int).Sub(market.MarkPrice, position.EntryPrice)
|
|
|
|
// PnL = size * priceDiff / Q96
|
|
pnl := new(big.Int).Mul(sizeToClose, priceDiff)
|
|
pnl.Div(pnl, Q96)
|
|
|
|
// If short, negate PnL
|
|
if position.Size.Sign() < 0 {
|
|
pnl.Neg(pnl)
|
|
}
|
|
|
|
// Add funding PnL
|
|
pnl.Add(pnl, fundingPnL)
|
|
|
|
// Update position
|
|
if sizeToClose.Cmp(positionSize) >= 0 {
|
|
// Full close
|
|
delete(userPositions, marketID)
|
|
} else {
|
|
// Partial close
|
|
closeFraction := new(big.Int).Mul(sizeToClose, big.NewInt(1e18))
|
|
closeFraction.Div(closeFraction, positionSize)
|
|
|
|
marginReduction := new(big.Int).Mul(position.Margin, closeFraction)
|
|
marginReduction.Div(marginReduction, big.NewInt(1e18))
|
|
position.Margin.Sub(position.Margin, marginReduction)
|
|
|
|
if position.Size.Sign() > 0 {
|
|
position.Size.Sub(position.Size, sizeToClose)
|
|
} else {
|
|
position.Size.Add(position.Size, sizeToClose)
|
|
}
|
|
}
|
|
|
|
// Update open interest
|
|
if position.Size.Sign() > 0 {
|
|
market.OpenInterestLong.Sub(market.OpenInterestLong, sizeToClose)
|
|
} else {
|
|
market.OpenInterestShort.Sub(market.OpenInterestShort, sizeToClose)
|
|
}
|
|
|
|
return pnl, nil
|
|
}
|
|
|
|
// AddMargin adds margin to an existing position
|
|
func (pe *PerpetualEngine) AddMargin(
|
|
owner common.Address,
|
|
marketID [32]byte,
|
|
amount *big.Int,
|
|
) error {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return ErrPositionNotFound
|
|
}
|
|
|
|
position.Margin.Add(position.Margin, amount)
|
|
return nil
|
|
}
|
|
|
|
// RemoveMargin removes margin from an existing position (if still safe)
|
|
func (pe *PerpetualEngine) RemoveMargin(
|
|
owner common.Address,
|
|
marketID [32]byte,
|
|
amount *big.Int,
|
|
) error {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return ErrPoolNotFound
|
|
}
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return ErrPositionNotFound
|
|
}
|
|
|
|
newMargin := new(big.Int).Sub(position.Margin, amount)
|
|
if newMargin.Sign() <= 0 {
|
|
return ErrInsufficientMargin
|
|
}
|
|
|
|
// Check if position is still safe
|
|
if !pe.isPositionSafe(position, market, newMargin) {
|
|
return ErrInsufficientMargin
|
|
}
|
|
|
|
position.Margin = newMargin
|
|
return nil
|
|
}
|
|
|
|
// LiquidatePosition liquidates an underwater position
|
|
func (pe *PerpetualEngine) LiquidatePosition(
|
|
liquidator common.Address,
|
|
owner common.Address,
|
|
marketID [32]byte,
|
|
) (*big.Int, error) { // Returns liquidation reward
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
// Check if position is liquidatable
|
|
if pe.isPositionSafe(position, market, position.Margin) {
|
|
return nil, ErrPositionNotLiquidatable
|
|
}
|
|
|
|
// Calculate position value and PnL
|
|
positionSize := new(big.Int).Abs(position.Size)
|
|
priceDiff := new(big.Int).Sub(market.MarkPrice, position.EntryPrice)
|
|
pnl := new(big.Int).Mul(positionSize, priceDiff)
|
|
pnl.Div(pnl, Q96)
|
|
if position.Size.Sign() < 0 {
|
|
pnl.Neg(pnl)
|
|
}
|
|
|
|
// Remaining margin after loss
|
|
remainingMargin := new(big.Int).Add(position.Margin, pnl)
|
|
|
|
// Liquidation reward (5% of position notional, capped at remaining margin)
|
|
notional := new(big.Int).Mul(positionSize, market.MarkPrice)
|
|
notional.Div(notional, Q96)
|
|
reward := new(big.Int).Div(notional, big.NewInt(20)) // 5%
|
|
|
|
if reward.Cmp(remainingMargin) > 0 {
|
|
// Socialized loss - use insurance fund
|
|
deficit := new(big.Int).Sub(reward, remainingMargin)
|
|
if market.InsuranceFund.Cmp(deficit) >= 0 {
|
|
market.InsuranceFund.Sub(market.InsuranceFund, deficit)
|
|
} else {
|
|
// ADL would be triggered here in production
|
|
reward = remainingMargin
|
|
}
|
|
}
|
|
|
|
// Update open interest
|
|
if position.Size.Sign() > 0 {
|
|
market.OpenInterestLong.Sub(market.OpenInterestLong, positionSize)
|
|
} else {
|
|
market.OpenInterestShort.Sub(market.OpenInterestShort, positionSize)
|
|
}
|
|
|
|
// Remove position
|
|
delete(userPositions, marketID)
|
|
|
|
return reward, nil
|
|
}
|
|
|
|
// UpdateFunding calculates and applies funding rate
|
|
func (pe *PerpetualEngine) UpdateFunding(marketID [32]byte) error {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return ErrPoolNotFound
|
|
}
|
|
|
|
fundingState := pe.FundingStates[marketID]
|
|
now := time.Now().Unix()
|
|
|
|
// Only update every 8 hours
|
|
if now-fundingState.LastUpdateTime < int64(fundingState.TWAPWindow) {
|
|
return nil
|
|
}
|
|
|
|
// Calculate premium (mark - index) / index
|
|
premium := new(big.Int).Sub(market.MarkPrice, market.IndexPrice)
|
|
premium.Mul(premium, big.NewInt(1e18))
|
|
premium.Div(premium, market.IndexPrice)
|
|
|
|
// Update EMA of premium
|
|
alpha := big.NewInt(125) // 0.125 for 8h EMA
|
|
oneMinusAlpha := big.NewInt(875)
|
|
|
|
newEMA := new(big.Int).Mul(premium, alpha)
|
|
oldEMA := new(big.Int).Mul(fundingState.PremiumEMA, oneMinusAlpha)
|
|
fundingState.PremiumEMA.Add(newEMA, oldEMA)
|
|
fundingState.PremiumEMA.Div(fundingState.PremiumEMA, big.NewInt(1000))
|
|
|
|
// Interest rate component (0.01% per 8 hours = 0.0001)
|
|
interestRate := big.NewInt(100) // 0.01% in 1e6
|
|
|
|
// Funding rate = premium EMA + interest rate
|
|
// Clamped to ±0.75% per 8 hours
|
|
fundingRate := new(big.Int).Add(fundingState.PremiumEMA, interestRate)
|
|
maxRate := big.NewInt(7500) // 0.75% in 1e6
|
|
minRate := big.NewInt(-7500)
|
|
|
|
if fundingRate.Cmp(maxRate) > 0 {
|
|
fundingRate = maxRate
|
|
} else if fundingRate.Cmp(minRate) < 0 {
|
|
fundingRate = minRate
|
|
}
|
|
|
|
market.FundingRate = fundingRate
|
|
market.LastFundingTime = now
|
|
|
|
// Update cumulative funding
|
|
// cumulative += fundingRate * markPrice / 1e6
|
|
fundingPayment := new(big.Int).Mul(fundingRate, market.MarkPrice)
|
|
fundingPayment.Div(fundingPayment, big.NewInt(1e6))
|
|
fundingState.CumulativeFunding.Add(fundingState.CumulativeFunding, fundingPayment)
|
|
fundingState.LastUpdateTime = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetFundingRate returns current funding rate for a market
|
|
func (pe *PerpetualEngine) GetFundingRate(marketID [32]byte) (*big.Int, error) {
|
|
pe.mu.RLock()
|
|
defer pe.mu.RUnlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
|
|
return new(big.Int).Set(market.FundingRate), nil
|
|
}
|
|
|
|
// GetPosition returns a user's position
|
|
func (pe *PerpetualEngine) GetPosition(owner common.Address, marketID [32]byte) (*PerpPosition, error) {
|
|
pe.mu.RLock()
|
|
defer pe.mu.RUnlock()
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
// Return a copy
|
|
return &PerpPosition{
|
|
Owner: position.Owner,
|
|
Market: position.Market,
|
|
Size: new(big.Int).Set(position.Size),
|
|
EntryPrice: new(big.Int).Set(position.EntryPrice),
|
|
Margin: new(big.Int).Set(position.Margin),
|
|
LastFundingIndex: new(big.Int).Set(position.LastFundingIndex),
|
|
IsIsolated: position.IsIsolated,
|
|
}, nil
|
|
}
|
|
|
|
// GetUnrealizedPnL calculates unrealized PnL for a position
|
|
func (pe *PerpetualEngine) GetUnrealizedPnL(owner common.Address, marketID [32]byte) (*big.Int, error) {
|
|
pe.mu.RLock()
|
|
defer pe.mu.RUnlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
|
|
userPositions := pe.Positions[owner]
|
|
if userPositions == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
position := userPositions[marketID]
|
|
if position == nil {
|
|
return nil, ErrPositionNotFound
|
|
}
|
|
|
|
// PnL = size * (markPrice - entryPrice) / Q96
|
|
priceDiff := new(big.Int).Sub(market.MarkPrice, position.EntryPrice)
|
|
pnl := new(big.Int).Mul(position.Size, priceDiff)
|
|
pnl.Div(pnl, Q96)
|
|
|
|
return pnl, nil
|
|
}
|
|
|
|
// UpdateMarkPrice updates the mark price for a market
|
|
func (pe *PerpetualEngine) UpdateMarkPrice(marketID [32]byte, newPrice *big.Int) error {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return ErrPoolNotFound
|
|
}
|
|
|
|
market.MarkPrice = new(big.Int).Set(newPrice)
|
|
return nil
|
|
}
|
|
|
|
// UpdateIndexPrice updates the oracle index price for a market
|
|
func (pe *PerpetualEngine) UpdateIndexPrice(marketID [32]byte, newPrice *big.Int) error {
|
|
pe.mu.Lock()
|
|
defer pe.mu.Unlock()
|
|
|
|
market, exists := pe.Markets[marketID]
|
|
if !exists {
|
|
return ErrPoolNotFound
|
|
}
|
|
|
|
market.IndexPrice = new(big.Int).Set(newPrice)
|
|
return nil
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func (pe *PerpetualEngine) settleFundingForPosition(position *PerpPosition, state *FundingState) *big.Int {
|
|
// Funding payment = size * (currentFundingIndex - lastFundingIndex) / Q96
|
|
fundingDiff := new(big.Int).Sub(state.CumulativeFunding, position.LastFundingIndex)
|
|
payment := new(big.Int).Mul(position.Size, fundingDiff)
|
|
payment.Div(payment, Q96)
|
|
|
|
// Longs pay shorts when funding > 0
|
|
// So longs get negative funding, shorts get positive
|
|
payment.Neg(payment)
|
|
|
|
position.LastFundingIndex = new(big.Int).Set(state.CumulativeFunding)
|
|
return payment
|
|
}
|
|
|
|
func (pe *PerpetualEngine) isPositionSafe(position *PerpPosition, market *PerpMarket, margin *big.Int) bool {
|
|
// Calculate maintenance margin requirement
|
|
positionSize := new(big.Int).Abs(position.Size)
|
|
notional := new(big.Int).Mul(positionSize, market.MarkPrice)
|
|
notional.Div(notional, Q96)
|
|
|
|
// maintenanceReq = notional * maintenanceMargin / 1e18
|
|
maintenanceReq := new(big.Int).Mul(notional, market.MaintenanceMargin)
|
|
maintenanceReq.Div(maintenanceReq, big.NewInt(1e18))
|
|
|
|
// Calculate unrealized PnL
|
|
priceDiff := new(big.Int).Sub(market.MarkPrice, position.EntryPrice)
|
|
pnl := new(big.Int).Mul(position.Size, priceDiff)
|
|
pnl.Div(pnl, Q96)
|
|
|
|
// Equity = margin + unrealizedPnL
|
|
equity := new(big.Int).Add(margin, pnl)
|
|
|
|
return equity.Cmp(maintenanceReq) >= 0
|
|
}
|
|
|
|
func generateMarketID(base, quote Currency) [32]byte {
|
|
var id [32]byte
|
|
copy(id[:20], base.Address[:])
|
|
copy(id[20:], quote.Address[:12])
|
|
return id
|
|
}
|
|
|
|
// Note: Error definitions are in types.go to avoid duplication
|