diff --git a/plugin/evm/limit_order.go b/plugin/evm/limit_order.go index 55da3f4e7b..1812f72aef 100644 --- a/plugin/evm/limit_order.go +++ b/plugin/evm/limit_order.go @@ -462,6 +462,7 @@ func executeFuncAndRecoverPanic(fn func(), panicMessage string, panicCounter met log.Error(panicMessage, "errorMessage", errorMessage, "stack_trace", string(debug.Stack())) panicCounter.Inc(1) + orderbook.AllPanicsCounter.Inc(1) } }() fn() diff --git a/plugin/evm/orderbook/config_service.go b/plugin/evm/orderbook/config_service.go index 85c686df23..de00d77056 100644 --- a/plugin/evm/orderbook/config_service.go +++ b/plugin/evm/orderbook/config_service.go @@ -24,6 +24,7 @@ type IConfigService interface { GetCumulativePremiumFraction(market Market) *big.Int GetAcceptableBounds(market Market) (*big.Int, *big.Int) GetAcceptableBoundsForLiquidation(market Market) (*big.Int, *big.Int) + GetTakerFee() *big.Int } type ConfigService struct { @@ -102,3 +103,8 @@ func (cs *ConfigService) GetCumulativePremiumFraction(market Market) *big.Int { markets := bibliophile.GetMarkets(cs.getStateAtCurrentBlock()) return bibliophile.GetCumulativePremiumFraction(cs.getStateAtCurrentBlock(), markets[market]) } + +func (cs *ConfigService) GetTakerFee() *big.Int { + takerFee := bibliophile.GetTakerFee(cs.getStateAtCurrentBlock()) + return hu.Div(hu.Mul(takerFee, big.NewInt(8)), big.NewInt(10)) // 20% discount, which is applied to everyone currently +} diff --git a/plugin/evm/orderbook/contract_events_processor.go b/plugin/evm/orderbook/contract_events_processor.go index c43b9df865..723429c390 100644 --- a/plugin/evm/orderbook/contract_events_processor.go +++ b/plugin/evm/orderbook/contract_events_processor.go @@ -155,9 +155,10 @@ func (cep *ContractEventsProcessor) handleOrderBookEvent(event *types.Log) { return } orderId := event.Topics[1] + errorString := args["err"].(string) if !removed { log.Info("OrderMatchingError", "args", args, "orderId", orderId.String(), "TxHash", event.TxHash, "number", event.BlockNumber) - if err := cep.database.SetOrderStatus(orderId, Execution_Failed, args["err"].(string), event.BlockNumber); err != nil { + if err := cep.database.SetOrderStatus(orderId, Execution_Failed, errorString, event.BlockNumber); err != nil { log.Error("error in SetOrderStatus", "method", "OrderMatchingError", "err", err) return } @@ -704,17 +705,32 @@ func (cep *ContractEventsProcessor) updateMetrics(logs []*types.Log) { metricName := fmt.Sprintf("%s/%s", "events", event_.Name) - if !event.Removed { - metrics.GetOrRegisterCounter(metricName, nil).Inc(1) - } else { - metrics.GetOrRegisterCounter(metricName, nil).Dec(1) - } + metrics.GetOrRegisterCounter(metricName, nil).Inc(1) switch event_.Name { case "OrderAccepted": - orderAcceptedCount++ + orderAcceptedCount += 1 case "OrderCancelAccepted": - orderCancelledCount++ + orderCancelledCount += 1 + case "OrderMatchingError": + // separate metrics for combination of order type and error string - for more granular analysis + args := map[string]interface{}{} + err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatchingError", event.Data) + if err != nil { + log.Error("error in orderBookAbi.UnpackIntoMap", "method", "OrderMatchingError", "err", err) + return + } + orderId := event.Topics[1] + errorString := args["err"].(string) + + order := cep.database.GetOrderById(orderId) + if order != nil { + ordertype := order.OrderType + metricName := fmt.Sprintf("%s/%s/%s/%s", "events", "OrderMatchingError", ordertype, utils.RemoveSpacesAndSpecialChars(errorString)) + metrics.GetOrRegisterCounter(metricName, nil).Inc(1) + } else { + log.Error("updateMetrics - error in getting order", "event", "OrderMatchingError") + } } } diff --git a/plugin/evm/orderbook/hubbleutils/margin_math.go b/plugin/evm/orderbook/hubbleutils/margin_math.go index e2755075fb..941894583f 100644 --- a/plugin/evm/orderbook/hubbleutils/margin_math.go +++ b/plugin/evm/orderbook/hubbleutils/margin_math.go @@ -12,6 +12,7 @@ type HubbleState struct { ActiveMarkets []Market MinAllowableMargin *big.Int MaintenanceMargin *big.Int + TakerFee *big.Int } type UserState struct { diff --git a/plugin/evm/orderbook/liquidations_test.go b/plugin/evm/orderbook/liquidations_test.go index 22ca3f2c36..25ad19f351 100644 --- a/plugin/evm/orderbook/liquidations_test.go +++ b/plugin/evm/orderbook/liquidations_test.go @@ -20,7 +20,7 @@ func TestGetLiquidableTraders(t *testing.T) { OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))}, ActiveMarkets: []hu.Market{market}, } - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -44,7 +44,7 @@ func TestGetLiquidableTraders(t *testing.T) { ActiveMarkets: []hu.Market{market}, MaintenanceMargin: db.configService.getMaintenanceMargin(), } - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -109,7 +109,7 @@ func TestGetLiquidableTraders(t *testing.T) { marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -165,7 +165,7 @@ func TestGetLiquidableTraders(t *testing.T) { marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) }) @@ -228,7 +228,7 @@ func TestGetLiquidableTraders(t *testing.T) { marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) @@ -283,7 +283,7 @@ func TestGetLiquidableTraders(t *testing.T) { marginFraction := calcMarginFraction(_trader, hState) assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction) - liquidablePositions, _ := db.GetNaughtyTraders(hState) + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) assert.Equal(t, 0, len(liquidablePositions)) }) }) diff --git a/plugin/evm/orderbook/matching_pipeline.go b/plugin/evm/orderbook/matching_pipeline.go index 56d015bebb..bff7ebce6f 100644 --- a/plugin/evm/orderbook/matching_pipeline.go +++ b/plugin/evm/orderbook/matching_pipeline.go @@ -79,19 +79,21 @@ func (pipeline *MatchingPipeline) Run(blockNumber *big.Int) bool { ActiveMarkets: markets, MinAllowableMargin: pipeline.configService.getMinAllowableMargin(), MaintenanceMargin: pipeline.configService.getMaintenanceMargin(), + TakerFee: pipeline.configService.GetTakerFee(), } // build trader map - liquidablePositions, ordersToCancel := pipeline.db.GetNaughtyTraders(hState) + liquidablePositions, ordersToCancel, marginMap := pipeline.db.GetNaughtyTraders(hState) cancellableOrderIds := pipeline.cancelLimitOrders(ordersToCancel) orderMap := make(map[Market]*Orders) for _, market := range markets { orderMap[market] = pipeline.fetchOrders(market, hState.OraclePrices[market], cancellableOrderIds, blockNumber) } - pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices) + pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices, marginMap) for _, market := range markets { // @todo should we prioritize matching in any particular market? - pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders) + upperBound, _ := pipeline.configService.GetAcceptableBounds(market) + pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders, marginMap, hState.MinAllowableMargin, hState.TakerFee, upperBound) } orderBookTxsCount := pipeline.lotp.GetOrderBookTxsCount() @@ -185,7 +187,7 @@ func (pipeline *MatchingPipeline) fetchOrders(market Market, underlyingPrice *bi return &Orders{longOrders, shortOrders} } -func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []LiquidablePosition, orderMap map[Market]*Orders, underlyingPrices map[Market]*big.Int) { +func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []LiquidablePosition, orderMap map[Market]*Orders, underlyingPrices map[Market]*big.Int, marginMap map[common.Address]*big.Int) { if len(liquidablePositions) == 0 { return } @@ -204,6 +206,8 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab liquidationBounds[market] = S{Upperbound: upperbound, Lowerbound: lowerbound} } + minAllowableMargin := pipeline.configService.getMinAllowableMargin() + takerFee := pipeline.configService.GetTakerFee() for _, liquidable := range liquidablePositions { market := liquidable.Market numOrdersExhausted := 0 @@ -215,6 +219,16 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab break } fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity()) + requiredMargin := getRequiredMargin(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound) + if marginMap[order.Trader] == nil { + // compatibility with existing tests + marginMap[order.Trader] = big.NewInt(0) + } + if requiredMargin.Cmp(marginMap[order.Trader]) == 1 { + numOrdersExhausted++ + continue + } + marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount) order.FilledBaseAssetQuantity.Add(order.FilledBaseAssetQuantity, fillAmount) liquidable.FilledSize.Add(liquidable.FilledSize, fillAmount) @@ -233,6 +247,15 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab break } fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity()) + requiredMargin := getRequiredMargin(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound) + if marginMap[order.Trader] == nil { + marginMap[order.Trader] = big.NewInt(0) + } + if requiredMargin.Cmp(marginMap[order.Trader]) == 1 { + numOrdersExhausted++ + continue + } + marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount) order.FilledBaseAssetQuantity.Sub(order.FilledBaseAssetQuantity, fillAmount) liquidable.FilledSize.Sub(liquidable.FilledSize, fillAmount) @@ -246,12 +269,13 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab orderMap[market].shortOrders = orderMap[market].shortOrders[numOrdersExhausted:] } if liquidable.GetUnfilledSize().Sign() != 0 { + unquenchedLiquidationsCounter.Inc(1) log.Info("unquenched liquidation", "liquidable", liquidable) } } } -func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order) { +func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) { for i := 0; i < len(longOrders); i++ { // if there are no short orders or if the price of the first long order is < the price of the first short order, then we can stop matching if len(shortOrders) == 0 || longOrders[i].Price.Cmp(shortOrders[0].Price) == -1 { @@ -259,7 +283,7 @@ func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, } numOrdersExhausted := 0 for j := 0; j < len(shortOrders); j++ { - fillAmount := areMatchingOrders(longOrders[i], shortOrders[j]) + fillAmount := areMatchingOrders(longOrders[i], shortOrders[j], marginMap, minAllowableMargin, takerFee, upperBound) if fillAmount == nil { continue } @@ -275,7 +299,7 @@ func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, } } -func areMatchingOrders(longOrder, shortOrder Order) *big.Int { +func areMatchingOrders(longOrder, shortOrder Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int { if longOrder.Price.Cmp(shortOrder.Price) == -1 { return nil } @@ -288,9 +312,42 @@ func areMatchingOrders(longOrder, shortOrder Order) *big.Int { if fillAmount.Sign() == 0 { return nil } + + // for ioc orders, check that they have enough margin to execute the trade + longMargin := big.NewInt(0) + shortMargin := big.NewInt(0) + if longOrder.OrderType == IOC { + longMargin := getRequiredMargin(&longOrder, fillAmount, minAllowableMargin, takerFee, upperBound) + if longMargin.Cmp(marginMap[longOrder.Trader]) == 1 { + return nil + } + } + if shortOrder.OrderType == IOC { + shortMargin := getRequiredMargin(&shortOrder, fillAmount, minAllowableMargin, takerFee, upperBound) + if shortMargin.Cmp(marginMap[shortOrder.Trader]) == 1 { + return nil + } + } + marginMap[longOrder.Trader].Sub(marginMap[longOrder.Trader], longMargin) + marginMap[shortOrder.Trader].Sub(marginMap[shortOrder.Trader], shortMargin) return fillAmount } +func getRequiredMargin(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int { + if order.OrderType != IOC { + return big.NewInt(0) // no extra margin required because for limit orders it is already reserved + // @todo change for signed orders + } + price := order.Price + if order.BaseAssetQuantity.Sign() == -1 && order.Price.Cmp(upperBound) == -1 { + price = upperBound + } + quoteAsset := hu.Div1e18(hu.Mul(fillAmount, price)) // fillAmount is scaled by 18 decimals + requiredMargin := hu.Div1e6(hu.Mul(minAllowableMargin, quoteAsset)) + _takerFee := hu.Div1e6(hu.Mul(quoteAsset, takerFee)) + return hu.Add(requiredMargin, _takerFee) +} + func ExecuteMatchedOrders(lotp LimitOrderTxProcessor, longOrder, shortOrder Order, fillAmount *big.Int) (Order, Order) { lotp.ExecuteMatchedOrdersTx(longOrder, shortOrder, fillAmount) longOrder.FilledBaseAssetQuantity = big.NewInt(0).Add(longOrder.FilledBaseAssetQuantity, fillAmount) diff --git a/plugin/evm/orderbook/matching_pipeline_test.go b/plugin/evm/orderbook/matching_pipeline_test.go index f8fbbfd6b1..90f2fc9de6 100644 --- a/plugin/evm/orderbook/matching_pipeline_test.go +++ b/plugin/evm/orderbook/matching_pipeline_test.go @@ -25,7 +25,7 @@ func TestRunLiquidations(t *testing.T) { shortOrders := []Order{getShortOrder()} orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} - pipeline.runLiquidations([]LiquidablePosition{}, orderMap, underlyingPrices) + pipeline.runLiquidations([]LiquidablePosition{}, orderMap, underlyingPrices, map[common.Address]*big.Int{}) assert.Equal(t, longOrders, orderMap[market].longOrders) assert.Equal(t, shortOrders, orderMap[market].shortOrders) lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) @@ -40,7 +40,9 @@ func TestRunLiquidations(t *testing.T) { orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) - pipeline.runLiquidations([]LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7)}, orderMap, underlyingPrices) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + pipeline.runLiquidations([]LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7)}, orderMap, underlyingPrices, map[common.Address]*big.Int{}) assert.Equal(t, longOrders, orderMap[market].longOrders) assert.Equal(t, shortOrders, orderMap[market].shortOrders) lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) @@ -53,11 +55,13 @@ func TestRunLiquidations(t *testing.T) { shortOrder := getShortOrder() expectedFillAmount := utils.BigIntMinAbs(longOrder.BaseAssetQuantity, liquidablePositions[0].Size) cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) lotp.On("ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount).Return(nil) orderMap := map[Market]*Orders{market: {[]Order{longOrder}, []Order{shortOrder}}} - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount) cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) @@ -75,11 +79,13 @@ func TestRunLiquidations(t *testing.T) { expectedFillAmount := utils.BigIntMinAbs(longOrder.BaseAssetQuantity, liquidablePositions[0].Size) cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) lotp.On("ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount).Return(nil) orderMap := map[Market]*Orders{market: {[]Order{longOrder, longOrder2}, []Order{}}} - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount) cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) @@ -99,6 +105,8 @@ func TestRunLiquidations(t *testing.T) { orderMap := map[Market]*Orders{market: {[]Order{longOrder0, longOrder1}, []Order{shortOrder0, shortOrder1}}} cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) lotp.On("ExecuteLiquidation", traderAddress, orderMap[market].longOrders[0], big.NewInt(5)).Return(nil) lotp.On("ExecuteLiquidation", traderAddress, orderMap[market].longOrders[1], big.NewInt(2)).Return(nil) lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].longOrders[1], big.NewInt(9)).Return(nil) @@ -106,7 +114,7 @@ func TestRunLiquidations(t *testing.T) { lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].shortOrders[0], big.NewInt(1)).Return(nil) lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].shortOrders[1], big.NewInt(1)).Return(nil) - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder0, big.NewInt(5)) @@ -144,7 +152,9 @@ func TestRunLiquidations(t *testing.T) { orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) assert.Equal(t, longOrders, orderMap[market].longOrders) assert.Equal(t, shortOrders, orderMap[market].shortOrders) lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) @@ -157,9 +167,11 @@ func TestRunLiquidations(t *testing.T) { expectedFillAmount := utils.BigIntMinAbs(shortOrder.BaseAssetQuantity, liquidablePositions[0].Size) lotp.On("ExecuteLiquidation", traderAddress, shortOrder, expectedFillAmount).Return(nil) cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("getMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) orderMap := map[Market]*Orders{market: {[]Order{longOrder}, []Order{shortOrder}}} - pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, shortOrder, expectedFillAmount) cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) @@ -171,19 +183,22 @@ func TestRunLiquidations(t *testing.T) { } func TestRunMatchingEngine(t *testing.T) { + minAllowableMargin := big.NewInt(1e6) + takerFee := big.NewInt(1e6) + upperBound := big.NewInt(22) t.Run("when either long or short orders are not present in memorydb", func(t *testing.T) { t.Run("when no short and long orders are present", func(t *testing.T) { _, lotp, pipeline, _, _ := setupDependencies(t) longOrders := make([]Order, 0) shortOrders := make([]Order, 0) - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) }) t.Run("when longOrders are not present but short orders are present", func(t *testing.T) { _, lotp, pipeline, _, _ := setupDependencies(t) longOrders := make([]Order, 0) shortOrders := []Order{getShortOrder()} - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) }) t.Run("when short orders are not present but long orders are present", func(t *testing.T) { @@ -195,7 +210,7 @@ func TestRunMatchingEngine(t *testing.T) { db.On("GetLongOrders").Return(longOrders) db.On("GetShortOrders").Return(shortOrders) lotp.On("PurgeLocalTx").Return(nil) - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) }) }) @@ -206,7 +221,7 @@ func TestRunMatchingEngine(t *testing.T) { longOrder := getLongOrder() longOrder.Price.Sub(shortOrder.Price, big.NewInt(1)) - pipeline.runMatchingEngine(lotp, []Order{longOrder}, []Order{shortOrder}) + pipeline.runMatchingEngine(lotp, []Order{longOrder}, []Order{shortOrder}, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) }) t.Run("When longOrder.Price >= shortOrder.Price same", func(t *testing.T) { @@ -229,9 +244,12 @@ func TestRunMatchingEngine(t *testing.T) { fillAmount1 := longOrder1.BaseAssetQuantity fillAmount2 := longOrder2.BaseAssetQuantity + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } lotp.On("ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount1).Return(nil) lotp.On("ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount2).Return(nil) - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount1) lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount2) }) @@ -256,7 +274,10 @@ func TestRunMatchingEngine(t *testing.T) { db.On("GetShortOrders").Return(shortOrders) lotp.On("PurgeLocalTx").Return(nil) lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount).Return(nil) - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount) }) }) @@ -293,7 +314,10 @@ func TestRunMatchingEngine(t *testing.T) { db.On("GetShortOrders").Return(shortOrders) lotp.On("PurgeLocalTx").Return(nil) log.Info("longOrder1", "longOrder1", longOrder1) - pipeline.runMatchingEngine(lotp, longOrders, shortOrders) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) log.Info("longOrder1", "longOrder1", longOrder1) //During 1st matching iteration @@ -492,6 +516,10 @@ func TestMatchLongAndShortOrder(t *testing.T) { } func TestAreMatchingOrders(t *testing.T) { + minAllowableMargin := big.NewInt(1e6) + takerFee := big.NewInt(1e6) + upperBound := big.NewInt(22) + trader := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") longOrder_ := Order{ Market: 1, @@ -547,7 +575,10 @@ func TestAreMatchingOrders(t *testing.T) { shortOrder := deepCopyOrder(&shortOrder_) longOrder.Price = big.NewInt(80) - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) @@ -565,8 +596,10 @@ func TestAreMatchingOrders(t *testing.T) { OrderType: 1, ExpireAt: big.NewInt(0), } - - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) t.Run("short order is post only", func(t *testing.T) { @@ -574,8 +607,10 @@ func TestAreMatchingOrders(t *testing.T) { longOrder.BlockNumber = big.NewInt(20) shortOrder.RawOrder.(*LimitOrder).PostOnly = true - - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) }) @@ -593,8 +628,10 @@ func TestAreMatchingOrders(t *testing.T) { OrderType: 1, ExpireAt: big.NewInt(0), } - - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) t.Run("longOrder is post only", func(t *testing.T) { @@ -602,8 +639,10 @@ func TestAreMatchingOrders(t *testing.T) { longOrder.BlockNumber = big.NewInt(21) longOrder.RawOrder.(*LimitOrder).PostOnly = true - - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) }) @@ -613,7 +652,10 @@ func TestAreMatchingOrders(t *testing.T) { shortOrder := deepCopyOrder(&shortOrder_) longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Nil(t, actualFillAmount) }) @@ -622,7 +664,10 @@ func TestAreMatchingOrders(t *testing.T) { shortOrder := deepCopyOrder(&shortOrder_) longOrder.FilledBaseAssetQuantity = big.NewInt(5) - actualFillAmount := areMatchingOrders(longOrder, shortOrder) + marginMap := map[common.Address]*big.Int{ + common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000 + } + actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) assert.Equal(t, big.NewInt(5), actualFillAmount) }) } diff --git a/plugin/evm/orderbook/memory_database.go b/plugin/evm/orderbook/memory_database.go index 9def549b3f..4ea0c38be4 100644 --- a/plugin/evm/orderbook/memory_database.go +++ b/plugin/evm/orderbook/memory_database.go @@ -240,7 +240,7 @@ type LimitOrderDatabase interface { Accept(acceptedBlockNumber uint64, blockTimestamp uint64) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error RevertLastStatus(orderId common.Hash) error - GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) + GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) GetAllOpenOrdersForTrader(trader common.Address) []Order GetOpenOrdersForTraderByType(trader common.Address, orderType OrderType) []Order UpdateLastPremiumFraction(market Market, trader common.Address, lastPremiumFraction *big.Int, cumlastPremiumFraction *big.Int) @@ -927,12 +927,13 @@ func determinePositionToLiquidate(trader *Trader, addr common.Address, marginFra return liquidable } -func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) { +func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) { db.mu.RLock() defer db.mu.RUnlock() liquidablePositions := []LiquidablePosition{} ordersToCancel := map[common.Address][]Order{} + marginMap := map[common.Address]*big.Int{} count := 0 // will be updated lazily only if liquidablePositions are found @@ -962,7 +963,8 @@ func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]Liquida // has orders that might be cancellable availableMargin := hu.GetAvailableMargin(hState, userState) if availableMargin.Sign() == -1 { - foundCancellableOrders := db.determineOrdersToCancel(addr, trader, availableMargin, hState.OraclePrices, ordersToCancel) + foundCancellableOrders := false + foundCancellableOrders, marginMap[addr] = db.determineOrdersToCancel(addr, trader, availableMargin, hState.OraclePrices, ordersToCancel, hState.MinAllowableMargin) if foundCancellableOrders { log.Info("negative available margin", "trader", addr.String(), "availableMargin", prettifyScaledBigInt(availableMargin, 6)) } else { @@ -975,12 +977,16 @@ func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]Liquida } // lower margin fraction positions should be liquidated first sortLiquidableSliceByMarginFraction(liquidablePositions) - return liquidablePositions, ordersToCancel + return liquidablePositions, ordersToCancel, marginMap } // assumes db.mu.RLock has been held by the caller -func (db *InMemoryDatabase) determineOrdersToCancel(addr common.Address, trader *Trader, availableMargin *big.Int, oraclePrices map[Market]*big.Int, ordersToCancel map[common.Address][]Order) bool { +func (db *InMemoryDatabase) determineOrdersToCancel(addr common.Address, trader *Trader, availableMargin *big.Int, oraclePrices map[Market]*big.Int, ordersToCancel map[common.Address][]Order, minAllowableMargin *big.Int) (bool, *big.Int) { traderOrders := db.getTraderOrders(addr, Limit) + if len(traderOrders) == 0 { + return false, availableMargin + } + sort.Slice(traderOrders, func(i, j int) bool { // higher diff comes first iDiff := big.NewInt(0).Abs(big.NewInt(0).Sub(traderOrders[i].Price, oraclePrices[traderOrders[i].Market])) @@ -989,25 +995,24 @@ func (db *InMemoryDatabase) determineOrdersToCancel(addr common.Address, trader }) _availableMargin := new(big.Int).Set(availableMargin) - if len(traderOrders) > 0 { - // cancel orders until available margin is positive - ordersToCancel[addr] = []Order{} - for _, order := range traderOrders { - // cannot cancel ReduceOnly orders or Market orders because no margin is reserved for them - if order.ReduceOnly || order.OrderType != Limit { - continue - } - ordersToCancel[addr] = append(ordersToCancel[addr], order) - orderNotional := big.NewInt(0).Abs(hu.Div1e18(hu.Mul(order.GetUnFilledBaseAssetQuantity(), order.Price))) // | size * current price | - marginReleased := hu.Div1e6(hu.Mul(orderNotional, db.configService.getMinAllowableMargin())) - _availableMargin.Add(_availableMargin, marginReleased) - if _availableMargin.Sign() >= 0 { - break - } + // cancel orders until available margin is positive + ordersToCancel[addr] = []Order{} + foundCancellableOrders := false + for _, order := range traderOrders { + // @todo how are reduce only orders that are not fillable cancelled? + if order.ReduceOnly || order.OrderType != Limit { + continue + } + ordersToCancel[addr] = append(ordersToCancel[addr], order) + foundCancellableOrders = true + orderNotional := big.NewInt(0).Abs(hu.Div1e18(hu.Mul(order.GetUnFilledBaseAssetQuantity(), order.Price))) // | size * current price | + marginReleased := hu.Div1e6(hu.Mul(orderNotional, db.configService.getMinAllowableMargin())) + _availableMargin.Add(_availableMargin, marginReleased) + if _availableMargin.Sign() >= 0 { + break } - return true } - return false + return foundCancellableOrders, _availableMargin } func (db *InMemoryDatabase) getTraderOrders(trader common.Address, orderType OrderType) []Order { diff --git a/plugin/evm/orderbook/memory_database_test.go b/plugin/evm/orderbook/memory_database_test.go index 587d289ba9..dbabb997dd 100644 --- a/plugin/evm/orderbook/memory_database_test.go +++ b/plugin/evm/orderbook/memory_database_test.go @@ -433,7 +433,7 @@ func TestGetCancellableOrders(t *testing.T) { availableMargin := getAvailableMargin(_trader, hState) // availableMargin = 40 - 9 - (99 + (10+9+8) * 3)/5 = -5 assert.Equal(t, hu.Mul1e6(big.NewInt(-5)), availableMargin) - _, ordersToCancel := inMemoryDatabase.GetNaughtyTraders(hState) + _, ordersToCancel, _ := inMemoryDatabase.GetNaughtyTraders(hState) // t.Log("####", "ordersToCancel", ordersToCancel) assert.Equal(t, 1, len(ordersToCancel)) // only one trader diff --git a/plugin/evm/orderbook/metrics.go b/plugin/evm/orderbook/metrics.go index d35301fff5..397c64e477 100644 --- a/plugin/evm/orderbook/metrics.go +++ b/plugin/evm/orderbook/metrics.go @@ -18,6 +18,7 @@ var ( orderBookTransactionsFailureTotalCounter = metrics.NewRegisteredCounter("orderbooktxs/total/failure", nil) // panics are recovered but monitored + AllPanicsCounter = metrics.NewRegisteredCounter("all_panics", nil) RunMatchingPipelinePanicsCounter = metrics.NewRegisteredCounter("matching_pipeline_panics", nil) HandleHubbleFeedLogsPanicsCounter = metrics.NewRegisteredCounter("handle_hubble_feed_logs_panics", nil) HandleChainAcceptedLogsPanicsCounter = metrics.NewRegisteredCounter("handle_chain_accepted_logs_panics", nil) @@ -32,4 +33,7 @@ var ( // order id not found while deleting deleteOrderIdNotFoundCounter = metrics.NewRegisteredCounter("delete_order_id_not_found", nil) + + // unquenched liquidations + unquenchedLiquidationsCounter = metrics.NewRegisteredCounter("unquenched_liquidations", nil) ) diff --git a/plugin/evm/orderbook/mocks.go b/plugin/evm/orderbook/mocks.go index 38d74b4327..4a490553d2 100644 --- a/plugin/evm/orderbook/mocks.go +++ b/plugin/evm/orderbook/mocks.go @@ -116,8 +116,8 @@ func (db *MockLimitOrderDatabase) GetLastPrices() map[Market]*big.Int { return map[Market]*big.Int{} } -func (db *MockLimitOrderDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order) { - return []LiquidablePosition{}, map[common.Address][]Order{} +func (db *MockLimitOrderDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) { + return []LiquidablePosition{}, map[common.Address][]Order{}, map[common.Address]*big.Int{} } func (db *MockLimitOrderDatabase) GetOrderBookData() InMemoryDatabase { @@ -287,6 +287,10 @@ func (cs *MockConfigService) GetCollaterals() []hu.Collateral { return []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} } +func (cs *MockConfigService) GetTakerFee() *big.Int { + return big.NewInt(0) +} + func NewMockConfigService() *MockConfigService { return &MockConfigService{} } diff --git a/plugin/evm/orderbook/tx_processor.go b/plugin/evm/orderbook/tx_processor.go index d41d67511e..98f958941d 100644 --- a/plugin/evm/orderbook/tx_processor.go +++ b/plugin/evm/orderbook/tx_processor.go @@ -47,18 +47,20 @@ type ValidatorTxFeeConfig struct { } type limitOrderTxProcessor struct { - txPool *txpool.TxPool - memoryDb LimitOrderDatabase - orderBookABI abi.ABI - clearingHouseABI abi.ABI - marginAccountABI abi.ABI - orderBookContractAddress common.Address - clearingHouseContractAddress common.Address - marginAccountContractAddress common.Address - backend *eth.EthAPIBackend - validatorAddress common.Address - validatorPrivateKey string - validatorTxFeeConfig ValidatorTxFeeConfig + txPool *txpool.TxPool + memoryDb LimitOrderDatabase + orderBookABI abi.ABI + limitOrderBookABI abi.ABI + clearingHouseABI abi.ABI + marginAccountABI abi.ABI + orderBookContractAddress common.Address + limitOrderBookContractAddress common.Address + clearingHouseContractAddress common.Address + marginAccountContractAddress common.Address + backend *eth.EthAPIBackend + validatorAddress common.Address + validatorPrivateKey string + validatorTxFeeConfig ValidatorTxFeeConfig } func NewLimitOrderTxProcessor(txPool *txpool.TxPool, memoryDb LimitOrderDatabase, backend *eth.EthAPIBackend, validatorPrivateKey string) LimitOrderTxProcessor { @@ -67,6 +69,11 @@ func NewLimitOrderTxProcessor(txPool *txpool.TxPool, memoryDb LimitOrderDatabase panic(err) } + limitOrderBookABI, err := abi.FromSolidityJson(string(abis.LimitOrderBookAbi)) + if err != nil { + panic(err) + } + clearingHouseABI, err := abi.FromSolidityJson(string(abis.ClearingHouseAbi)) if err != nil { panic(err) @@ -83,18 +90,20 @@ func NewLimitOrderTxProcessor(txPool *txpool.TxPool, memoryDb LimitOrderDatabase } lotp := &limitOrderTxProcessor{ - txPool: txPool, - orderBookABI: orderBookABI, - clearingHouseABI: clearingHouseABI, - marginAccountABI: marginAccountABI, - memoryDb: memoryDb, - orderBookContractAddress: OrderBookContractAddress, - clearingHouseContractAddress: ClearingHouseContractAddress, - marginAccountContractAddress: MarginAccountContractAddress, - backend: backend, - validatorAddress: validatorAddress, - validatorPrivateKey: validatorPrivateKey, - validatorTxFeeConfig: ValidatorTxFeeConfig{baseFeeEstimate: big.NewInt(0), blockNumber: 0}, + txPool: txPool, + limitOrderBookABI: limitOrderBookABI, + orderBookABI: orderBookABI, + clearingHouseABI: clearingHouseABI, + marginAccountABI: marginAccountABI, + memoryDb: memoryDb, + limitOrderBookContractAddress: LimitOrderBookContractAddress, + orderBookContractAddress: OrderBookContractAddress, + clearingHouseContractAddress: ClearingHouseContractAddress, + marginAccountContractAddress: MarginAccountContractAddress, + backend: backend, + validatorAddress: validatorAddress, + validatorPrivateKey: validatorPrivateKey, + validatorTxFeeConfig: ValidatorTxFeeConfig{baseFeeEstimate: big.NewInt(0), blockNumber: 0}, } return lotp } @@ -147,7 +156,7 @@ func (lotp *limitOrderTxProcessor) ExecuteMatchedOrdersTx(longOrder Order, short } func (lotp *limitOrderTxProcessor) ExecuteLimitOrderCancel(orders []LimitOrder) error { - txHash, err := lotp.executeLocalTx(lotp.orderBookContractAddress, lotp.orderBookABI, "cancelOrdersWithLowMargin", orders) + txHash, err := lotp.executeLocalTx(lotp.limitOrderBookContractAddress, lotp.limitOrderBookABI, "cancelOrdersWithLowMargin", orders) log.Info("ExecuteLimitOrderCancel", "orders", orders, "txHash", txHash.String(), "err", err) return err } diff --git a/utils/string.go b/utils/string.go index dc0ee92f22..ef00402eff 100644 --- a/utils/string.go +++ b/utils/string.go @@ -1,5 +1,10 @@ package utils +import ( + "strings" + "unicode" +) + func ContainsString(list []string, item string) bool { for _, i := range list { if i == item { @@ -8,3 +13,13 @@ func ContainsString(list []string, item string) bool { } return false } + +func RemoveSpacesAndSpecialChars(str string) string { + var builder strings.Builder + for _, r := range str { + if unicode.IsLetter(r) || unicode.IsNumber(r) { + builder.WriteRune(r) + } + } + return builder.String() +}