Skip to content

Commit 260cc2e

Browse files
authored
Merge pull request #11731 from Nebula-DEX/feature/locked-sell
chore: implement an allow list on market for the sell side
2 parents df52e26 + ac37af8 commit 260cc2e

34 files changed

+3622
-2997
lines changed

core/execution/common/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,6 @@ var (
9191
// ErrSettlementDataOutOfRange is returned when a capped future receives settlement data that is outside of the acceptable range (either > max price, or neither 0 nor max for binary settlements).
9292
ErrSettlementDataOutOfRange = errors.New("settlement data is outside of the price cap")
9393
ErrAMMBoundsOutsidePriceCap = errors.New("an AMM bound is outside of the price cap")
94+
// ErrSellOrderNotAllowed no sell orders are allowed in the current state.
95+
ErrSellOrderNotAllowed = errors.New("sell order not allowed")
9496
)

core/execution/future/market_snapshot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ func NewMarketFromSnapshot(
220220
marketType := mkt.MarketType()
221221

222222
markPriceCalculator := common.NewCompositePriceCalculatorFromSnapshot(ctx, em.CurrentMarkPrice, timeService, oracleEngine, em.MarkPriceCalculator)
223+
223224
market := &Market{
224225
log: log,
225226
mkt: mkt,

core/execution/spot/market.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ type Market struct {
142142
minDuration time.Duration
143143
epoch types.Epoch
144144

145-
pap *ProtocolAutomatedPurchase
145+
pap *ProtocolAutomatedPurchase
146+
allowedSellers map[string]struct{}
146147
}
147148

148149
// NewMarket creates a new market using the market framework configuration and creates underlying engines.
@@ -226,6 +227,12 @@ func NewMarket(
226227
els := common.NewEquityShares(num.DecimalZero())
227228
// @TODO pass in AMM
228229
marketLiquidity := common.NewMarketLiquidity(log, liquidity, collateralEngine, broker, book, els, marketActivityTracker, feeEngine, common.SpotMarketType, mkt.ID, quoteAsset, priceFactor, mkt.LiquiditySLAParams.PriceRange, nil)
230+
231+
allowedSellers := map[string]struct{}{}
232+
for _, v := range mkt.AllowedSellers {
233+
allowedSellers[v] = struct{}{}
234+
}
235+
229236
market := &Market{
230237
log: log,
231238
idgen: nil,
@@ -266,6 +273,7 @@ func NewMarket(
266273
stopOrders: stoporders.New(log),
267274
expiringStopOrders: common.NewExpiringOrders(),
268275
banking: banking,
276+
allowedSellers: allowedSellers,
269277
}
270278
liquidity.SetGetStaticPricesFunc(market.getBestStaticPricesDecimal)
271279

@@ -310,6 +318,11 @@ func (m *Market) Update(ctx context.Context, config *types.Market) error {
310318
m.liquidity.UpdateMarketConfig(riskModel, m.pMonitor)
311319
m.updateLiquidityFee(ctx)
312320

321+
clear(m.allowedSellers)
322+
for _, v := range config.AllowedSellers {
323+
m.allowedSellers[v] = struct{}{}
324+
}
325+
313326
if tickSizeChanged {
314327
tickSizeInAsset, _ := num.UintFromDecimal(m.mkt.TickSize.ToDecimal().Mul(m.priceFactor))
315328
peggedOrders := m.matching.GetActivePeggedOrderIDs()
@@ -1259,6 +1272,16 @@ func (m *Market) SubmitStopOrdersWithIDGeneratorAndOrderIDs(
12591272
return nil, common.ErrTradingNotAllowed
12601273
}
12611274

1275+
if fallsBelow != nil && fallsBelow.OrderSubmission != nil && !m.canSubmitMaybeSell(fallsBelow.Party, fallsBelow.OrderSubmission.Side) {
1276+
rejectStopOrders(types.StopOrderRejectionSellOrderNotAllowed, fallsBelow, risesAbove)
1277+
return nil, common.ErrSellOrderNotAllowed
1278+
}
1279+
1280+
if risesAbove != nil && risesAbove.OrderSubmission != nil && !m.canSubmitMaybeSell(risesAbove.Party, risesAbove.OrderSubmission.Side) {
1281+
rejectStopOrders(types.StopOrderRejectionSellOrderNotAllowed, risesAbove, risesAbove)
1282+
return nil, common.ErrSellOrderNotAllowed
1283+
}
1284+
12621285
now := m.timeService.GetTimeNow()
12631286
orderCnt := 0
12641287
if fallsBelow != nil {
@@ -1468,6 +1491,13 @@ func (m *Market) SubmitOrderWithIDGeneratorAndOrderID(ctx context.Context, order
14681491
return nil, common.ErrTradingNotAllowed
14691492
}
14701493

1494+
if !m.canSubmitMaybeSell(order.Party, order.Side) {
1495+
order.Status = types.OrderStatusRejected
1496+
order.Reason = types.OrderErrorSellOrderNotAllowed
1497+
m.broker.Send(events.NewOrderEvent(ctx, order))
1498+
return nil, common.ErrSellOrderNotAllowed
1499+
}
1500+
14711501
conf, _, err := m.submitOrder(ctx, order)
14721502
if err != nil {
14731503
return nil, err
@@ -2876,6 +2906,19 @@ func (m *Market) canTrade() bool {
28762906
m.mkt.State == types.MarketStateSuspendedViaGovernance
28772907
}
28782908

2909+
func (m *Market) canSubmitMaybeSell(party string, side types.Side) bool {
2910+
// buy side
2911+
// or network party
2912+
// or no empty allowedSellers list
2913+
// are always fine
2914+
if len(m.allowedSellers) <= 0 || side == types.SideBuy || party == types.NetworkParty {
2915+
return true
2916+
}
2917+
2918+
_, isAllowed := m.allowedSellers[party]
2919+
return isAllowed
2920+
}
2921+
28792922
// cleanupOnReject removes all resources created while the market was on PREPARED state.
28802923
// at this point no fees would have been collected or anything like this.
28812924
func (m *Market) cleanupOnReject(ctx context.Context) {

core/execution/spot/market_snapshot.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ func NewMarketFromSnapshot(
148148
}
149149

150150
now := timeService.GetTimeNow()
151+
allowedSellers := map[string]struct{}{}
152+
for _, v := range mkt.AllowedSellers {
153+
allowedSellers[v] = struct{}{}
154+
}
151155
market := &Market{
152156
log: log,
153157
mkt: mkt,
@@ -190,6 +194,7 @@ func NewMarketFromSnapshot(
190194
hasTraded: em.HasTraded,
191195
orderHoldingTracker: NewHoldingAccountTracker(mkt.ID, log, collateralEngine),
192196
banking: banking,
197+
allowedSellers: allowedSellers,
193198
}
194199
liquidity.SetGetStaticPricesFunc(market.getBestStaticPricesDecimal)
195200
for _, p := range em.Parties {

core/execution/spot/market_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"code.vegaprotocol.io/vega/core/types"
3737
"code.vegaprotocol.io/vega/libs/crypto"
3838
"code.vegaprotocol.io/vega/libs/num"
39+
"code.vegaprotocol.io/vega/libs/ptr"
3940
"code.vegaprotocol.io/vega/logging"
4041
"code.vegaprotocol.io/vega/protos/vega"
4142

@@ -182,6 +183,17 @@ func newTestMarket(
182183
pMonitorSettings *types.PriceMonitoringSettings,
183184
openingAuctionDuration *types.AuctionDuration,
184185
now time.Time,
186+
) *testMarket {
187+
t.Helper()
188+
return newTestMarketWithAllowedSellers(t, pMonitorSettings, openingAuctionDuration, now, nil)
189+
}
190+
191+
func newTestMarketWithAllowedSellers(
192+
t *testing.T,
193+
pMonitorSettings *types.PriceMonitoringSettings,
194+
openingAuctionDuration *types.AuctionDuration,
195+
now time.Time,
196+
allowedSellers []string,
185197
) *testMarket {
186198
t.Helper()
187199
base := "BTC"
@@ -202,6 +214,7 @@ func newTestMarket(
202214

203215
statevarEngine := stubs.NewStateVar()
204216
mkt := getMarketWithDP(base, quote, pMonitorSettings, openingAuctionDuration, quoteDP, positionDP)
217+
mkt.AllowedSellers = allowedSellers
205218

206219
as := monitor.NewAuctionState(&mkt, now)
207220
epoch := mocks.NewMockEpochEngine(ctrl)
@@ -302,3 +315,51 @@ func getGTCLimitOrder(tm *testMarket,
302315
}
303316
return order
304317
}
318+
319+
//nolint:unparam
320+
func getStopOrderSubmission(tm *testMarket,
321+
now time.Time,
322+
id string,
323+
side1 types.Side,
324+
side2 types.Side,
325+
partyID string,
326+
size uint64,
327+
price uint64,
328+
) *types.StopOrdersSubmission {
329+
return &types.StopOrdersSubmission{
330+
RisesAbove: &types.StopOrderSetup{
331+
OrderSubmission: &types.OrderSubmission{
332+
Type: types.OrderTypeLimit,
333+
TimeInForce: types.OrderTimeInForceGTC,
334+
Side: side1,
335+
MarketID: tm.market.GetID(),
336+
Size: size,
337+
Price: num.NewUint(price),
338+
Reference: "marketorder",
339+
},
340+
Expiry: &types.StopOrderExpiry{
341+
ExpiryStrategy: ptr.From(types.StopOrderExpiryStrategyCancels),
342+
},
343+
Trigger: types.NewTrailingStopOrderTrigger(types.StopOrderTriggerDirectionRisesAbove, num.DecimalFromFloat(0.9)),
344+
SizeOverrideSetting: types.StopOrderSizeOverrideSettingNone,
345+
SizeOverrideValue: nil,
346+
},
347+
FallsBelow: &types.StopOrderSetup{
348+
OrderSubmission: &types.OrderSubmission{
349+
Type: types.OrderTypeLimit,
350+
TimeInForce: types.OrderTimeInForceGTC,
351+
Side: side2,
352+
MarketID: tm.market.GetID(),
353+
Size: size,
354+
Price: num.NewUint(price),
355+
Reference: "marketorder",
356+
},
357+
Expiry: &types.StopOrderExpiry{
358+
ExpiryStrategy: ptr.From(types.StopOrderExpiryStrategyCancels),
359+
},
360+
Trigger: types.NewTrailingStopOrderTrigger(types.StopOrderTriggerDirectionRisesAbove, num.DecimalFromFloat(0.9)),
361+
SizeOverrideSetting: types.StopOrderSizeOverrideSettingNone,
362+
SizeOverrideValue: nil,
363+
},
364+
}
365+
}

core/execution/spot/spot_execution_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import (
2121
"time"
2222

2323
"code.vegaprotocol.io/vega/core/execution/common"
24+
"code.vegaprotocol.io/vega/core/idgeneration"
2425
"code.vegaprotocol.io/vega/core/types"
2526
vegacontext "code.vegaprotocol.io/vega/libs/context"
2627
"code.vegaprotocol.io/vega/libs/crypto"
2728
"code.vegaprotocol.io/vega/libs/num"
29+
"code.vegaprotocol.io/vega/libs/ptr"
2830

2931
"github.com/stretchr/testify/require"
3032
)
@@ -164,6 +166,100 @@ func TestAmend(t *testing.T) {
164166
require.Equal(t, "60000", haBalance1.Balance.String())
165167
}
166168

169+
func TestMarketWithAllowedSellers(t *testing.T) {
170+
now := time.Now()
171+
ctx := context.Background()
172+
ctx = vegacontext.WithTraceID(ctx, crypto.RandomHash())
173+
tm := newTestMarketWithAllowedSellers(t, defaultPriceMonitorSettings, &types.AuctionDuration{Duration: 1}, now, []string{"party1", "party2"})
174+
175+
addAccountWithAmount(tm, "party1", 100000, "ETH")
176+
addAccountWithAmount(tm, "party2", 100000, "ETH")
177+
addAccountWithAmount(tm, "party3", 100000, "ETH")
178+
addAccountWithAmount(tm, "party1", 100000, "BTC")
179+
addAccountWithAmount(tm, "party2", 100000, "BTC")
180+
addAccountWithAmount(tm, "party3", 100000, "BTC")
181+
tm.market.StartOpeningAuction(ctx)
182+
183+
t.Run("allowed seller can post sell orders", func(t *testing.T) {
184+
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party1", 1, 300)
185+
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
186+
require.NoError(t, err)
187+
order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party2", 1, 300)
188+
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
189+
require.NoError(t, err)
190+
})
191+
192+
t.Run("allowed seller can post buy orders", func(t *testing.T) {
193+
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party1", 2, 200)
194+
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
195+
require.NoError(t, err)
196+
order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party2", 2, 200)
197+
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
198+
require.NoError(t, err)
199+
})
200+
201+
t.Run("non allowed seller cannot post sell orders", func(t *testing.T) {
202+
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party3", 2, 300)
203+
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
204+
require.EqualError(t, err, "sell order not allowed")
205+
})
206+
207+
t.Run("non allowed seller can post buy orders", func(t *testing.T) {
208+
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party3", 2, 200)
209+
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
210+
require.NoError(t, err)
211+
})
212+
213+
t.Run("exit auction", func(t *testing.T) {
214+
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party1", 2, 30000)
215+
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
216+
require.NoError(t, err)
217+
218+
order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party2", 1, 30000)
219+
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
220+
require.NoError(t, err)
221+
222+
tm.market.OnTick(ctx, now.Add(2*time.Second))
223+
md := tm.market.GetMarketData()
224+
require.Equal(t, types.MarketTradingModeContinuous, md.MarketTradingMode)
225+
})
226+
227+
t.Run("increase max stop orders per parties", func(t *testing.T) {
228+
tm.market.OnMarketPartiesMaximumStopOrdersUpdate(
229+
context.Background(), num.NewUint(1000))
230+
})
231+
232+
t.Run("allowed seller can post sell stop orders", func(t *testing.T) {
233+
idgen := idgeneration.New(crypto.RandomHash())
234+
order1 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party1", 1, 300)
235+
_, err := tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order1, "party1", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
236+
require.NoError(t, err)
237+
238+
order2 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party2", 1, 300)
239+
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order2, "party2", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
240+
require.NoError(t, err)
241+
242+
order3 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party1", 1, 300)
243+
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order3, "party1", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
244+
require.NoError(t, err)
245+
246+
order4 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party2", 1, 300)
247+
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order4, "party2", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
248+
require.NoError(t, err)
249+
})
250+
251+
t.Run("non allowed seller cannot post sell stop orders", func(t *testing.T) {
252+
idgen := idgeneration.New(crypto.RandomHash())
253+
order1 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party3", 1, 300)
254+
_, err := tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order1, "party3", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
255+
require.EqualError(t, err, "sell order not allowed")
256+
257+
order2 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party3", 1, 300)
258+
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order2, "party3", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
259+
require.EqualError(t, err, "sell order not allowed")
260+
})
261+
}
262+
167263
func TestCancelAll(t *testing.T) {
168264
now := time.Now()
169265
ctx := context.Background()

core/governance/engine.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,7 @@ func (e *Engine) updatedSpotMarketFromProposal(p *proposal) (*types.Market, type
12811281
TickSize: terms.Changes.TickSize,
12821282
LiquidityFeeSettings: terms.Changes.LiquidityFeeSettings,
12831283
EnableTxReordering: terms.Changes.EnableTxReordering,
1284+
AllowedSellers: append([]string{}, terms.Changes.AllowedSellers...),
12841285
},
12851286
}
12861287

@@ -1339,6 +1340,7 @@ func (e *Engine) updatedMarketFromProposal(p *proposal) (*types.Market, types.Pr
13391340
TickSize: terms.Changes.TickSize,
13401341
EnableTxReordering: terms.Changes.EnableTxReordering,
13411342
AllowedEmptyAmmLevels: &allowedEmptyAMMLevels,
1343+
AllowedSellers: append([]string{}, terms.Changes.AllowedSellers...),
13421344
},
13431345
}
13441346

core/governance/market.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ func buildSpotMarketFromProposal(
388388
MarkPriceConfiguration: defaultMarkPriceConfig,
389389
TickSize: definition.Changes.TickSize,
390390
EnableTxReordering: definition.Changes.EnableTxReordering,
391+
AllowedSellers: append([]string{}, definition.Changes.AllowedSellers...),
391392
}
392393
if err := assignSpotRiskModel(definition.Changes, market.TradableInstrument); err != nil {
393394
return nil, types.ProposalErrorUnspecified, err

core/types/governance_new_market.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ type NewMarketConfiguration struct {
414414
TickSize *num.Uint
415415
EnableTxReordering bool
416416
AllowedEmptyAmmLevels *uint64
417+
AllowedSellers []string
417418
}
418419

419420
func (n NewMarketConfiguration) IntoProto() *vegapb.NewMarketConfiguration {
@@ -486,11 +487,13 @@ func (n NewMarketConfiguration) DeepClone() *NewMarketConfiguration {
486487
TickSize: n.TickSize.Clone(),
487488
EnableTxReordering: n.EnableTxReordering,
488489
AllowedEmptyAmmLevels: n.AllowedEmptyAmmLevels,
490+
AllowedSellers: append([]string{}, n.AllowedSellers...),
489491
}
490492
cpy.Metadata = append(cpy.Metadata, n.Metadata...)
491493
if n.Instrument != nil {
492494
cpy.Instrument = n.Instrument.DeepClone()
493495
}
496+
cpy.AllowedSellers = append(cpy.AllowedSellers, n.AllowedSellers...)
494497
if n.PriceMonitoringParameters != nil {
495498
cpy.PriceMonitoringParameters = n.PriceMonitoringParameters.DeepClone()
496499
}

0 commit comments

Comments
 (0)