Skip to content

Commit

Permalink
Merge pull request #11731 from Nebula-DEX/feature/locked-sell
Browse files Browse the repository at this point in the history
chore: implement an allow list on market for the sell side
  • Loading branch information
jeremyletang authored Oct 17, 2024
2 parents df52e26 + ac37af8 commit 260cc2e
Show file tree
Hide file tree
Showing 34 changed files with 3,622 additions and 2,997 deletions.
2 changes: 2 additions & 0 deletions core/execution/common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,6 @@ var (
// 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).
ErrSettlementDataOutOfRange = errors.New("settlement data is outside of the price cap")
ErrAMMBoundsOutsidePriceCap = errors.New("an AMM bound is outside of the price cap")
// ErrSellOrderNotAllowed no sell orders are allowed in the current state.
ErrSellOrderNotAllowed = errors.New("sell order not allowed")
)
1 change: 1 addition & 0 deletions core/execution/future/market_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func NewMarketFromSnapshot(
marketType := mkt.MarketType()

markPriceCalculator := common.NewCompositePriceCalculatorFromSnapshot(ctx, em.CurrentMarkPrice, timeService, oracleEngine, em.MarkPriceCalculator)

market := &Market{
log: log,
mkt: mkt,
Expand Down
45 changes: 44 additions & 1 deletion core/execution/spot/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ type Market struct {
minDuration time.Duration
epoch types.Epoch

pap *ProtocolAutomatedPurchase
pap *ProtocolAutomatedPurchase
allowedSellers map[string]struct{}
}

// NewMarket creates a new market using the market framework configuration and creates underlying engines.
Expand Down Expand Up @@ -226,6 +227,12 @@ func NewMarket(
els := common.NewEquityShares(num.DecimalZero())
// @TODO pass in AMM
marketLiquidity := common.NewMarketLiquidity(log, liquidity, collateralEngine, broker, book, els, marketActivityTracker, feeEngine, common.SpotMarketType, mkt.ID, quoteAsset, priceFactor, mkt.LiquiditySLAParams.PriceRange, nil)

allowedSellers := map[string]struct{}{}
for _, v := range mkt.AllowedSellers {
allowedSellers[v] = struct{}{}
}

market := &Market{
log: log,
idgen: nil,
Expand Down Expand Up @@ -266,6 +273,7 @@ func NewMarket(
stopOrders: stoporders.New(log),
expiringStopOrders: common.NewExpiringOrders(),
banking: banking,
allowedSellers: allowedSellers,
}
liquidity.SetGetStaticPricesFunc(market.getBestStaticPricesDecimal)

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

clear(m.allowedSellers)
for _, v := range config.AllowedSellers {
m.allowedSellers[v] = struct{}{}
}

if tickSizeChanged {
tickSizeInAsset, _ := num.UintFromDecimal(m.mkt.TickSize.ToDecimal().Mul(m.priceFactor))
peggedOrders := m.matching.GetActivePeggedOrderIDs()
Expand Down Expand Up @@ -1259,6 +1272,16 @@ func (m *Market) SubmitStopOrdersWithIDGeneratorAndOrderIDs(
return nil, common.ErrTradingNotAllowed
}

if fallsBelow != nil && fallsBelow.OrderSubmission != nil && !m.canSubmitMaybeSell(fallsBelow.Party, fallsBelow.OrderSubmission.Side) {
rejectStopOrders(types.StopOrderRejectionSellOrderNotAllowed, fallsBelow, risesAbove)
return nil, common.ErrSellOrderNotAllowed
}

if risesAbove != nil && risesAbove.OrderSubmission != nil && !m.canSubmitMaybeSell(risesAbove.Party, risesAbove.OrderSubmission.Side) {
rejectStopOrders(types.StopOrderRejectionSellOrderNotAllowed, risesAbove, risesAbove)
return nil, common.ErrSellOrderNotAllowed
}

now := m.timeService.GetTimeNow()
orderCnt := 0
if fallsBelow != nil {
Expand Down Expand Up @@ -1468,6 +1491,13 @@ func (m *Market) SubmitOrderWithIDGeneratorAndOrderID(ctx context.Context, order
return nil, common.ErrTradingNotAllowed
}

if !m.canSubmitMaybeSell(order.Party, order.Side) {
order.Status = types.OrderStatusRejected
order.Reason = types.OrderErrorSellOrderNotAllowed
m.broker.Send(events.NewOrderEvent(ctx, order))
return nil, common.ErrSellOrderNotAllowed
}

conf, _, err := m.submitOrder(ctx, order)
if err != nil {
return nil, err
Expand Down Expand Up @@ -2876,6 +2906,19 @@ func (m *Market) canTrade() bool {
m.mkt.State == types.MarketStateSuspendedViaGovernance
}

func (m *Market) canSubmitMaybeSell(party string, side types.Side) bool {
// buy side
// or network party
// or no empty allowedSellers list
// are always fine
if len(m.allowedSellers) <= 0 || side == types.SideBuy || party == types.NetworkParty {
return true
}

_, isAllowed := m.allowedSellers[party]
return isAllowed
}

// cleanupOnReject removes all resources created while the market was on PREPARED state.
// at this point no fees would have been collected or anything like this.
func (m *Market) cleanupOnReject(ctx context.Context) {
Expand Down
5 changes: 5 additions & 0 deletions core/execution/spot/market_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ func NewMarketFromSnapshot(
}

now := timeService.GetTimeNow()
allowedSellers := map[string]struct{}{}
for _, v := range mkt.AllowedSellers {
allowedSellers[v] = struct{}{}
}
market := &Market{
log: log,
mkt: mkt,
Expand Down Expand Up @@ -190,6 +194,7 @@ func NewMarketFromSnapshot(
hasTraded: em.HasTraded,
orderHoldingTracker: NewHoldingAccountTracker(mkt.ID, log, collateralEngine),
banking: banking,
allowedSellers: allowedSellers,
}
liquidity.SetGetStaticPricesFunc(market.getBestStaticPricesDecimal)
for _, p := range em.Parties {
Expand Down
61 changes: 61 additions & 0 deletions core/execution/spot/market_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"code.vegaprotocol.io/vega/core/types"
"code.vegaprotocol.io/vega/libs/crypto"
"code.vegaprotocol.io/vega/libs/num"
"code.vegaprotocol.io/vega/libs/ptr"
"code.vegaprotocol.io/vega/logging"
"code.vegaprotocol.io/vega/protos/vega"

Expand Down Expand Up @@ -182,6 +183,17 @@ func newTestMarket(
pMonitorSettings *types.PriceMonitoringSettings,
openingAuctionDuration *types.AuctionDuration,
now time.Time,
) *testMarket {
t.Helper()
return newTestMarketWithAllowedSellers(t, pMonitorSettings, openingAuctionDuration, now, nil)
}

func newTestMarketWithAllowedSellers(
t *testing.T,
pMonitorSettings *types.PriceMonitoringSettings,
openingAuctionDuration *types.AuctionDuration,
now time.Time,
allowedSellers []string,
) *testMarket {
t.Helper()
base := "BTC"
Expand All @@ -202,6 +214,7 @@ func newTestMarket(

statevarEngine := stubs.NewStateVar()
mkt := getMarketWithDP(base, quote, pMonitorSettings, openingAuctionDuration, quoteDP, positionDP)
mkt.AllowedSellers = allowedSellers

as := monitor.NewAuctionState(&mkt, now)
epoch := mocks.NewMockEpochEngine(ctrl)
Expand Down Expand Up @@ -302,3 +315,51 @@ func getGTCLimitOrder(tm *testMarket,
}
return order
}

//nolint:unparam
func getStopOrderSubmission(tm *testMarket,
now time.Time,
id string,
side1 types.Side,
side2 types.Side,
partyID string,
size uint64,
price uint64,
) *types.StopOrdersSubmission {
return &types.StopOrdersSubmission{
RisesAbove: &types.StopOrderSetup{
OrderSubmission: &types.OrderSubmission{
Type: types.OrderTypeLimit,
TimeInForce: types.OrderTimeInForceGTC,
Side: side1,
MarketID: tm.market.GetID(),
Size: size,
Price: num.NewUint(price),
Reference: "marketorder",
},
Expiry: &types.StopOrderExpiry{
ExpiryStrategy: ptr.From(types.StopOrderExpiryStrategyCancels),
},
Trigger: types.NewTrailingStopOrderTrigger(types.StopOrderTriggerDirectionRisesAbove, num.DecimalFromFloat(0.9)),
SizeOverrideSetting: types.StopOrderSizeOverrideSettingNone,
SizeOverrideValue: nil,
},
FallsBelow: &types.StopOrderSetup{
OrderSubmission: &types.OrderSubmission{
Type: types.OrderTypeLimit,
TimeInForce: types.OrderTimeInForceGTC,
Side: side2,
MarketID: tm.market.GetID(),
Size: size,
Price: num.NewUint(price),
Reference: "marketorder",
},
Expiry: &types.StopOrderExpiry{
ExpiryStrategy: ptr.From(types.StopOrderExpiryStrategyCancels),
},
Trigger: types.NewTrailingStopOrderTrigger(types.StopOrderTriggerDirectionRisesAbove, num.DecimalFromFloat(0.9)),
SizeOverrideSetting: types.StopOrderSizeOverrideSettingNone,
SizeOverrideValue: nil,
},
}
}
96 changes: 96 additions & 0 deletions core/execution/spot/spot_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"time"

"code.vegaprotocol.io/vega/core/execution/common"
"code.vegaprotocol.io/vega/core/idgeneration"
"code.vegaprotocol.io/vega/core/types"
vegacontext "code.vegaprotocol.io/vega/libs/context"
"code.vegaprotocol.io/vega/libs/crypto"
"code.vegaprotocol.io/vega/libs/num"
"code.vegaprotocol.io/vega/libs/ptr"

"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -164,6 +166,100 @@ func TestAmend(t *testing.T) {
require.Equal(t, "60000", haBalance1.Balance.String())
}

func TestMarketWithAllowedSellers(t *testing.T) {
now := time.Now()
ctx := context.Background()
ctx = vegacontext.WithTraceID(ctx, crypto.RandomHash())
tm := newTestMarketWithAllowedSellers(t, defaultPriceMonitorSettings, &types.AuctionDuration{Duration: 1}, now, []string{"party1", "party2"})

addAccountWithAmount(tm, "party1", 100000, "ETH")
addAccountWithAmount(tm, "party2", 100000, "ETH")
addAccountWithAmount(tm, "party3", 100000, "ETH")
addAccountWithAmount(tm, "party1", 100000, "BTC")
addAccountWithAmount(tm, "party2", 100000, "BTC")
addAccountWithAmount(tm, "party3", 100000, "BTC")
tm.market.StartOpeningAuction(ctx)

t.Run("allowed seller can post sell orders", func(t *testing.T) {
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party1", 1, 300)
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
require.NoError(t, err)
order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party2", 1, 300)
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
require.NoError(t, err)
})

t.Run("allowed seller can post buy orders", func(t *testing.T) {
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party1", 2, 200)
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
require.NoError(t, err)
order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party2", 2, 200)
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
require.NoError(t, err)
})

t.Run("non allowed seller cannot post sell orders", func(t *testing.T) {
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party3", 2, 300)
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
require.EqualError(t, err, "sell order not allowed")
})

t.Run("non allowed seller can post buy orders", func(t *testing.T) {
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party3", 2, 200)
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
require.NoError(t, err)
})

t.Run("exit auction", func(t *testing.T) {
order1 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideBuy, "party1", 2, 30000)
_, err := tm.market.SubmitOrder(ctx, order1.IntoSubmission(), order1.Party, crypto.RandomHash())
require.NoError(t, err)

order2 := getGTCLimitOrder(tm, now, crypto.RandomHash(), types.SideSell, "party2", 1, 30000)
_, err = tm.market.SubmitOrder(ctx, order2.IntoSubmission(), order2.Party, crypto.RandomHash())
require.NoError(t, err)

tm.market.OnTick(ctx, now.Add(2*time.Second))
md := tm.market.GetMarketData()
require.Equal(t, types.MarketTradingModeContinuous, md.MarketTradingMode)
})

t.Run("increase max stop orders per parties", func(t *testing.T) {
tm.market.OnMarketPartiesMaximumStopOrdersUpdate(
context.Background(), num.NewUint(1000))
})

t.Run("allowed seller can post sell stop orders", func(t *testing.T) {
idgen := idgeneration.New(crypto.RandomHash())
order1 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party1", 1, 300)
_, err := tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order1, "party1", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.NoError(t, err)

order2 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party2", 1, 300)
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order2, "party2", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.NoError(t, err)

order3 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party1", 1, 300)
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order3, "party1", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.NoError(t, err)

order4 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party2", 1, 300)
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order4, "party2", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.NoError(t, err)
})

t.Run("non allowed seller cannot post sell stop orders", func(t *testing.T) {
idgen := idgeneration.New(crypto.RandomHash())
order1 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideSell, types.SideBuy, "party3", 1, 300)
_, err := tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order1, "party3", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.EqualError(t, err, "sell order not allowed")

order2 := getStopOrderSubmission(tm, now, crypto.RandomHash(), types.SideBuy, types.SideSell, "party3", 1, 300)
_, err = tm.market.SubmitStopOrdersWithIDGeneratorAndOrderIDs(ctx, order2, "party3", idgen, ptr.From(idgen.NextID()), ptr.From(idgen.NextID()))
require.EqualError(t, err, "sell order not allowed")
})
}

func TestCancelAll(t *testing.T) {
now := time.Now()
ctx := context.Background()
Expand Down
2 changes: 2 additions & 0 deletions core/governance/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,7 @@ func (e *Engine) updatedSpotMarketFromProposal(p *proposal) (*types.Market, type
TickSize: terms.Changes.TickSize,
LiquidityFeeSettings: terms.Changes.LiquidityFeeSettings,
EnableTxReordering: terms.Changes.EnableTxReordering,
AllowedSellers: append([]string{}, terms.Changes.AllowedSellers...),
},
}

Expand Down Expand Up @@ -1339,6 +1340,7 @@ func (e *Engine) updatedMarketFromProposal(p *proposal) (*types.Market, types.Pr
TickSize: terms.Changes.TickSize,
EnableTxReordering: terms.Changes.EnableTxReordering,
AllowedEmptyAmmLevels: &allowedEmptyAMMLevels,
AllowedSellers: append([]string{}, terms.Changes.AllowedSellers...),
},
}

Expand Down
1 change: 1 addition & 0 deletions core/governance/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ func buildSpotMarketFromProposal(
MarkPriceConfiguration: defaultMarkPriceConfig,
TickSize: definition.Changes.TickSize,
EnableTxReordering: definition.Changes.EnableTxReordering,
AllowedSellers: append([]string{}, definition.Changes.AllowedSellers...),
}
if err := assignSpotRiskModel(definition.Changes, market.TradableInstrument); err != nil {
return nil, types.ProposalErrorUnspecified, err
Expand Down
3 changes: 3 additions & 0 deletions core/types/governance_new_market.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ type NewMarketConfiguration struct {
TickSize *num.Uint
EnableTxReordering bool
AllowedEmptyAmmLevels *uint64
AllowedSellers []string
}

func (n NewMarketConfiguration) IntoProto() *vegapb.NewMarketConfiguration {
Expand Down Expand Up @@ -486,11 +487,13 @@ func (n NewMarketConfiguration) DeepClone() *NewMarketConfiguration {
TickSize: n.TickSize.Clone(),
EnableTxReordering: n.EnableTxReordering,
AllowedEmptyAmmLevels: n.AllowedEmptyAmmLevels,
AllowedSellers: append([]string{}, n.AllowedSellers...),
}
cpy.Metadata = append(cpy.Metadata, n.Metadata...)
if n.Instrument != nil {
cpy.Instrument = n.Instrument.DeepClone()
}
cpy.AllowedSellers = append(cpy.AllowedSellers, n.AllowedSellers...)
if n.PriceMonitoringParameters != nil {
cpy.PriceMonitoringParameters = n.PriceMonitoringParameters.DeepClone()
}
Expand Down
Loading

0 comments on commit 260cc2e

Please sign in to comment.