diff --git a/plugin/evm/orderbook/hubbleutils/margin_math.go b/plugin/evm/orderbook/hubbleutils/margin_math.go index e36230727b..e2755075fb 100644 --- a/plugin/evm/orderbook/hubbleutils/margin_math.go +++ b/plugin/evm/orderbook/hubbleutils/margin_math.go @@ -22,7 +22,7 @@ type UserState struct { } func GetAvailableMargin(hState *HubbleState, userState *UserState) *big.Int { - notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Min_Allowable_Margin) + notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Min_Allowable_Margin, 0) return GetAvailableMargin_(notionalPosition, margin, userState.ReservedMargin, hState.MinAllowableMargin) } @@ -32,31 +32,32 @@ func GetAvailableMargin_(notionalPosition, margin, reservedMargin, minAllowableM } func GetMarginFraction(hState *HubbleState, userState *UserState) *big.Int { - notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin) + notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin, 0) if notionalPosition.Sign() == 0 { return big.NewInt(math.MaxInt64) } return Div(Mul1e6(margin), notionalPosition) } -func GetNotionalPositionAndMargin(hState *HubbleState, userState *UserState, marginMode MarginMode) (*big.Int, *big.Int) { +func GetNotionalPositionAndMargin(hState *HubbleState, userState *UserState, marginMode MarginMode, blockTimestamp uint64) (*big.Int, *big.Int) { margin := Sub(GetNormalizedMargin(hState.Assets, userState.Margins), userState.PendingFunding) - notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode) + notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, blockTimestamp) return notionalPosition, Add(margin, unrealizedPnl) } -func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode) (*big.Int, *big.Int) { +func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode, blockTimestamp uint64) (*big.Int, *big.Int) { notionalPosition := big.NewInt(0) unrealizedPnl := big.NewInt(0) + for _, market := range hState.ActiveMarkets { - _notionalPosition, _unrealizedPnl := GetOptimalPnl(hState, userState.Positions[market], margin, market, marginMode) + _notionalPosition, _unrealizedPnl := getOptimalPnl(hState, userState.Positions[market], margin, market, marginMode, blockTimestamp) notionalPosition.Add(notionalPosition, _notionalPosition) unrealizedPnl.Add(unrealizedPnl, _unrealizedPnl) } return notionalPosition, unrealizedPnl } -func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, market Market, marginMode MarginMode) (notionalPosition *big.Int, uPnL *big.Int) { +func getOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, market Market, marginMode MarginMode, blockTimestamp uint64) (notionalPosition *big.Int, uPnL *big.Int) { if position == nil || position.Size.Sign() == 0 { return big.NewInt(0), big.NewInt(0) } @@ -77,6 +78,16 @@ func GetOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, mar margin, ) + // Thursday, 12 October 2023 16:45:00 GMT + if blockTimestamp == 0 || blockTimestamp >= 1697129100 { // use new algorithm + if (marginMode == Maintenance_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == 1) || // for liquidations + (marginMode == Min_Allowable_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == -1) { // for increasing leverage + return oracleBasedNotional, oracleBasedUnrealizedPnl + } + return notionalPosition, unrealizedPnl + } + + // use old algorithm if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == 1) || // for liquidations (marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == -1) { // for increasing leverage return oracleBasedNotional, oracleBasedUnrealizedPnl diff --git a/plugin/evm/orderbook/hubbleutils/margin_math_test.go b/plugin/evm/orderbook/hubbleutils/margin_math_test.go index f7970eab1d..e37cc36711 100644 --- a/plugin/evm/orderbook/hubbleutils/margin_math_test.go +++ b/plugin/evm/orderbook/hubbleutils/margin_math_test.go @@ -8,25 +8,62 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWeightedAndSpotCollateral(t *testing.T) { - assets := []Collateral{ +var hState = &HubbleState{ + Assets: []Collateral{ { - Price: big.NewInt(80500000), // 80.5 - Weight: big.NewInt(800000), // 0.8 + Price: big.NewInt(1.01 * 1e6), // 1.01 + Weight: big.NewInt(1e6), // 1 Decimals: 6, }, { - Price: big.NewInt(410000), // 0.41 - Weight: big.NewInt(900000), // 0.9 + Price: big.NewInt(54.36 * 1e6), // 54.36 + Weight: big.NewInt(0.7 * 1e6), // 0.7 Decimals: 6, }, - } - margins := []*big.Int{ - big.NewInt(3500000), // 3.5 - big.NewInt(1040000000), // 1040 - } - expectedWeighted := big.NewInt(609160000) // 609.16 - expectedSpot := big.NewInt(708150000) // 708.15 + }, + MidPrices: map[Market]*big.Int{ + 0: big.NewInt(1544.21 * 1e6), // 1544.21 + 1: big.NewInt(19.5 * 1e6), // 19.5 + }, + OraclePrices: map[Market]*big.Int{ + 0: big.NewInt(1503.21 * 1e6), + 1: big.NewInt(17.5 * 1e6), + }, + ActiveMarkets: []Market{ + 0, 1, + }, + MinAllowableMargin: big.NewInt(100000), // 0.1 + MaintenanceMargin: big.NewInt(200000), // 0.2 +} + +var userState = &UserState{ + Positions: map[Market]*Position{ + 0: { + Size: big.NewInt(0.582 * 1e18), // 0.0582 + OpenNotional: big.NewInt(875 * 1e6), // 87.5, openPrice = 1503.43 + }, + 1: { + Size: Scale(big.NewInt(-101), 18), // -101 + OpenNotional: big.NewInt(1767.5 * 1e6), // 1767.5, openPrice = 17.5 + }, + }, + Margins: []*big.Int{ + big.NewInt(30.5 * 1e6), // 30.5 + big.NewInt(14 * 1e6), // 14 + }, + PendingFunding: big.NewInt(0), + ReservedMargin: big.NewInt(0), +} + +func TestWeightedAndSpotCollateral(t *testing.T) { + assets := hState.Assets + margins := userState.Margins + expectedWeighted := Unscale(Mul(Mul(margins[0], assets[0].Price), assets[0].Weight), assets[0].Decimals+6) + expectedWeighted.Add(expectedWeighted, Unscale(Mul(Mul(margins[1], assets[1].Price), assets[1].Weight), assets[1].Decimals+6)) + + expectedSpot := Unscale(Mul(margins[0], assets[0].Price), assets[0].Decimals) + expectedSpot.Add(expectedSpot, Unscale(Mul(margins[1], assets[1].Price), assets[1].Decimals)) + resultWeighted, resultSpot := WeightedAndSpotCollateral(assets, margins) fmt.Println(resultWeighted, resultSpot) assert.Equal(t, expectedWeighted, resultWeighted) @@ -79,43 +116,17 @@ func TestGetPositionMetadata(t *testing.T) { } func TestGetOptimalPnl(t *testing.T) { - hState := &HubbleState{ - Assets: []Collateral{ - { - Price: big.NewInt(101000000), // 101 - Weight: big.NewInt(900000), // 0.9 - Decimals: 6, - }, - { - Price: big.NewInt(54360000), // 54.36 - Weight: big.NewInt(700000), // 0.7 - Decimals: 6, - }, - }, - MidPrices: map[Market]*big.Int{ - 0: big.NewInt(1545340000), // 1545.34 - }, - OraclePrices: map[Market]*big.Int{ - 0: big.NewInt(1545210000), // 1545.21 - }, - ActiveMarkets: []Market{ - 0, - }, - MinAllowableMargin: big.NewInt(100000), // 0.1 - MaintenanceMargin: big.NewInt(200000), // 0.2 - } - position := &Position{ - Size: Scale(big.NewInt(582), 14), // 0.0582 - OpenNotional: big.NewInt(87500000), // 87.5 - } - margin := big.NewInt(20000000) // 20 + margin := big.NewInt(20 * 1e6) // 20 market := 0 + position := userState.Positions[market] marginMode := Maintenance_Margin - notionalPosition, uPnL := GetOptimalPnl(hState, position, margin, market, marginMode) + notionalPosition, uPnL := getOptimalPnl(hState, position, margin, market, marginMode, 0) - expectedNotionalPosition := big.NewInt(89938788) - expectedUPnL := big.NewInt(2438788) + // mid price pnl is more than oracle price pnl + expectedNotionalPosition := Unscale(Mul(position.Size, hState.MidPrices[market]), 18) + expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) assert.Equal(t, expectedNotionalPosition, notionalPosition) assert.Equal(t, expectedUPnL, uPnL) @@ -123,11 +134,87 @@ func TestGetOptimalPnl(t *testing.T) { // ------ when marginMode is Min_Allowable_Margin ------ marginMode = Min_Allowable_Margin + notionalPosition, uPnL = getOptimalPnl(hState, position, margin, market, marginMode, 0) + + expectedNotionalPosition = Unscale(Mul(position.Size, hState.OraclePrices[market]), 18) + expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} + +func TestGetOptimalPnlDeprecated(t *testing.T) { + margin := big.NewInt(20 * 1e6) // 20 + market := 0 + position := userState.Positions[market] + marginMode := Maintenance_Margin - notionalPosition, uPnL = GetOptimalPnl(hState, position, margin, market, marginMode) + notionalPosition, uPnL := getOptimalPnl(hState, position, margin, market, marginMode, 1) - expectedNotionalPosition = big.NewInt(89931222) - expectedUPnL = big.NewInt(2431222) + // mid price pnl is more than oracle price pnl + expectedNotionalPosition := Unscale(Mul(position.Size, hState.MidPrices[market]), 18) + expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = getOptimalPnl(hState, position, margin, market, marginMode, 1) + + expectedNotionalPosition = Unscale(Mul(position.Size, hState.OraclePrices[market]), 18) + expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} + +func TestGetTotalNotionalPositionAndUnrealizedPnl(t *testing.T) { + margin := GetNormalizedMargin(hState.Assets, userState.Margins) + // margin := big.NewInt(2000 * 1e6) // 50 + fmt.Println("margin = ", margin) // 563.533 + marginMode := Maintenance_Margin + fmt.Println("availableMargin = ", GetAvailableMargin(hState, userState)) + fmt.Println("marginFraction = ", GetMarginFraction(hState, userState)) + + notionalPosition, uPnL := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, 0) + fmt.Println("Maintenace_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL) + _, pnl := getOptimalPnl(hState, userState.Positions[0], margin, 0, marginMode, 0) + fmt.Println("best pnl market 0 =", pnl) + _, pnl = getOptimalPnl(hState, userState.Positions[1], margin, 1, marginMode, 0) + fmt.Println("best pnl market 1 =", pnl) + + // mid price pnl is more than oracle price pnl for long position + expectedNotionalPosition := Unscale(Mul(userState.Positions[0].Size, hState.MidPrices[0]), 18) + expectedUPnL := Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + // oracle price pnl is more than mid price pnl for short position + expectedNotional2 := Abs(Unscale(Mul(userState.Positions[1].Size, hState.OraclePrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode, 0) + fmt.Println("Min_Allowable_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL) + + _, pnl = getOptimalPnl(hState, userState.Positions[0], margin, 0, marginMode, 0) + fmt.Println("worst pnl market 0 =", pnl) + _, pnl = getOptimalPnl(hState, userState.Positions[1], margin, 1, marginMode, 0) + fmt.Println("worst pnl market 1 =", pnl) + + expectedNotionalPosition = Unscale(Mul(userState.Positions[0].Size, hState.OraclePrices[0]), 18) + expectedUPnL = Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + expectedNotional2 = Abs(Unscale(Mul(userState.Positions[1].Size, hState.MidPrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) assert.Equal(t, expectedNotionalPosition, notionalPosition) assert.Equal(t, expectedUPnL, uPnL) diff --git a/plugin/evm/orderbook/liquidations.go b/plugin/evm/orderbook/liquidations.go index 0bdd7caa24..43680fce98 100644 --- a/plugin/evm/orderbook/liquidations.go +++ b/plugin/evm/orderbook/liquidations.go @@ -82,6 +82,7 @@ func getTotalNotionalPositionAndUnrealizedPnl(trader *Trader, margin *big.Int, m }, margin, marginMode, + 0, ) } diff --git a/precompile/contracts/bibliophile/api.go b/precompile/contracts/bibliophile/api.go index 5858056c3b..bec01fb409 100644 --- a/precompile/contracts/bibliophile/api.go +++ b/precompile/contracts/bibliophile/api.go @@ -30,7 +30,7 @@ func GetClearingHouseVariables(stateDB contract.StateDB, trader common.Address) Trader: trader, IncludeFundingPayments: false, Mode: 0, - }) + }, 0 /* use new algorithm */) totalFunding := GetTotalFunding(stateDB, &trader) positionSizes := getPosSizes(stateDB, &trader) underlyingPrices := GetUnderlyingPrices(stateDB) @@ -131,7 +131,7 @@ func GetAMMVariables(stateDB contract.StateDB, ammAddress common.Address, ammInd minAllowableMargin := GetMinAllowableMargin(stateDB) takerFee := GetTakerFee(stateDB) totalMargin := GetNormalizedMargin(stateDB, trader) - availableMargin := GetAvailableMargin(stateDB, trader) + availableMargin := GetAvailableMargin(stateDB, trader, 0) reduceOnlyAmount := getReduceOnlyAmount(stateDB, trader, big.NewInt(ammIndex)) longOpenOrdersAmount := getLongOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex)) shortOpenOrdersAmount := getShortOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex)) diff --git a/precompile/contracts/bibliophile/clearing_house.go b/precompile/contracts/bibliophile/clearing_house.go index bcd67c7ec5..79a06d1d0e 100644 --- a/precompile/contracts/bibliophile/clearing_house.go +++ b/precompile/contracts/bibliophile/clearing_house.go @@ -66,7 +66,7 @@ type GetNotionalPositionAndMarginOutput struct { Margin *big.Int } -func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput) GetNotionalPositionAndMarginOutput { +func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput, blockTimestamp uint64) GetNotionalPositionAndMarginOutput { markets := GetMarkets(stateDB) numMarkets := len(markets) positions := make(map[int]*hu.Position, numMarkets) @@ -96,6 +96,7 @@ func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPo PendingFunding: pendingFunding, }, input.Mode, + blockTimestamp, ) return GetNotionalPositionAndMarginOutput{ NotionalPosition: notionalPosition, diff --git a/precompile/contracts/bibliophile/client.go b/precompile/contracts/bibliophile/client.go index 5faf538d8b..7787e1dad2 100644 --- a/precompile/contracts/bibliophile/client.go +++ b/precompile/contracts/bibliophile/client.go @@ -178,11 +178,11 @@ func (b *bibliophileClient) GetReduceOnlyAmount(trader common.Address, ammIndex } func (b *bibliophileClient) GetAvailableMargin(trader common.Address) *big.Int { - return GetAvailableMargin(b.accessibleState.GetStateDB(), trader) + return GetAvailableMargin(b.accessibleState.GetStateDB(), trader, b.GetTimeStamp()) } func (b *bibliophileClient) GetNotionalPositionAndMargin(trader common.Address, includeFundingPayments bool, mode uint8) (*big.Int, *big.Int) { - output := getNotionalPositionAndMargin(b.accessibleState.GetStateDB(), &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: includeFundingPayments, Mode: mode}) + output := getNotionalPositionAndMargin(b.accessibleState.GetStateDB(), &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: includeFundingPayments, Mode: mode}, b.GetTimeStamp()) return output.NotionalPosition, output.Margin } diff --git a/precompile/contracts/bibliophile/margin_account.go b/precompile/contracts/bibliophile/margin_account.go index 631805dc18..6a41f57dbd 100644 --- a/precompile/contracts/bibliophile/margin_account.go +++ b/precompile/contracts/bibliophile/margin_account.go @@ -44,8 +44,8 @@ func getReservedMargin(stateDB contract.StateDB, trader common.Address) *big.Int return stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BytesToHash(baseMappingHash)).Big() } -func GetAvailableMargin(stateDB contract.StateDB, trader common.Address) *big.Int { - output := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: true, Mode: uint8(1)}) // Min_Allowable_Margin +func GetAvailableMargin(stateDB contract.StateDB, trader common.Address, blockTimeStamp uint64) *big.Int { + output := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: true, Mode: uint8(1)}, blockTimeStamp) // Min_Allowable_Margin return hu.GetAvailableMargin_(output.NotionalPosition, output.Margin, getReservedMargin(stateDB, trader), GetMinAllowableMargin(stateDB)) }