From 680a2096400e2565973db5f5cf4a9802517395d5 Mon Sep 17 00:00:00 2001 From: ze97286 Date: Thu, 18 Jul 2024 09:18:18 +0100 Subject: [PATCH 01/12] fix: fail checkTx downstream for delayed transactions so they don't get included in more than one block --- CHANGELOG.md | 2 +- core/processor/abci.go | 7 +++++++ core/txcache/cache.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7fee922703..07f6a415804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ - [11453](https://github.com/vegaprotocol/vega/issues/11453) - Fix margin for fully collateralised markets. - [11462](https://github.com/vegaprotocol/vega/issues/11462) - Update the time weighted notional when publishing live game scores. - +- [11474](https://github.com/vegaprotocol/vega/issues/11474) - Fail `checkTx` downstream for delayed transactions so they don't get included in more than one block. ## 0.77.0 diff --git a/core/processor/abci.go b/core/processor/abci.go index 5e3b598ea0d..7a6cadcb42e 100644 --- a/core/processor/abci.go +++ b/core/processor/abci.go @@ -198,6 +198,7 @@ type TxCache interface { NewDelayedTransaction(ctx context.Context, delayed [][]byte, height uint64) []byte IsDelayRequired(marketID string) bool IsDelayRequiredAnyMarket() bool + IsTxInCache(tx []byte) bool } type App struct { @@ -1577,6 +1578,12 @@ func (app *App) removeOldCheckpoints() error { func (app *App) OnCheckTxSpam(tx abci.Tx) tmtypes.ResponseCheckTx { resp := tmtypes.ResponseCheckTx{} + if app.txCache.IsTxInCache(tx.Hash()) { + resp.Code = blockchain.AbciSpamError + resp.Data = []byte("delayed transaction already included in a block") + return resp + } + // verify proof of work and replay if !app.nilPow { if err := app.pow.CheckTx(tx); err != nil { diff --git a/core/txcache/cache.go b/core/txcache/cache.go index 179e79e3fd7..8c99eb7f35f 100644 --- a/core/txcache/cache.go +++ b/core/txcache/cache.go @@ -17,8 +17,10 @@ package txcache import ( "context" + "encoding/hex" "fmt" "sort" + "sync" "code.vegaprotocol.io/vega/core/nodewallets" "code.vegaprotocol.io/vega/core/txn" @@ -27,6 +29,8 @@ import ( "code.vegaprotocol.io/vega/libs/proto" commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" + + "github.com/cometbft/cometbft/crypto/tmhash" ) func NewTxCache(commander *nodewallets.Commander) *TxCache { @@ -34,6 +38,7 @@ func NewTxCache(commander *nodewallets.Commander) *TxCache { commander: commander, marketToDelayRequired: map[string]bool{}, heightToTxs: map[uint64][][]byte{}, + cachedTxs: map[string]struct{}{}, } } @@ -44,6 +49,34 @@ type TxCache struct { numBlocksToDelay uint64 // no need to include is snapshot - is updated when markets are created/updated/loaded from snapshot marketToDelayRequired map[string]bool + // map of transactions that have been picked up for delay + cachedTxs map[string]struct{} + lock sync.RWMutex +} + +func (t *TxCache) IsTxInCache(txHash []byte) bool { + t.lock.RLock() + defer t.lock.RUnlock() + _, ok := t.cachedTxs[hex.EncodeToString(txHash)] + return ok +} + +func (t *TxCache) removeHeightFromCache(height uint64) { + t.lock.Lock() + defer t.lock.Unlock() + if txs, ok := t.heightToTxs[height]; ok { + for _, tx := range txs { + delete(t.cachedTxs, hex.EncodeToString(tmhash.Sum(tx))) + } + } +} + +func (t *TxCache) addHeightToCache(txs [][]byte) { + t.lock.Lock() + defer t.lock.Unlock() + for _, tx := range txs { + t.cachedTxs[hex.EncodeToString(tmhash.Sum(tx))] = struct{}{} + } } // MarketDelayRequiredUpdated is called when the market configuration is created/updated with support for @@ -84,9 +117,11 @@ func (t *TxCache) NewDelayedTransaction(ctx context.Context, delayed [][]byte, c func (t *TxCache) SetRawTxs(rtx [][]byte, height uint64) { if rtx == nil { + t.removeHeightFromCache(height) delete(t.heightToTxs, height) } else { t.heightToTxs[height] = rtx + t.addHeightToCache(rtx) } } @@ -139,6 +174,7 @@ func (t *TxCache) LoadState(_ context.Context, p *types.Payload) ([]types.StateP t.heightToTxs = map[uint64][][]byte{} for _, tx := range data.TxCache.Txs { t.heightToTxs[tx.Height] = tx.Tx + t.addHeightToCache(tx.Tx) } return nil, nil default: From 310e16c1643a918c9a0f897e334a310f817adf92 Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Fri, 19 Jul 2024 14:32:44 +0100 Subject: [PATCH 02/12] fix: separate on-book and off-book volume in auction uncrossing calcs to ensure exact order extraction --- core/matching/cached_orderbook.go | 2 +- core/matching/can_uncross_test.go | 4 +- core/matching/indicative_price_and_volume.go | 86 +++++++++++++++----- core/matching/orderbook.go | 33 ++++---- 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/core/matching/cached_orderbook.go b/core/matching/cached_orderbook.go index c8a0dc5dddf..8707e974864 100644 --- a/core/matching/cached_orderbook.go +++ b/core/matching/cached_orderbook.go @@ -200,7 +200,7 @@ func (b *CachedOrderBook) GetIndicativePriceAndVolume() (*num.Uint, uint64, type volume, cachedVolOk := b.cache.GetIndicativeVolume() side, cachedSideOk := b.cache.GetIndicativeUncrossingSide() if !cachedPriceOk || !cachedVolOk || !cachedSideOk { - price, volume, side = b.OrderBook.GetIndicativePriceAndVolume() + price, volume, side, _ = b.OrderBook.GetIndicativePriceAndVolume() b.cache.SetIndicativePrice(price.Clone()) b.cache.SetIndicativeVolume(volume) b.cache.SetIndicativeUncrossingSide(side) diff --git a/core/matching/can_uncross_test.go b/core/matching/can_uncross_test.go index 6449aac813b..f07d434b293 100644 --- a/core/matching/can_uncross_test.go +++ b/core/matching/can_uncross_test.go @@ -80,7 +80,7 @@ func TestBidAndAskPresentAfterAuction(t *testing.T) { assert.NoError(t, err) } - indicativePrice, indicativeVolume, indicativeSide := book.GetIndicativePriceAndVolume() + indicativePrice, indicativeVolume, indicativeSide, _ := book.GetIndicativePriceAndVolume() assert.Equal(t, indicativePrice.Uint64(), uint64(1975)) assert.Equal(t, int(indicativeVolume), 5) assert.Equal(t, indicativeSide, types.SideBuy) @@ -143,7 +143,7 @@ func TestBidAndAskPresentAfterAuctionInverse(t *testing.T) { assert.NoError(t, err) } - indicativePrice, indicativeVolume, indicativeSide := book.GetIndicativePriceAndVolume() + indicativePrice, indicativeVolume, indicativeSide, _ := book.GetIndicativePriceAndVolume() assert.Equal(t, indicativePrice.Uint64(), uint64(1950)) assert.Equal(t, int(indicativeVolume), 5) assert.Equal(t, indicativeSide, types.SideBuy) diff --git a/core/matching/indicative_price_and_volume.go b/core/matching/indicative_price_and_volume.go index 6bba28205a3..3483af832fc 100644 --- a/core/matching/indicative_price_and_volume.go +++ b/core/matching/indicative_price_and_volume.go @@ -53,7 +53,8 @@ type ipvPriceLevel struct { } type ipvVolume struct { - volume uint64 + volume uint64 + offbookVolume uint64 // how much of the above total volume is coming from AMMs } type ipvGeneratedOffbook struct { @@ -131,13 +132,15 @@ func (ipv *IndicativePriceAndVolume) buildInitialOffbookShape(offbook OffbookSou // expand all AMM's into orders within the crossed region and add them to the price-level cache buys, sells := offbook.OrderbookShape(min, max, nil) - for _, o := range buys { + for i := len(buys) - 1; i >= 0; i-- { + o := buys[i] mpl, ok := mplm[*o.Price] if !ok { - mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0}, sellpl: ipvVolume{0}} + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} } // increment the volume at this level mpl.buypl.volume += o.Size + mpl.buypl.offbookVolume += o.Size mplm[*o.Price] = mpl if ipv.generated[o.Party] == nil { @@ -149,10 +152,11 @@ func (ipv *IndicativePriceAndVolume) buildInitialOffbookShape(offbook OffbookSou for _, o := range sells { mpl, ok := mplm[*o.Price] if !ok { - mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0}, sellpl: ipvVolume{0}} + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} } mpl.sellpl.volume += o.Size + mpl.sellpl.offbookVolume += o.Size mplm[*o.Price] = mpl if ipv.generated[o.Party] == nil { @@ -170,10 +174,10 @@ func (ipv *IndicativePriceAndVolume) removeOffbookShape(party string) { // remove all the old volume for the AMM's for _, o := range orders.buy { - ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side) + ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side, true) } for _, o := range orders.sell { - ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side) + ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side, true) } // clear it out the saved generated orders for the offbook shape @@ -184,8 +188,10 @@ func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, ma // recalculate new orders for the shape and add the volume in buys, sells := ipv.offbook.OrderbookShape(minPrice, maxPrice, party) - for _, o := range buys { - ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side) + // add buys backwards so that the best-bid is first + for i := len(buys) - 1; i >= 0; i-- { + o := buys[i] + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) if ipv.generated[o.Party] == nil { ipv.generated[o.Party] = &ipvGeneratedOffbook{} @@ -193,8 +199,9 @@ func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, ma ipv.generated[o.Party].add(o) } + // add buys fowards so that the best-ask is first for _, o := range sells { - ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side) + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) if ipv.generated[o.Party] == nil { ipv.generated[o.Party] = &ipvGeneratedOffbook{} @@ -223,7 +230,7 @@ func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *Ord mplm := map[num.Uint]ipvPriceLevel{} for i := len(buy.levels) - 1; i >= 0; i-- { - mplm[*buy.levels[i].price] = ipvPriceLevel{price: buy.levels[i].price.Clone(), buypl: ipvVolume{buy.levels[i].volume}, sellpl: ipvVolume{0}} + mplm[*buy.levels[i].price] = ipvPriceLevel{price: buy.levels[i].price.Clone(), buypl: ipvVolume{buy.levels[i].volume, 0}, sellpl: ipvVolume{0, 0}} } // now we add all the sells @@ -232,10 +239,10 @@ func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *Ord for i := len(sell.levels) - 1; i >= 0; i-- { price := sell.levels[i].price.Clone() if mpl, ok := mplm[*price]; ok { - mpl.sellpl = ipvVolume{sell.levels[i].volume} + mpl.sellpl = ipvVolume{sell.levels[i].volume, 0} mplm[*price] = mpl } else { - mplm[*price] = ipvPriceLevel{price: price, sellpl: ipvVolume{sell.levels[i].volume}, buypl: ipvVolume{0}} + mplm[*price] = ipvPriceLevel{price: price, sellpl: ipvVolume{sell.levels[i].volume, 0}, buypl: ipvVolume{0, 0}} } } @@ -254,16 +261,22 @@ func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *Ord sort.Slice(ipv.levels, func(i, j int) bool { return ipv.levels[i].price.GT(ipv.levels[j].price) }) } -func (ipv *IndicativePriceAndVolume) incrementLevelVolume(idx int, volume uint64, side types.Side) { +func (ipv *IndicativePriceAndVolume) incrementLevelVolume(idx int, volume uint64, side types.Side, isOffbook bool) { switch side { case types.SideBuy: ipv.levels[idx].buypl.volume += volume + if isOffbook { + ipv.levels[idx].buypl.offbookVolume += volume + } case types.SideSell: ipv.levels[idx].sellpl.volume += volume + if isOffbook { + ipv.levels[idx].sellpl.offbookVolume += volume + } } } -func (ipv *IndicativePriceAndVolume) AddVolumeAtPrice(price *num.Uint, volume uint64, side types.Side) { +func (ipv *IndicativePriceAndVolume) AddVolumeAtPrice(price *num.Uint, volume uint64, side types.Side, isOffbook bool) { if price.GTE(ipv.lastMinPrice) || price.LTE(ipv.lastMaxPrice) { // the new price added is in the range, that will require // to recompute the whole range when we call GetCumulativePriceLevels @@ -275,25 +288,31 @@ func (ipv *IndicativePriceAndVolume) AddVolumeAtPrice(price *num.Uint, volume ui }) if i < len(ipv.levels) && ipv.levels[i].price.EQ(price) { // we found the price level, let's add the volume there, and we are done - ipv.incrementLevelVolume(i, volume, side) + ipv.incrementLevelVolume(i, volume, side, isOffbook) } else { ipv.levels = append(ipv.levels, ipvPriceLevel{}) copy(ipv.levels[i+1:], ipv.levels[i:]) ipv.levels[i] = ipvPriceLevel{price: price.Clone()} - ipv.incrementLevelVolume(i, volume, side) + ipv.incrementLevelVolume(i, volume, side, isOffbook) } } -func (ipv *IndicativePriceAndVolume) decrementLevelVolume(idx int, volume uint64, side types.Side) { +func (ipv *IndicativePriceAndVolume) decrementLevelVolume(idx int, volume uint64, side types.Side, isOffbook bool) { switch side { case types.SideBuy: ipv.levels[idx].buypl.volume -= volume + if isOffbook { + ipv.levels[idx].buypl.offbookVolume -= volume + } case types.SideSell: ipv.levels[idx].sellpl.volume -= volume + if isOffbook { + ipv.levels[idx].sellpl.offbookVolume -= volume + } } } -func (ipv *IndicativePriceAndVolume) RemoveVolumeAtPrice(price *num.Uint, volume uint64, side types.Side) { +func (ipv *IndicativePriceAndVolume) RemoveVolumeAtPrice(price *num.Uint, volume uint64, side types.Side, isOffbook bool) { if price.GTE(ipv.lastMinPrice) || price.LTE(ipv.lastMaxPrice) { // the new price added is in the range, that will require // to recompute the whole range when we call GetCumulativePriceLevels @@ -305,7 +324,7 @@ func (ipv *IndicativePriceAndVolume) RemoveVolumeAtPrice(price *num.Uint, volume }) if i < len(ipv.levels) && ipv.levels[i].price.EQ(price) { // we found the price level, let's add the volume there, and we are done - ipv.decrementLevelVolume(i, volume, side) + ipv.decrementLevelVolume(i, volume, side, isOffbook) } else { ipv.log.Panic("cannot remove volume from a non-existing level", logging.String("side", side.String()), @@ -379,6 +398,7 @@ func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice var ( cumulativeVolumeSell, cumulativeVolumeBuy, maxTradable uint64 + cumulativeOffbookSell, cumulativeOffbookBuy uint64 // here the caching buf is already allocated, we can just resize it // based on the required length cumulativeVolumes = ipv.buf[:len(rangedLevels)] @@ -404,18 +424,23 @@ func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice // if we had a price level in the buy side, use it if rangedLevels[j].buypl.volume > 0 { cumulativeVolumeBuy += rangedLevels[j].buypl.volume + cumulativeOffbookBuy += rangedLevels[j].buypl.offbookVolume cumulativeVolumes[j].bidVolume = rangedLevels[j].buypl.volume } // same same if rangedLevels[i].sellpl.volume > 0 { cumulativeVolumeSell += rangedLevels[i].sellpl.volume + cumulativeOffbookSell += rangedLevels[i].sellpl.offbookVolume cumulativeVolumes[i].askVolume = rangedLevels[i].sellpl.volume } // this will always erase the previous values cumulativeVolumes[j].cumulativeBidVolume = cumulativeVolumeBuy + cumulativeVolumes[j].cumulativeBidOffbook = cumulativeOffbookBuy + cumulativeVolumes[i].cumulativeAskVolume = cumulativeVolumeSell + cumulativeVolumes[i].cumulativeAskOffbook = cumulativeOffbookSell // we just do that // price | sell | buy | vol | ibuy | isell @@ -448,7 +473,11 @@ func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice // ExtractOffbookOrders returns the cached expanded orders of AMM's in the crossed region of the given side. These // are the order that we will send in aggressively to uncrossed the book. -func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side types.Side) ([]*types.Order, uint64) { +func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side types.Side, target uint64) []*types.Order { + if target == 0 { + return []*types.Order{} + } + var volume uint64 orders := []*types.Order{} // the ipv keeps track of all the expand AMM orders in the crossed region @@ -469,7 +498,22 @@ func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side } orders = append(orders, o) volume += o.Size + + // if we're extracted enough we can stop now + if volume == target { + return orders + } } } - return orders, volume + + if volume != target { + ipv.log.Panic("Failed to extract AMM orders for uncrossing", + logging.BigUint("price", price), + logging.Uint64("volume", volume), + logging.Uint64("extracted-volume", volume), + logging.Uint64("target-volume", target), + ) + } + + return orders } diff --git a/core/matching/orderbook.go b/core/matching/orderbook.go index 0aa42e06788..694304015a0 100644 --- a/core/matching/orderbook.go +++ b/core/matching/orderbook.go @@ -86,6 +86,10 @@ type CumulativeVolumeLevel struct { cumulativeBidVolume uint64 cumulativeAskVolume uint64 maxTradableAmount uint64 + + // keep track of how much of the cumulative volume is from AMMs + cumulativeBidOffbook uint64 + cumulativeAskOffbook uint64 } func (b *OrderBook) Hash() []byte { @@ -345,7 +349,7 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { if buyMatch && sellMatch { return true } - _, v, _ := b.GetIndicativePriceAndVolume() + _, v, _, _ := b.GetIndicativePriceAndVolume() // no buy orders remaining on the book after uncrossing, it buyMatches exactly vol := uint64(0) if !buyMatch { @@ -394,14 +398,14 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { } // GetIndicativePriceAndVolume Calculates the indicative price and volume of the order book without modifying the order book state. -func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol uint64, retside types.Side) { +func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol uint64, retside types.Side, offbookVolume uint64) { // Generate a set of price level pairs with their maximum tradable volumes cumulativeVolumes, maxTradableAmount, err := b.buildCumulativePriceLevels() if err != nil { if b.log.GetLevel() <= logging.DebugLevel { b.log.Debug("could not get cumulative price levels", logging.Error(err)) } - return num.UintZero(), 0, types.SideUnspecified + return num.UintZero(), 0, types.SideUnspecified, 0 } // Pull out all prices that match that volume @@ -432,14 +436,17 @@ func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol ui if ordersToFill == 0 { // Buys fill exactly, uncross from the buy side uncrossSide = types.SideBuy + offbookVolume = value.cumulativeBidOffbook break } else if ordersToFill < 0 { // Buys are not exact, uncross from the sell side uncrossSide = types.SideSell + offbookVolume = value.cumulativeAskOffbook break } } - return uncrossPrice, maxTradableAmount, uncrossSide + + return uncrossPrice, maxTradableAmount, uncrossSide, offbookVolume } // GetIndicativePrice Calculates the indicative price of the order book without modifying the order book state. @@ -474,7 +481,7 @@ func (b *OrderBook) GetIndicativePrice() (retprice *num.Uint) { func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { // Get the uncrossing price and which side has the most volume at that price - price, volume, uncrossSide := b.GetIndicativePriceAndVolume() + price, volume, uncrossSide, offbookVolume := b.GetIndicativePriceAndVolume() // If we have no uncrossing price, we have nothing to do if price.IsZero() && volume == 0 { @@ -497,8 +504,7 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { } // extract uncrossing orders from all AMMs - var offbookVolume uint64 - uncrossOrders, offbookVolume = b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide) + uncrossOrders = b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide, offbookVolume) // the remaining volume should now come from the orderbook volume -= offbookVolume @@ -545,7 +551,7 @@ func (b *OrderBook) buildCumulativePriceLevels() ([]CumulativeVolumeLevel, uint6 // if removeOrders is set to true then matched orders get removed from the book. func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) { // Get the uncrossing price and which side has the most volume at that price - price, volume, uncrossSide := b.GetIndicativePriceAndVolume() + price, volume, uncrossSide, offbookVolume := b.GetIndicativePriceAndVolume() // If we have no uncrossing price, we have nothing to do if price.IsZero() && volume == 0 { @@ -567,8 +573,7 @@ func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) { } // extract uncrossing orders from all AMMs - var offbookVolume uint64 - uncrossOrders, offbookVolume := b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide) + uncrossOrders := b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide, offbookVolume) // the remaining volume should now come from the orderbook volume -= offbookVolume @@ -851,12 +856,12 @@ func (b *OrderBook) AmendOrder(originalOrder, amendedOrder *types.Order) error { if volumeChange < 0 { b.indicativePriceAndVolume.RemoveVolumeAtPrice( - amendedOrder.Price, uint64(-volumeChange), amendedOrder.Side) + amendedOrder.Price, uint64(-volumeChange), amendedOrder.Side, false) } if volumeChange > 0 { b.indicativePriceAndVolume.AddVolumeAtPrice( - amendedOrder.Price, uint64(volumeChange), amendedOrder.Side) + amendedOrder.Price, uint64(volumeChange), amendedOrder.Side, false) } return nil @@ -1008,7 +1013,7 @@ func (b *OrderBook) SubmitOrder(order *types.Order) (*types.OrderConfirmation, e // also add it to the indicative price and volume if in auction if b.auction { b.indicativePriceAndVolume.AddVolumeAtPrice( - order.Price, order.TrueRemaining(), order.Side) + order.Price, order.TrueRemaining(), order.Side, false) } } @@ -1095,7 +1100,7 @@ func (b *OrderBook) DeleteOrder( // cancel the order if it expires it if b.auction { b.indicativePriceAndVolume.RemoveVolumeAtPrice( - dorder.Price, dorder.TrueRemaining(), dorder.Side) + dorder.Price, dorder.TrueRemaining(), dorder.Side, false) } return dorder, err } From 2752acc0c3b1b05bd8f3fe69bf601644c5bd6e0f Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Fri, 19 Jul 2024 17:01:07 +0100 Subject: [PATCH 03/12] chore: add feature test for stagnet panic for AMM uncrossing --- .../features/amm/0090-VAMM-auction.feature | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/core/integration/features/amm/0090-VAMM-auction.feature b/core/integration/features/amm/0090-VAMM-auction.feature index 42cddc9a64e..4c7e0590e03 100644 --- a/core/integration/features/amm/0090-VAMM-auction.feature +++ b/core/integration/features/amm/0090-VAMM-auction.feature @@ -367,4 +367,30 @@ Feature: vAMM rebasing when created or amended Then the network moves ahead "1" blocks And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | best bid price | best offer price | - | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | \ No newline at end of file + | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | + + Scenario: Stagnet auction uncrossing panic where uncrossing side has both AMM and orderbook volume at the same level but we only need the orderbook volume + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | lp1 | ETH/MAR22 | sell | 10 | 99 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp1 | ETH/MAR22 | sell | 160 | 95 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp2 | ETH/MAR22 | buy | 2 | 98 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | best bid price | best offer price | + | TRADING_MODE_OPENING_AUCTION | 96 | 160 | 99 | 95 | + + + When the opening auction period ends for market "ETH/MAR22" + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 96 | TRADING_MODE_CONTINUOUS | 96 | 98 | \ No newline at end of file From 38c1ae74bd0cfa96a2e97184008e0fa3327f772d Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Mon, 22 Jul 2024 17:02:24 +0100 Subject: [PATCH 04/12] fix: handle point ranges in AMM matching --- core/execution/amm/engine.go | 18 ++++++++ core/execution/amm/pool.go | 4 +- .../features/amm/0090-VAMM-auction.feature | 43 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/core/execution/amm/engine.go b/core/execution/amm/engine.go index 0e7daba27db..96865f93646 100644 --- a/core/execution/amm/engine.go +++ b/core/execution/amm/engine.go @@ -130,6 +130,7 @@ type Engine struct { // gets us from the price in the submission -> price in full asset dp priceFactor num.Decimal positionFactor num.Decimal + oneTick *num.Uint // map of party -> pool pools map[string]*Pool @@ -157,6 +158,7 @@ func New( marketActivityTracker *common.MarketActivityTracker, parties common.Parties, ) *Engine { + oneTick, _ := num.UintFromDecimal(priceFactor) return &Engine{ log: log, broker: broker, @@ -173,6 +175,7 @@ func New( priceFactor: priceFactor, positionFactor: positionFactor, parties: parties, + oneTick: oneTick, } } @@ -493,6 +496,21 @@ func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, [ inner, outer = outer, inner } + // if inner and outer are equal then we are wanting to trade with AMMs *only at* this given price + // this can happen quite easily during auction uncrossing where two AMMs have bases offset by 2 + // and the crossed region is simply a point and not an interval. To be able to query the tradable + // volume of an AMM at a point, we need to first convert it to an interval by stepping one tick away first. + // This is because to get the BUY volume an AMM has at price P, we need to calculate the difference + // in its position between prices P -> P + 1. For SELL volume its the other way around and we + // need the difference in position from P - 1 -> P. + if inner.EQ(outer) { + if agg.Side == types.SideSell { + outer = num.UintZero().Add(outer, e.oneTick) + } else { + inner = num.UintZero().Sub(inner, e.oneTick) + } + } + if inner != nil { bounds[inner.String()] = inner.Clone() } diff --git a/core/execution/amm/pool.go b/core/execution/amm/pool.go index fb2f130b95c..0f8d7be181f 100644 --- a/core/execution/amm/pool.go +++ b/core/execution/amm/pool.go @@ -128,7 +128,7 @@ func NewPool( positionFactor num.Decimal, maxCalculationLevels *num.Uint, ) (*Pool, error) { - oneTick, _ := num.UintFromDecimal(num.DecimalOne().Mul(priceFactor)) + oneTick, _ := num.UintFromDecimal(priceFactor) pool := &Pool{ log: log, ID: id, @@ -164,7 +164,7 @@ func NewPoolFromProto( party string, priceFactor num.Decimal, ) (*Pool, error) { - oneTick, _ := num.UintFromDecimal(num.DecimalOne().Mul(priceFactor)) + oneTick, _ := num.UintFromDecimal(priceFactor) var lowerLeverage, upperLeverage *num.Decimal if state.Parameters.LeverageAtLowerBound != nil { diff --git a/core/integration/features/amm/0090-VAMM-auction.feature b/core/integration/features/amm/0090-VAMM-auction.feature index 4c7e0590e03..05bf409062e 100644 --- a/core/integration/features/amm/0090-VAMM-auction.feature +++ b/core/integration/features/amm/0090-VAMM-auction.feature @@ -106,6 +106,49 @@ Feature: vAMM rebasing when created or amended | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | + @VAMM + Scenario: two AMM's that cross at a single point i.e no overlap + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 95 | 105 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 95 | 105 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 102 | 97 | 107 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 102 | 97 | 107 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 101 | 92 | + + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm2-id | 101 | 92 | vamm1-id | true | + + Then the network moves ahead "1" blocks + + # two AMMs are now prices at ~100 which is between their base values + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 101 | TRADING_MODE_CONTINUOUS | 100 | 102 | + @VAMM Scenario: AMM crossed with SELL orders From 49384bad735b1499b8f2ececddf5d697de8e3439 Mon Sep 17 00:00:00 2001 From: Charlie Date: Thu, 18 Jul 2024 19:49:29 +0100 Subject: [PATCH 05/12] test: example of fcap liquidation --- .../verified/fcap_liquidations.feature | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 core/integration/features/verified/fcap_liquidations.feature diff --git a/core/integration/features/verified/fcap_liquidations.feature b/core/integration/features/verified/fcap_liquidations.feature new file mode 100644 index 00000000000..9ffbf25e187 --- /dev/null +++ b/core/integration/features/verified/fcap_liquidations.feature @@ -0,0 +1,75 @@ +Feature: FCAP liquidations + + vega-market-sim fuzz testing shows parties being liquidated after + opening positions with market orders. + + Test replicates behaviour and shows network liquidating party. + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0 | + | market.fee.factors.infrastructureFee | 0 | + | network.markPriceUpdateMaximumFrequency | 0s | + + And the following assets are registered: + | id | decimal places | quantum | + | USD-1-10 | 0 | 1 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | lp | USD-1-10 | 100000000000 | + | aux1 | USD-1-10 | 100000000000 | + | aux2 | USD-1-10 | 100000000000 | + + # Setup the FCAP market in continuous trading + Given the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | max price cap | binary | fully collateralised | + | FCAP/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | 100 | true | true | + And the parties submit the following liquidity provision: + | id | party | market id | commitment amount | fee | lp type | + | lp1 | lp | FCAP/USD-1-10 | 1000000 | 0 | submission | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | FCAP/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + When the opening auction period ends for market "FCAP/USD-1-10" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP/USD-1-10" + + + Scenario: Party opens a short position with a market order and is liquidiated at the next mark to market. + + And the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 5s | + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | trader | USD-1-10 | 50 | + + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | + | trader | FCAP/USD-1-10 | buy | 1 | 50 | 1 | TYPE_MARKET | TIF_FOK | + When the network moves ahead "1" blocks + And the following trades should be executed: + | buyer | price | size | seller | + | trader | 60 | 1 | aux1 | + And the parties should have the following account balances: + | party | asset | market id | margin | general | + | trader | USD-1-10 | FCAP/USD-1-10 | 50 | 0 | + + When the network moves ahead "5" blocks + And the following trades should be executed: + | buyer | price | size | seller | + | network | 60 | 1 | trader | + Then the parties should have the following account balances: + | party | asset | market id | margin | general | + | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 0 | + + + From 2bec4fef1eca11fe647e56c4dba42ea73e75a52c Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 19 Jul 2024 10:43:56 +0100 Subject: [PATCH 06/12] feat: FCAP liquidations and fees --- .../features/verified/auction_bug.feature | 84 ++++++++++++++++ .../verified/fcap_liquidations.feature | 60 +++++++++++- .../fcap_liquidations_and_fees.feature | 98 +++++++++++++++++++ .../features/verified/overflow.feature | 84 ++++++++++++++++ .../features/verified/rewards.feature | 91 +++++++++++++++++ 5 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 core/integration/features/verified/auction_bug.feature create mode 100644 core/integration/features/verified/fcap_liquidations_and_fees.feature create mode 100644 core/integration/features/verified/overflow.feature create mode 100644 core/integration/features/verified/rewards.feature diff --git a/core/integration/features/verified/auction_bug.feature b/core/integration/features/verified/auction_bug.feature new file mode 100644 index 00000000000..7318d1c7365 --- /dev/null +++ b/core/integration/features/verified/auction_bug.feature @@ -0,0 +1,84 @@ +Feature: Indicative price within bounds but mark price outside bounds + + Scenario: + + + Given the average block duration is "1" + + And the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 0s | + + And the following assets are registered: + | id | decimal places | quantum | + | USDT.0.1 | 0 | 1 | + + Given the price monitoring named "pm": + | horizon | probability | auction extension | + | 60 | 0.999999999 | 5 | + | 60 | 0.999999999 | 5 | + | 120 | 0.999999999 | 10 | + | 120 | 0.999999999 | 10 | + + + + And the composite price oracles from "0xCAFECAFE1": + | name | price property | price type | price decimals | + | oracle1 | price.USD.value | TYPE_INTEGER | 0 | + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | price type | decay weight | decay power | cash amount | source weights | source staleness tolerance | oracle1 | + | ETH/USDT | USDT | USDT.0.1 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | pm | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | weight | 1 | 1 | 0 | 0,0,1,0 | 1m0s,1m0s,1m0s,1m0s | oracle1 | + + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | aux1 | USDT.0.1 | 10000000 | + | aux2 | USDT.0.1 | 10000000 | + | party1 | USDT.0.1 | 10000000 | + | party2 | USDT.0.1 | 10000000 | + + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | ETH/USDT | buy | 100 | 999 | 0 | TYPE_LIMIT | TIF_GTC | + | aux1 | ETH/USDT | buy | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | ETH/USDT | sell | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | ETH/USDT | sell | 100 | 1001 | 0 | TYPE_LIMIT | TIF_GTC | + Then the opening auction period ends for market "ETH/USDT" + And the market data for the market "ETH/USDT" should be: + | mark price | trading mode | horizon | min bound | max bound | + | 1000 | TRADING_MODE_CONTINUOUS | 60 | 984 | 1016 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | + + Then the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | price.USD.value | 800 | 0s | + + + Given the network moves ahead "1" blocks + # And the parties place the following orders: + # | party | market id | side | volume | price | resulting trades | type | tif | + # | party1 | ETH/USDT | buy | 1 | 1001 | 0 | TYPE_LIMIT | TIF_GTC | + And the market data for the market "ETH/USDT" should be: + | mark price | trading mode | horizon | min bound | max bound | indicative price | timestamp | auction start | auction end | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 60 | 984 | 1016 | 0 | 1575072003000000000 | 1575072002000000000 | 1575072007000000000 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | 0 | 1575072003000000000 | 1575072002000000000 | 1575072007000000000 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | 0 | 1575072003000000000 | 1575072002000000000 | 1575072007000000000 | + + # Advance 5 seconds to end of auction + Given the network moves ahead "5" blocks + # Auction ends at indicative price, trades excecuted + # And the following trades should be executed: + # | buyer | price | size | seller | + # | party1 | 1001 | 1 | aux2 | + # Market instantly reenters auction as latest price from oracle is outside bounds + And the market data for the market "ETH/USDT" should be: + | mark price | trading mode | horizon | min bound | max bound | indicative price | timestamp | auction start | auction end | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 60 | 984 | 1016 | 0 | 1575072008000000000 | 1575072008000000000 | 1575072013000000000 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | 0 | 1575072008000000000 | 1575072008000000000 | 1575072013000000000 | + | 1000 | TRADING_MODE_MONITORING_AUCTION | 120 | 977 | 1024 | 0 | 1575072008000000000 | 1575072008000000000 | 1575072013000000000 | + + + + + diff --git a/core/integration/features/verified/fcap_liquidations.feature b/core/integration/features/verified/fcap_liquidations.feature index 9ffbf25e187..8e982562748 100644 --- a/core/integration/features/verified/fcap_liquidations.feature +++ b/core/integration/features/verified/fcap_liquidations.feature @@ -27,9 +27,13 @@ Feature: FCAP liquidations | aux2 | USD-1-10 | 100000000000 | # Setup the FCAP market in continuous trading + Given the price monitoring named "price-monitoring": + | horizon | probability | auction extension | + | 3600 | 0.99 | 5 | Given the markets: - | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | max price cap | binary | fully collateralised | - | FCAP/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | 100 | true | true | + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | max price cap | binary | fully collateralised | + | FCAP/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | 100 | true | true | + | FCAP-PM/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | price-monitoring | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | 100 | true | true | And the parties submit the following liquidity provision: | id | party | market id | commitment amount | fee | lp type | | lp1 | lp | FCAP/USD-1-10 | 1000000 | 0 | submission | @@ -37,8 +41,17 @@ Feature: FCAP liquidations | party | market id | side | volume | price | resulting trades | type | tif | | aux1 | FCAP/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | | aux2 | FCAP/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | - When the opening auction period ends for market "FCAP/USD-1-10" + And the parties submit the following liquidity provision: + | id | party | market id | commitment amount | fee | lp type | + | lp1 | lp | FCAP-PM/USD-1-10 | 1000000 | 0 | submission | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP-PM/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | FCAP-PM/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "2" blocks And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP/USD-1-10" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" + Scenario: Party opens a short position with a market order and is liquidiated at the next mark to market. @@ -54,7 +67,7 @@ Feature: FCAP liquidations Given the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | | aux1 | FCAP/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | - | trader | FCAP/USD-1-10 | buy | 1 | 50 | 1 | TYPE_MARKET | TIF_FOK | + | trader | FCAP/USD-1-10 | buy | 1 | 0 | 1 | TYPE_MARKET | TIF_FOK | When the network moves ahead "1" blocks And the following trades should be executed: | buyer | price | size | seller | @@ -72,4 +85,43 @@ Feature: FCAP liquidations | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 0 | + Scenario: Party place a limit order with not enough to cover fes + + And the following network parameters are set: + | name | value | + | network.markPriceUpdateMaximumFrequency | 5s | + | market.fee.factors.infrastructureFee | 0.1 | + + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | trader | USD-1-10 | 50 | + + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | trader | FCAP-PM/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "1" blocks + And the parties should have the following account balances: + | party | asset | market id | margin | general | + | trader | USD-1-10 | FCAP-PM/USD-1-10 | 50 | 0 | + + And the market data for the market "FCAP-PM/USD-1-10" should be: + | mark price | trading mode | horizon | min bound | max bound | + | 50 | TRADING_MODE_CONTINUOUS | 3600 | 48 | 52 | + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | aux1 | FCAP-PM/USD-1-10 | buy | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | auction-order-aux1 | + | aux2 | FCAP-PM/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | auction-order-aux2 | + When the network moves ahead "1" blocks + And the trading mode should be "TRADING_MODE_MONITORING_AUCTION" for the market "FCAP-PM/USD-1-10" + Then the parties cancel the following orders: + | party | reference | + | aux1 | auction-order-aux1 | + | aux2 | auction-order-aux2 | + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP-PM/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + Then the network moves ahead "10" blocks + Then debug trades + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" diff --git a/core/integration/features/verified/fcap_liquidations_and_fees.feature b/core/integration/features/verified/fcap_liquidations_and_fees.feature new file mode 100644 index 00000000000..61d18dda970 --- /dev/null +++ b/core/integration/features/verified/fcap_liquidations_and_fees.feature @@ -0,0 +1,98 @@ +Feature: FCAP liquidations + + Checking interactions between parties paying fees when barely having + enough funds to cover positions on FCAP markets. + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0.1 | + | market.fee.factors.infrastructureFee | 0.1 | + | network.markPriceUpdateMaximumFrequency | 0s | + And the following assets are registered: + | id | decimal places | quantum | + | USD-1-10 | 0 | 1 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | lp | USD-1-10 | 100000000000 | + | aux1 | USD-1-10 | 100000000000 | + | aux2 | USD-1-10 | 100000000000 | + + # Setup the FCAP market in continuous trading + Given the price monitoring named "price-monitoring": + | horizon | probability | auction extension | + | 3600 | 0.99 | 5 | + Given the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | max price cap | binary | fully collateralised | + | FCAP-PM/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | price-monitoring | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | 100 | true | true | + And the parties submit the following liquidity provision: + | id | party | market id | commitment amount | fee | lp type | + | lp1 | lp | FCAP-PM/USD-1-10 | 1000000 | 0 | submission | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP-PM/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | FCAP-PM/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "2" blocks + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" + + + Scenario: Party places a limit order with not enough funds to cover fes + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | trader | USD-1-10 | 50 | + + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP-PM/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + | trader | FCAP-PM/USD-1-10 | buy | 1 | 50 | 1 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "1" blocks + Then the following trades should be executed: + | buyer | price | size | seller | + | trader | 50 | 1 | aux1 | + | network | 50 | 1 | trader | + + + Scenario: Party places a limit order which requires no fees in continuous trading but later requires fees in auction + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | trader | USD-1-10 | 50 | + + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | trader | FCAP-PM/USD-1-10 | buy | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + When the network moves ahead "1" blocks + And the parties should have the following account balances: + | party | asset | market id | margin | general | + | trader | USD-1-10 | FCAP-PM/USD-1-10 | 50 | 0 | + + And the market data for the market "FCAP-PM/USD-1-10" should be: + | mark price | trading mode | horizon | min bound | max bound | + | 50 | TRADING_MODE_CONTINUOUS | 3600 | 48 | 52 | + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | aux1 | FCAP-PM/USD-1-10 | buy | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | auction-order-aux1 | + | aux2 | FCAP-PM/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | auction-order-aux2 | + When the network moves ahead "1" blocks + And the trading mode should be "TRADING_MODE_MONITORING_AUCTION" for the market "FCAP-PM/USD-1-10" + Then the parties cancel the following orders: + | party | reference | + | aux1 | auction-order-aux1 | + | aux2 | auction-order-aux2 | + Given the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP-PM/USD-1-10 | sell | 1 | 50 | 0 | TYPE_LIMIT | TIF_GTC | + Then the network moves ahead "10" blocks + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" + And the following trades should be executed: + | buyer | price | size | seller | + | trader | 50 | 1 | aux1 | + | network | 50 | 1 | trader | + + diff --git a/core/integration/features/verified/overflow.feature b/core/integration/features/verified/overflow.feature new file mode 100644 index 00000000000..1fa5a0fdec7 --- /dev/null +++ b/core/integration/features/verified/overflow.feature @@ -0,0 +1,84 @@ +Feature: FCAP liquidations + + vega-market-sim fuzz testing shows parties being liquidated on + fully collateralised capped future (FCAP) markets. + + For the following test cases, the margin requirements are checked + and the price moved against the party to check for liquidations. + + - cross margin: + - party opens short position + - party reduces short position + - party increases short position + - party opens long position + - party reduces long position + - party increases long position + - party switches from long to short position + - party switches from short to long position + + Background: + + # Initialise the network and register the assets + Given the average block duration is "1" + And the following network parameters are set: + | name | value | + | market.fee.factors.makerFee | 0 | + | market.fee.factors.infrastructureFee | 0 | + | network.markPriceUpdateMaximumFrequency | 0s | + + And the following assets are registered: + | id | decimal places | quantum | + | USD-1-10 | 18 | 1000000000000000000 | + + # Initialise the parties and deposit assets + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | lp | USD-1-10 | 1000000000000000000000000000 | + | aux1 | USD-1-10 | 10000000000000000000000000000000000000000000000000000000 | + | aux2 | USD-1-10 | 10000000000000000000000000000000000000000000000000000000 | + + # Setup the FCAP market in continuous trading + Given the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | max price cap | binary | fully collateralised | + | FCAP/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 4 | 1 | 10000 | true | true | + And the parties submit the following liquidity provision: + | id | party | market id | commitment amount | fee | lp type | + | lp1 | lp | FCAP/USD-1-10 | 1000000000000000000000 | 0 | submission | + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP/USD-1-10 | buy | 1 | 5000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | FCAP/USD-1-10 | sell | 1 | 5000 | 0 | TYPE_LIMIT | TIF_GTC | + When the opening auction period ends for market "FCAP/USD-1-10" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP/USD-1-10" + + Scenario Outline: Simple test case, party opens long position, margin correctly taken and party never closed out. + + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | trader | USD-1-10 | | + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP/USD-1-10 | sell | | | 0 | TYPE_LIMIT | TIF_GTC | + | trader | FCAP/USD-1-10 | buy | | | 1 | TYPE_LIMIT | TIF_GTC | + Then the network moves ahead "1" blocks + Then the parties should have the following account balances: + | party | asset | market id | margin | + | trader | USD-1-10 | FCAP/USD-1-10 | | + + Then the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | FCAP/USD-1-10 | buy | 1 | | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | FCAP/USD-1-10 | sell | 1 | | 1 | TYPE_LIMIT | TIF_GTC | + Then the network moves ahead "1" blocks + Then the parties should have the following account balances: + | party | asset | market id | margin | + | trader | USD-1-10 | FCAP/USD-1-10 | | + + Examples: + | funds | size | entry price | entry margin | final price | final margin | + # | 25000000000000000000 | 1 | 2500 | 25000000000000000 | 1 | 10000000000000 | + # | 250000000000000000000 | 10 | 2500 | 250000000000000000 | 1 | 100000000000000 | + # | 75000000000000000000 | 1 | 7500 | 75000000000000000 | 1 | 10000000000000 | + # | 750000000000000000000 | 10 | 7500 | 750000000000000000 | 1 | 100000000000000 | + | 50966286350000000000000 | 1075465 | 4739 | 50966286350000000000000 | 1 | 10754650000000000000 | \ No newline at end of file diff --git a/core/integration/features/verified/rewards.feature b/core/integration/features/verified/rewards.feature new file mode 100644 index 00000000000..1cfb5a032df --- /dev/null +++ b/core/integration/features/verified/rewards.feature @@ -0,0 +1,91 @@ +Feature: Team Rewards + + Setup a maker fees received team game with a fee cap. + + We want to make it so that one team is allocated rewards and the other team is allocated rewards. + + - Team A should have also paid rewards and not have their rewards capped. + - Team B should not have paid rewards and have their rewards capped. + + Question is what happens to the left over rewards. + + + Background: + + And the average block duration is "1" + And the following network parameters are set: + | name | value | + | referralProgram.minStakedVegaTokens | 0 | + | market.fee.factors.makerFee | 0.01 | + | network.markPriceUpdateMaximumFrequency | 0s | + | validators.epoch.length | 60s | + + # Initialise the markets + And the following assets are registered: + | id | decimal places | quantum | + | USD-1-10 | 0 | 1 | + And the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | decimal places | position decimal places | + | ETH/USD-1-10 | ETH | USD-1-10 | default-log-normal-risk-model | default-margin-calculator | 1 | default-none | default-none | default-eth-for-future | 1e-3 | 0 | default-futures | 0 | 0 | + + # Initialise the parties + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | aux1 | USD-1-10 | 1000000000000 | + | aux2 | USD-1-10 | 1000000000000 | + | party1 | USD-1-10 | 1000000000000 | + | party2 | USD-1-10 | 1000000000000 | + | a3c024b4e23230c89884a54a813b1ecb4cb0f827a38641c66eeca466da6b2ddf | USD-1-10 | 1000000000000 | + + # Exit opening auctions + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | ETH/USD-1-10 | buy | 1 | 990 | 0 | TYPE_LIMIT | TIF_GTC | + | aux1 | ETH/USD-1-10 | buy | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | ETH/USD-1-10 | sell | 1 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux2 | ETH/USD-1-10 | sell | 1 | 1100 | 0 | TYPE_LIMIT | TIF_GTC | + And the opening auction period ends for market "ETH/USD-1-10" + And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "ETH/USD-1-10" + + Given the parties create the following referral codes: + | party | code | is_team | team | + | party1 | referral-code-1 | true | team1 | + Given the parties create the following referral codes: + | party | code | is_team | team | + | party2 | referral-code-1 | true | team2 | + + Scenario: This is a test to check what happens to the capped fees. + + Given the current epoch is "0" + When the parties submit the following recurring transfers: + | id | from | from_account_type | to | to_account_type | entity_scope | asset | amount | start_epoch | end_epoch | factor | metric | metric_asset | markets | lock_period | window_length | ntop | cap_reward_fee_multiple | + | 1 | a3c024b4e23230c89884a54a813b1ecb4cb0f827a38641c66eeca466da6b2ddf | ACCOUNT_TYPE_GENERAL | 0000000000000000000000000000000000000000000000000000000000000000 | ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES | TEAMS | USD-1-10 | 100 | 1 | 5 | 1 | DISPATCH_METRIC_MAKER_FEES_RECEIVED | USD-1-10 | ETH/USD-1-10 | 10 | 1 | 1 | 1 | + Then the network moves ahead "1" epochs + + ## pary1 + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | ETH/USD-1-10 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | party1 | ETH/USD-1-10 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC | + | party1 | ETH/USD-1-10 | buy | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | aux1 | ETH/USD-1-10 | sell | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC | + And the following trades should be executed: + | buyer | size | price | seller | aggressor side | buyer maker fee | seller maker fee | + | party1 | 10 | 1000 | aux1 | buy | 100 | 0 | + | party1 | 10 | 1000 | aux1 | sell | 0 | 100 | + + # party2 + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | aux1 | ETH/USD-1-10 | sell | 10 | 1000 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/USD-1-10 | buy | 10 | 1000 | 1 | TYPE_LIMIT | TIF_GTC | + And the following trades should be executed: + | buyer | size | price | seller | aggressor side | buyer maker fee | + | party2 | 10 | 1000 | aux1 | buy | 100 | + + When the network moves ahead "1" epochs + Then debug transfers + And parties should have the following vesting account balances: + | party | asset | balance | + | party1 | USD-1-10 | 100 | + | party2 | USD-1-10 | 0 | \ No newline at end of file From 1d2775890b5fd1c679c3b30f64860f161fb21dbc Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 19 Jul 2024 10:48:52 +0100 Subject: [PATCH 07/12] feat: add tags --- .../features/verified/fcap_liquidations_and_fees.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/integration/features/verified/fcap_liquidations_and_fees.feature b/core/integration/features/verified/fcap_liquidations_and_fees.feature index 61d18dda970..4f0f9c0911b 100644 --- a/core/integration/features/verified/fcap_liquidations_and_fees.feature +++ b/core/integration/features/verified/fcap_liquidations_and_fees.feature @@ -40,7 +40,7 @@ Feature: FCAP liquidations When the network moves ahead "2" blocks And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" - + @CappedF Scenario: Party places a limit order with not enough funds to cover fes Given the parties deposit on asset's general account the following amount: @@ -57,7 +57,7 @@ Feature: FCAP liquidations | trader | 50 | 1 | aux1 | | network | 50 | 1 | trader | - + @CappedF Scenario: Party places a limit order which requires no fees in continuous trading but later requires fees in auction Given the parties deposit on asset's general account the following amount: From 790054e7fc78ae7387157b2281ef6b6a31127419 Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Mon, 22 Jul 2024 10:54:36 +0100 Subject: [PATCH 08/12] chore: tag related tests Signed-off-by: Elias Van Ootegem --- core/integration/features/verified/fcap_liquidations.feature | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/integration/features/verified/fcap_liquidations.feature b/core/integration/features/verified/fcap_liquidations.feature index 8e982562748..fc8661f545e 100644 --- a/core/integration/features/verified/fcap_liquidations.feature +++ b/core/integration/features/verified/fcap_liquidations.feature @@ -54,6 +54,7 @@ Feature: FCAP liquidations + @CappedF Scenario: Party opens a short position with a market order and is liquidiated at the next mark to market. And the following network parameters are set: @@ -74,7 +75,7 @@ Feature: FCAP liquidations | trader | 60 | 1 | aux1 | And the parties should have the following account balances: | party | asset | market id | margin | general | - | trader | USD-1-10 | FCAP/USD-1-10 | 50 | 0 | + | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 50 | When the network moves ahead "5" blocks And the following trades should be executed: @@ -85,6 +86,7 @@ Feature: FCAP liquidations | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 0 | + @CappedF Scenario: Party place a limit order with not enough to cover fes And the following network parameters are set: From 3668c1585da6d17e28946870e05da6820779f19a Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Mon, 22 Jul 2024 11:49:54 +0100 Subject: [PATCH 09/12] fix: add missing margin check for market orders and X-margin mode Signed-off-by: Elias Van Ootegem --- core/execution/future/market.go | 11 +++++++++++ ...zero-after-trading-out-market-order.feature | 4 ++-- .../598-open-position-with-zero-margin.feature | 2 +- .../release-margins-issues-decimals.feature | 2 +- .../features/release-margins-issues.feature | 4 ++-- .../verified/fcap_liquidations.feature | 18 +++--------------- core/positions/market_position.go | 4 ++++ 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/core/execution/future/market.go b/core/execution/future/market.go index 4bf2c65722a..19f97a8dff3 100644 --- a/core/execution/future/market.go +++ b/core/execution/future/market.go @@ -2569,6 +2569,17 @@ func (m *Market) submitValidatedOrder(ctx context.Context, order *types.Order) ( return nil, nil, err } } + if order.Type == types.OrderTypeMarket && marginMode == types.MarginModeCrossMargin && !order.ReduceOnly && !pos.OrderReducesExposure(order) { + if err := m.checkMarginForOrder(ctx, posWithTrades, order); err != nil { + if m.log.GetLevel() <= logging.DebugLevel { + m.log.Debug("Unable to check/add margin for party", + logging.Order(*order), logging.Error(err)) + } + _ = m.unregisterAndReject( + ctx, order, types.OrderErrorMarginCheckFailed) + return nil, nil, common.ErrMarginCheckFailed + } + } } // Send the aggressive order into matching engine diff --git a/core/integration/features/margin/596-margin-account-non-zero-after-trading-out-market-order.feature b/core/integration/features/margin/596-margin-account-non-zero-after-trading-out-market-order.feature index a4a43a04e9c..8c1bb80ed38 100644 --- a/core/integration/features/margin/596-margin-account-non-zero-after-trading-out-market-order.feature +++ b/core/integration/features/margin/596-margin-account-non-zero-after-trading-out-market-order.feature @@ -68,7 +68,7 @@ Feature: Regression test for issue 596 Then the parties should have the following account balances: | party | asset | market id | margin | general | | edd | BTC | ETH/DEC19 | 1491 | 8449 | - | chris | BTC | ETH/DEC19 | 1048 | 8503 | + | chris | BTC | ETH/DEC19 | 1077 | 8474 | | barney | BTC | ETH/DEC19 | 594 | 9406 | And the cumulated balance for all accounts should be worth "3041000" @@ -169,7 +169,7 @@ Feature: Regression test for issue 596 Then the parties should have the following account balances: | party | asset | market id | margin | general | | edd | BTC | ETH/DEC19 | 1491 | 8449 | - | chris | BTC | ETH/DEC19 | 1048 | 8503 | + | chris | BTC | ETH/DEC19 | 1077 | 8474 | | barney | BTC | ETH/DEC19 | 594 | 9406 | And the cumulated balance for all accounts should be worth "3041000" diff --git a/core/integration/features/margin/598-open-position-with-zero-margin.feature b/core/integration/features/margin/598-open-position-with-zero-margin.feature index ed17162527c..855b3e8b087 100644 --- a/core/integration/features/margin/598-open-position-with-zero-margin.feature +++ b/core/integration/features/margin/598-open-position-with-zero-margin.feature @@ -72,7 +72,7 @@ Feature: Regression test for issue 598 Then the parties should have the following account balances: | party | asset | market id | margin | general | | edd | BTC | ETH/DEC19 | 571 | 429 | - | chris | BTC | ETH/DEC19 | 109 | 790 | + | chris | BTC | ETH/DEC19 | 108 | 791 | # next instruction will trade with barney When the parties place the following orders with ticks: diff --git a/core/integration/features/release-margins-issues-decimals.feature b/core/integration/features/release-margins-issues-decimals.feature index 45c9cdc7427..fce082e15cf 100644 --- a/core/integration/features/release-margins-issues-decimals.feature +++ b/core/integration/features/release-margins-issues-decimals.feature @@ -115,7 +115,7 @@ Feature: Test margin release on order cancel And the parties should have the following account balances: | party | asset | market id | margin | general | - | partyGuy | ETH | ETH/DEC19 | 415800 | 9485200 | + | partyGuy | ETH | ETH/DEC19 | 420000 | 9481000 | Then the parties should have the following profit and loss: | party | volume | unrealised pnl | realised pnl | diff --git a/core/integration/features/release-margins-issues.feature b/core/integration/features/release-margins-issues.feature index e88e44e89a3..e0dabc0716a 100644 --- a/core/integration/features/release-margins-issues.feature +++ b/core/integration/features/release-margins-issues.feature @@ -64,7 +64,7 @@ Feature: Test margin release on order cancel | partyGuy | ETH | ETH/DEC19 | 0 | 10000 | - @MarginRelease + @MarginRelease @Chris Scenario: a party place a new market order in the system, order, trade, party margin is updated, then place an GTC order which will trade, margin is 0ed Given the parties deposit on asset's general account the following amount: | party | asset | amount | @@ -109,7 +109,7 @@ Feature: Test margin release on order cancel And the parties should have the following account balances: | party | asset | market id | margin | general | - | partyGuy | ETH | ETH/DEC19 | 416 | 9485 | + | partyGuy | ETH | ETH/DEC19 | 420 | 9481 | Then the parties should have the following profit and loss: | party | volume | unrealised pnl | realised pnl | diff --git a/core/integration/features/verified/fcap_liquidations.feature b/core/integration/features/verified/fcap_liquidations.feature index fc8661f545e..a5b43361f87 100644 --- a/core/integration/features/verified/fcap_liquidations.feature +++ b/core/integration/features/verified/fcap_liquidations.feature @@ -66,25 +66,13 @@ Feature: FCAP liquidations | trader | USD-1-10 | 50 | Given the parties place the following orders: - | party | market id | side | volume | price | resulting trades | type | tif | - | aux1 | FCAP/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | - | trader | FCAP/USD-1-10 | buy | 1 | 0 | 1 | TYPE_MARKET | TIF_FOK | - When the network moves ahead "1" blocks - And the following trades should be executed: - | buyer | price | size | seller | - | trader | 60 | 1 | aux1 | + | party | market id | side | volume | price | resulting trades | type | tif | error | + | aux1 | FCAP/USD-1-10 | sell | 1 | 60 | 0 | TYPE_LIMIT | TIF_GTC | | + | trader | FCAP/USD-1-10 | buy | 1 | 0 | 0 | TYPE_MARKET | TIF_FOK | margin check failed | And the parties should have the following account balances: | party | asset | market id | margin | general | | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 50 | - When the network moves ahead "5" blocks - And the following trades should be executed: - | buyer | price | size | seller | - | network | 60 | 1 | trader | - Then the parties should have the following account balances: - | party | asset | market id | margin | general | - | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 0 | - @CappedF Scenario: Party place a limit order with not enough to cover fes diff --git a/core/positions/market_position.go b/core/positions/market_position.go index b2a5686955a..5c3f6ba4211 100644 --- a/core/positions/market_position.go +++ b/core/positions/market_position.go @@ -73,8 +73,12 @@ func (p *MarketPosition) Closed() bool { // UpdateInPlaceOnTrades takes a clone of the receiver position, and updates it with the position from the given trades. func (p *MarketPosition) UpdateInPlaceOnTrades(log *logging.Logger, traderSide types.Side, trades []*types.Trade, order *types.Order) *MarketPosition { + isMO := order.Type == types.OrderTypeMarket pos := p.Clone() for _, t := range trades { + if isMO { + pos.price = CalcVWAP(pos.price, pos.size, int64(t.Size), t.Price) + } pos.averageEntryPrice = CalcVWAP(pos.averageEntryPrice, pos.size, int64(t.Size), t.Price) if traderSide == types.SideBuy { pos.size += int64(t.Size) From 7ae568b81405d375353250452bbde276c8e0b573 Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Mon, 22 Jul 2024 12:47:27 +0100 Subject: [PATCH 10/12] test: do not create perps equivalent of capped future Signed-off-by: Elias Van Ootegem --- core/integration/steps/the_markets.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/integration/steps/the_markets.go b/core/integration/steps/the_markets.go index 2596c445007..7c5bedea666 100644 --- a/core/integration/steps/the_markets.go +++ b/core/integration/steps/the_markets.go @@ -94,7 +94,7 @@ func TheMarkets( } } var mkt types.Market - if isPerp { + if isPerp && !mRow.IsCapped() { mkt = newPerpMarket(config, mRow) } else { mkt = newMarket(config, mRow) @@ -470,6 +470,7 @@ func newPerpMarket(config *market.Config, row marketRow) types.Market { panic(err) } + pCap := row.getCapped() specs, binding := row.oracles(config) markPriceConfig := &types.CompositePriceConfiguration{ CompositePriceType: row.markPriceType(), @@ -526,6 +527,9 @@ func newPerpMarket(config *market.Config, row marketRow) types.Market { tip := m.TradableInstrument.IntoProto() err = config.RiskModels.LoadModel(row.riskModel(), tip) + if row.IsCapped() { + tip.MarginCalculator.FullyCollateralised = ptr.From(pCap.FullyCollateralised) + } m.TradableInstrument = types.TradableInstrumentFromProto(tip) if err != nil { panic(err) From 197666a815ab31d72767b68e937470a2f1f629fd Mon Sep 17 00:00:00 2001 From: Elias Van Ootegem Date: Mon, 22 Jul 2024 12:55:45 +0100 Subject: [PATCH 11/12] test: add missing NoPerp tags to capped future tests, remove check from integration test framework Signed-off-by: Elias Van Ootegem --- CHANGELOG.md | 1 + core/integration/features/verified/fcap_liquidations.feature | 4 ++-- .../features/verified/fcap_liquidations_and_fees.feature | 4 ++-- core/integration/features/verified/overflow.feature | 3 ++- core/integration/steps/the_markets.go | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f6a415804..49b1cb89a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - [11453](https://github.com/vegaprotocol/vega/issues/11453) - Fix margin for fully collateralised markets. - [11462](https://github.com/vegaprotocol/vega/issues/11462) - Update the time weighted notional when publishing live game scores. - [11474](https://github.com/vegaprotocol/vega/issues/11474) - Fail `checkTx` downstream for delayed transactions so they don't get included in more than one block. +- [11481](https://github.com/vegaprotocol/vega/issues/11481) - Fix margin check for cross margin market orders. ## 0.77.0 diff --git a/core/integration/features/verified/fcap_liquidations.feature b/core/integration/features/verified/fcap_liquidations.feature index a5b43361f87..5401ad4f95c 100644 --- a/core/integration/features/verified/fcap_liquidations.feature +++ b/core/integration/features/verified/fcap_liquidations.feature @@ -54,7 +54,7 @@ Feature: FCAP liquidations - @CappedF + @CappedF @NoPerp Scenario: Party opens a short position with a market order and is liquidiated at the next mark to market. And the following network parameters are set: @@ -74,7 +74,7 @@ Feature: FCAP liquidations | trader | USD-1-10 | FCAP/USD-1-10 | 0 | 50 | - @CappedF + @CappedF @NoPerp Scenario: Party place a limit order with not enough to cover fes And the following network parameters are set: diff --git a/core/integration/features/verified/fcap_liquidations_and_fees.feature b/core/integration/features/verified/fcap_liquidations_and_fees.feature index 4f0f9c0911b..d649971889c 100644 --- a/core/integration/features/verified/fcap_liquidations_and_fees.feature +++ b/core/integration/features/verified/fcap_liquidations_and_fees.feature @@ -40,7 +40,7 @@ Feature: FCAP liquidations When the network moves ahead "2" blocks And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP-PM/USD-1-10" - @CappedF + @CappedF @NoPerp Scenario: Party places a limit order with not enough funds to cover fes Given the parties deposit on asset's general account the following amount: @@ -57,7 +57,7 @@ Feature: FCAP liquidations | trader | 50 | 1 | aux1 | | network | 50 | 1 | trader | - @CappedF + @CappedF @NoPerp Scenario: Party places a limit order which requires no fees in continuous trading but later requires fees in auction Given the parties deposit on asset's general account the following amount: diff --git a/core/integration/features/verified/overflow.feature b/core/integration/features/verified/overflow.feature index 1fa5a0fdec7..7eaedfa023a 100644 --- a/core/integration/features/verified/overflow.feature +++ b/core/integration/features/verified/overflow.feature @@ -51,6 +51,7 @@ Feature: FCAP liquidations When the opening auction period ends for market "FCAP/USD-1-10" And the trading mode should be "TRADING_MODE_CONTINUOUS" for the market "FCAP/USD-1-10" + @NoPerp Scenario Outline: Simple test case, party opens long position, margin correctly taken and party never closed out. Given the parties deposit on asset's general account the following amount: @@ -81,4 +82,4 @@ Feature: FCAP liquidations # | 250000000000000000000 | 10 | 2500 | 250000000000000000 | 1 | 100000000000000 | # | 75000000000000000000 | 1 | 7500 | 75000000000000000 | 1 | 10000000000000 | # | 750000000000000000000 | 10 | 7500 | 750000000000000000 | 1 | 100000000000000 | - | 50966286350000000000000 | 1075465 | 4739 | 50966286350000000000000 | 1 | 10754650000000000000 | \ No newline at end of file + | 50966286350000000000000 | 1075465 | 4739 | 50966286350000000000000 | 1 | 10754650000000000000 | diff --git a/core/integration/steps/the_markets.go b/core/integration/steps/the_markets.go index 7c5bedea666..392591bdfbf 100644 --- a/core/integration/steps/the_markets.go +++ b/core/integration/steps/the_markets.go @@ -94,7 +94,7 @@ func TheMarkets( } } var mkt types.Market - if isPerp && !mRow.IsCapped() { + if isPerp { mkt = newPerpMarket(config, mRow) } else { mkt = newMarket(config, mRow) From 5aab8fba9670435284cf04a3dabfe78b87a0708a Mon Sep 17 00:00:00 2001 From: Jeremy Letang Date: Tue, 23 Jul 2024 10:57:53 +0200 Subject: [PATCH 12/12] chore: release version 0.77.2 Signed-off-by: Jeremy Letang --- CHANGELOG.md | 9 ++++++++- protos/blockexplorer/api/v1/blockexplorer.pb.go | 2 +- protos/data-node/api/v2/trading_data.pb.go | 2 +- protos/sources/blockexplorer/api/v1/blockexplorer.proto | 2 +- protos/sources/data-node/api/v2/trading_data.proto | 2 +- protos/sources/vega/api/v1/core.proto | 2 +- protos/sources/vega/api/v1/corestate.proto | 2 +- protos/vega/api/v1/core.pb.go | 2 +- protos/vega/api/v1/corestate.pb.go | 2 +- version/version.go | 2 +- 10 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b1cb89a51..00382adfb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ - [](https://github.com/vegaprotocol/vega/issues/xxx) +## 0.77.2 + +### 🐛 Fixes + +- [11481](https://github.com/vegaprotocol/vega/issues/11481) - Fix margin check for cross margin market orders. +- [11474](https://github.com/vegaprotocol/vega/issues/11474) - Fail `checkTx` downstream for delayed transactions so they don't get included in more than one block. + ## 0.77.1 ### 🛠 Improvements @@ -29,7 +36,7 @@ - [11453](https://github.com/vegaprotocol/vega/issues/11453) - Fix margin for fully collateralised markets. - [11462](https://github.com/vegaprotocol/vega/issues/11462) - Update the time weighted notional when publishing live game scores. -- [11474](https://github.com/vegaprotocol/vega/issues/11474) - Fail `checkTx` downstream for delayed transactions so they don't get included in more than one block. +- [11474](https://github.com/vegaprotocol/vega/issues/11474) - Fail `checkTx` downstream for delayed transactions so they don't get included in more than one block. - [11481](https://github.com/vegaprotocol/vega/issues/11481) - Fix margin check for cross margin market orders. ## 0.77.0 diff --git a/protos/blockexplorer/api/v1/blockexplorer.pb.go b/protos/blockexplorer/api/v1/blockexplorer.pb.go index e0550cf5ee5..f65deeea15b 100644 --- a/protos/blockexplorer/api/v1/blockexplorer.pb.go +++ b/protos/blockexplorer/api/v1/blockexplorer.pb.go @@ -656,7 +656,7 @@ var file_blockexplorer_api_v1_blockexplorer_proto_rawDesc = []byte{ 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x92, 0x41, 0x3e, 0x12, 0x23, 0x0a, 0x18, 0x56, 0x65, 0x67, 0x61, 0x20, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x72, 0x20, - 0x41, 0x50, 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x31, 0x1a, 0x13, 0x6c, + 0x41, 0x50, 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x32, 0x1a, 0x13, 0x6c, 0x62, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x65, 0x67, 0x61, 0x2e, 0x78, 0x79, 0x7a, 0x2a, 0x02, 0x01, 0x02, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } diff --git a/protos/data-node/api/v2/trading_data.pb.go b/protos/data-node/api/v2/trading_data.pb.go index 6b838bf45d3..8bc663ff350 100644 --- a/protos/data-node/api/v2/trading_data.pb.go +++ b/protos/data-node/api/v2/trading_data.pb.go @@ -32009,7 +32009,7 @@ var file_data_node_api_v2_trading_data_proto_rawDesc = []byte{ 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x92, 0x41, 0x8f, 0x01, 0x12, 0x1e, 0x0a, 0x13, 0x56, 0x65, 0x67, 0x61, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x6e, 0x6f, 0x64, 0x65, 0x20, - 0x41, 0x50, 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x31, 0x1a, 0x1c, 0x68, + 0x41, 0x50, 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x32, 0x1a, 0x1c, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x65, 0x67, 0x61, 0x2e, 0x78, 0x79, 0x7a, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, diff --git a/protos/sources/blockexplorer/api/v1/blockexplorer.proto b/protos/sources/blockexplorer/api/v1/blockexplorer.proto index 4aceaa75dca..79d28df454d 100644 --- a/protos/sources/blockexplorer/api/v1/blockexplorer.proto +++ b/protos/sources/blockexplorer/api/v1/blockexplorer.proto @@ -11,7 +11,7 @@ option go_package = "code.vegaprotocol.io/vega/protos/blockexplorer/api/v1"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Vega block explorer APIs"; - version: "v0.77.1"; + version: "v0.77.2"; } schemes: [ HTTP, diff --git a/protos/sources/data-node/api/v2/trading_data.proto b/protos/sources/data-node/api/v2/trading_data.proto index 21476e21af9..3241dc997e7 100644 --- a/protos/sources/data-node/api/v2/trading_data.proto +++ b/protos/sources/data-node/api/v2/trading_data.proto @@ -17,7 +17,7 @@ option go_package = "code.vegaprotocol.io/vega/protos/data-node/api/v2"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Vega data node APIs"; - version: "v0.77.1"; + version: "v0.77.2"; } schemes: [ HTTP, diff --git a/protos/sources/vega/api/v1/core.proto b/protos/sources/vega/api/v1/core.proto index 744534c3e1b..335236adb68 100644 --- a/protos/sources/vega/api/v1/core.proto +++ b/protos/sources/vega/api/v1/core.proto @@ -12,7 +12,7 @@ option go_package = "code.vegaprotocol.io/vega/protos/vega/api/v1"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Vega core APIs"; - version: "v0.77.1"; + version: "v0.77.2"; } schemes: [ HTTP, diff --git a/protos/sources/vega/api/v1/corestate.proto b/protos/sources/vega/api/v1/corestate.proto index 078e1ccb6e9..15d108c779d 100644 --- a/protos/sources/vega/api/v1/corestate.proto +++ b/protos/sources/vega/api/v1/corestate.proto @@ -13,7 +13,7 @@ option go_package = "code.vegaprotocol.io/vega/protos/vega/api/v1"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Vega core state APIs"; - version: "v0.77.1"; + version: "v0.77.2"; } schemes: [ HTTP, diff --git a/protos/vega/api/v1/core.pb.go b/protos/vega/api/v1/core.pb.go index 49e877ebafa..6cd53b7129d 100644 --- a/protos/vega/api/v1/core.pb.go +++ b/protos/vega/api/v1/core.pb.go @@ -2796,7 +2796,7 @@ var file_vega_api_v1_core_proto_rawDesc = []byte{ 0x6f, 0x6c, 0x2e, 0x69, 0x6f, 0x2f, 0x76, 0x65, 0x67, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x65, 0x67, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x92, 0x41, 0x34, 0x12, 0x19, 0x0a, 0x0e, 0x56, 0x65, 0x67, 0x61, 0x20, 0x63, 0x6f, 0x72, 0x65, 0x20, 0x41, 0x50, - 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x31, 0x1a, 0x13, 0x6c, 0x62, 0x2e, + 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, 0x2e, 0x32, 0x1a, 0x13, 0x6c, 0x62, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x65, 0x67, 0x61, 0x2e, 0x78, 0x79, 0x7a, 0x2a, 0x02, 0x01, 0x02, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } diff --git a/protos/vega/api/v1/corestate.pb.go b/protos/vega/api/v1/corestate.pb.go index 4be93704fc6..541803c0269 100644 --- a/protos/vega/api/v1/corestate.pb.go +++ b/protos/vega/api/v1/corestate.pb.go @@ -1512,7 +1512,7 @@ var file_vega_api_v1_corestate_proto_rawDesc = []byte{ 0x6f, 0x73, 0x2f, 0x76, 0x65, 0x67, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x92, 0x41, 0x3a, 0x12, 0x1f, 0x0a, 0x14, 0x56, 0x65, 0x67, 0x61, 0x20, 0x63, 0x6f, 0x72, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x41, 0x50, 0x49, 0x73, 0x32, 0x07, 0x76, 0x30, 0x2e, 0x37, 0x37, - 0x2e, 0x31, 0x1a, 0x13, 0x6c, 0x62, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x2e, 0x32, 0x1a, 0x13, 0x6c, 0x62, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x65, 0x67, 0x61, 0x2e, 0x78, 0x79, 0x7a, 0x2a, 0x02, 0x01, 0x02, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } diff --git a/version/version.go b/version/version.go index 788ceeb6f1f..50bf7b8c402 100644 --- a/version/version.go +++ b/version/version.go @@ -22,7 +22,7 @@ import ( var ( cliVersionHash = "" - cliVersion = "v0.77.1" + cliVersion = "v0.77.2" ) func init() {