code.vegaprotocol.io/vega@v0.79.0/core/governance/engine_new_market_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package governance_test
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/datasource"
    25  	dstypes "code.vegaprotocol.io/vega/core/datasource/common"
    26  	dsdefinition "code.vegaprotocol.io/vega/core/datasource/definition"
    27  	dserrors "code.vegaprotocol.io/vega/core/datasource/errors"
    28  	"code.vegaprotocol.io/vega/core/datasource/external/signedoracle"
    29  	"code.vegaprotocol.io/vega/core/events"
    30  	"code.vegaprotocol.io/vega/core/governance"
    31  	"code.vegaprotocol.io/vega/core/types"
    32  	"code.vegaprotocol.io/vega/libs/num"
    33  	"code.vegaprotocol.io/vega/libs/ptr"
    34  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    35  	datapb "code.vegaprotocol.io/vega/protos/vega/data/v1"
    36  
    37  	"github.com/golang/mock/gomock"
    38  	"github.com/pkg/errors"
    39  	"github.com/stretchr/testify/assert"
    40  	"github.com/stretchr/testify/require"
    41  )
    42  
    43  func TestProposalForNewMarket(t *testing.T) {
    44  	t.Run("Submitting a proposal for new market succeeds", testSubmittingProposalForNewMarketSucceeds)
    45  	t.Run("Submitting a proposal for new capped market succeeds", testSubmittingProposalForNewCappedMarketSucceeds)
    46  	t.Run("Submitting a proposal for invalid capped market fails", testSubmittingProposalForInvalidCappedMarketFails)
    47  	t.Run("Submitting a proposal for new perps market succeeds", testSubmittingProposalForNewPerpsMarketSucceeds)
    48  	t.Run("Submitting a proposal for new perps market succeeds 2", testSubmittingProposalForNewPerpsMarketWithCustomInitialTimeSucceeds)
    49  	t.Run("Submitting a proposal for new perps market with initial time in past fails", testSubmittingProposalForNewPerpsMarketWithPastInitialTimeFails)
    50  	t.Run("Submitting a proposal with internal time termination for new market succeeds", testSubmittingProposalWithInternalTimeTerminationForNewMarketSucceeds)
    51  	t.Run("Submitting a proposal with internal time termination with `less than equal` condition fails", testSubmittingProposalWithInternalTimeTerminationWithLessThanEqualConditionForNewMarketFails)
    52  	t.Run("Submitting a proposal with internal time settling for new market fails", testSubmittingProposalWithInternalTimeSettlingForNewMarketFails)
    53  	t.Run("Submitting a proposal with empty settling data for marker market fails", testSubmittingProposalWithEmptySettlingDataForNewMarketFails)
    54  	t.Run("Submitting a proposal with empty termination data for marker market fails", testSubmittingProposalWithEmptyTerminationDataForNewMarketFails)
    55  	t.Run("Submitting a proposal with external source using internal time termination key for new market succeeds", testSubmittingProposalWithExternalWithInternalTimeTerminationKeyForNewMarketSucceeds)
    56  	t.Run("Submitting a proposal with using internal time trigger termination fails", testSubmittingProposalWithInternalTimeTriggerTerminationFails)
    57  	t.Run("Submitting a proposal with using internal time trigger settlement fails", testSubmittingProposalWithInternalTimeTriggerSettlementFails)
    58  	t.Run("Submitting a duplicated proposal for new market fails", testSubmittingDuplicatedProposalForNewMarketFails)
    59  	t.Run("Submitting a duplicated proposal with internal time termination for new market fails", testSubmittingDuplicatedProposalWithInternalTimeTerminationForNewMarketFails)
    60  	t.Run("Submitting a proposal for new market with bad risk parameter fails", testSubmittingProposalForNewMarketWithBadRiskParameterFails)
    61  	t.Run("Submitting a proposal for new market with internal time termination with bad risk parameter fails", testSubmittingProposalForNewMarketWithInternalTimeTerminationWithBadRiskParameterFails)
    62  	t.Run("Submitting a proposal for a ne market without disposal slippage range fails", testSubmittingProposalWithoutDisposalSlippageFails)
    63  
    64  	t.Run("Rejecting a proposal for new market succeeds", testRejectingProposalForNewMarketSucceeds)
    65  
    66  	t.Run("Voting for a new market proposal succeeds", testVotingForNewMarketProposalSucceeds)
    67  	t.Run("Voting with a majority of 'yes' makes the new market proposal passed", testVotingWithMajorityOfYesMakesNewMarketProposalPassed)
    68  	t.Run("Voting with a majority of 'no' makes the new market proposal declined", testVotingWithMajorityOfNoMakesNewMarketProposalDeclined)
    69  	t.Run("Voting with insufficient participation makes the new market proposal declined", testVotingWithInsufficientParticipationMakesNewMarketProposalDeclined)
    70  	t.Run("Invalid combination of decimals for market", testInvalidDecimalPlace)
    71  }
    72  
    73  func TestProposalForSuccessorMarket(t *testing.T) {
    74  	t.Run("Submitting a proposal for fully defined successor market succeeds", testSubmittingProposalForFullSuccessorMarketSucceeds)
    75  
    76  	t.Run("Reject successor markets with an invalid insurance pool fraction", testRejectSuccessorInvalidInsurancePoolFraction)
    77  	t.Run("Reject successor market proposal if the product is incompatible", testRejectSuccessorProductMismatch)
    78  	t.Run("Reject successor market if the parent market does not exist", testRejectSuccessorNoParent)
    79  
    80  	t.Run("Remove proposals for an already succeeded market", testRemoveSuccessorsForSucceeded)
    81  	t.Run("Remove proposals for an already succeeded market on tick", testRemoveSuccessorsForRejectedMarket)
    82  }
    83  
    84  func testSubmittingProposalForInvalidCappedMarketFails(t *testing.T) {
    85  	eng := getTestEngine(t, time.Now())
    86  
    87  	// given
    88  	party := eng.newValidParty("a-valid-party", 123456789)
    89  	proposal := eng.newProposalForCapped(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &types.FutureCap{
    90  		MaxPrice:            num.UintZero(),
    91  		Binary:              true,
    92  		FullyCollateralised: true,
    93  	})
    94  
    95  	// setup
    96  	eng.ensureAllAssetEnabled(t)
    97  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
    98  
    99  	// when
   100  	toSubmit, err := eng.submitProposal(t, proposal)
   101  
   102  	// then
   103  	require.Error(t, err)
   104  	require.Nil(t, toSubmit)
   105  
   106  	// another failed scenario is when the max price doesn't respect the tick size
   107  	proposal = eng.newProposalForCapped(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &types.FutureCap{
   108  		MaxPrice:            num.NewUint(1234),
   109  		Binary:              true,
   110  		FullyCollateralised: true,
   111  	})
   112  	nmp := proposal.Terms.GetNewMarket()
   113  	nmp.Changes.TickSize = num.NewUint(10)
   114  
   115  	// setup
   116  	eng.ensureAllAssetEnabled(t)
   117  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   118  
   119  	// when
   120  	toSubmit, err = eng.submitProposal(t, proposal)
   121  
   122  	// then
   123  	require.Error(t, err)
   124  	require.Nil(t, toSubmit)
   125  }
   126  
   127  func testSubmittingProposalForNewCappedMarketSucceeds(t *testing.T) {
   128  	eng := getTestEngine(t, time.Now())
   129  
   130  	// given
   131  	party := eng.newValidParty("a-valid-party", 123456789)
   132  	proposal := eng.newProposalForCapped(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &types.FutureCap{
   133  		MaxPrice:            num.NewUint(1000),
   134  		Binary:              true,
   135  		FullyCollateralised: true,
   136  	})
   137  
   138  	// setup
   139  	eng.ensureAllAssetEnabled(t)
   140  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   141  
   142  	// when
   143  	toSubmit, err := eng.submitProposal(t, proposal)
   144  
   145  	// then
   146  	require.NoError(t, err)
   147  	require.NotNil(t, toSubmit)
   148  	assert.True(t, toSubmit.IsNewMarket())
   149  	require.NotNil(t, toSubmit.NewMarket().Market())
   150  
   151  	// same as above, but with a tick size that is non-zero
   152  	proposal = eng.newProposalForCapped(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &types.FutureCap{
   153  		MaxPrice:            num.NewUint(1000),
   154  		Binary:              true,
   155  		FullyCollateralised: true,
   156  	})
   157  	nmp := proposal.Terms.GetNewMarket()
   158  	nmp.Changes.TickSize = num.NewUint(10)
   159  
   160  	// setup
   161  	eng.ensureAllAssetEnabled(t)
   162  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   163  
   164  	// when
   165  	toSubmit, err = eng.submitProposal(t, proposal)
   166  
   167  	// then
   168  	require.NoError(t, err)
   169  	require.NotNil(t, toSubmit)
   170  	assert.True(t, toSubmit.IsNewMarket())
   171  	require.NotNil(t, toSubmit.NewMarket().Market())
   172  }
   173  
   174  func testSubmittingProposalForNewMarketSucceeds(t *testing.T) {
   175  	eng := getTestEngine(t, time.Now())
   176  
   177  	// given
   178  	party := eng.newValidParty("a-valid-party", 123456789)
   179  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
   180  
   181  	// setup
   182  	eng.ensureAllAssetEnabled(t)
   183  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   184  
   185  	// when
   186  	toSubmit, err := eng.submitProposal(t, proposal)
   187  
   188  	// then
   189  	require.NoError(t, err)
   190  	require.NotNil(t, toSubmit)
   191  	assert.True(t, toSubmit.IsNewMarket())
   192  	require.NotNil(t, toSubmit.NewMarket().Market())
   193  }
   194  
   195  func testRemoveSuccessorsForRejectedMarket(t *testing.T) {
   196  	eng := getTestEngine(t, time.Now())
   197  
   198  	// given
   199  	party := eng.newValidParty("a-valid-party", 123456789)
   200  	suc := types.SuccessorConfig{
   201  		ParentID:              "parentID",
   202  		InsurancePoolFraction: num.DecimalFromFloat(.5),
   203  	}
   204  	// add 3 proposals for the same parent
   205  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   206  	eng.markets.EXPECT().IsSucceeded(suc.ParentID).Times(3).Return(false)
   207  	filter, binding := produceTimeTriggeredDataSourceSpec(now.Add(3 * 48 * time.Hour))
   208  	enact := now.Add(24 * time.Hour)
   209  	proposals := []types.Proposal{
   210  		eng.newProposalForSuccessorMarket(party.Id, enact, filter, binding, true, &suc),
   211  		eng.newProposalForSuccessorMarket(party.Id, enact, filter, binding, true, &suc),
   212  		eng.newProposalForNewMarket(party.Id, enact, filter, binding, true), // non successor just because
   213  		eng.newProposalForSuccessorMarket(party.Id, enact, filter, binding, true, &suc),
   214  	}
   215  	first := proposals[0]
   216  	pFuture := first.NewMarket().Changes.GetFuture()
   217  	eng.ensureAllAssetEnabled(t)
   218  	for _, p := range proposals {
   219  		eng.expectOpenProposalEvent(t, party.Id, p.ID)
   220  	}
   221  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(6).Return(
   222  		types.Market{
   223  			TradableInstrument: &types.TradableInstrument{
   224  				Instrument: &types.Instrument{
   225  					Product: &types.InstrumentFuture{
   226  						Future: &types.Future{
   227  							SettlementAsset: pFuture.Future.SettlementAsset,
   228  							QuoteName:       pFuture.Future.SettlementAsset,
   229  						},
   230  					},
   231  				},
   232  			},
   233  		}, true)
   234  
   235  	// submit all proposals
   236  	for _, p := range proposals {
   237  		toSubmit, err := eng.submitProposal(t, p)
   238  
   239  		// then
   240  		require.NoError(t, err)
   241  		require.NotNil(t, toSubmit)
   242  		assert.True(t, toSubmit.IsNewMarket())
   243  		require.NotNil(t, toSubmit.NewMarket().Market())
   244  	}
   245  	// all proposals will be in the active proposals slice, so let's make sure all of them are removed
   246  	for _, p := range proposals {
   247  		if p.IsSuccessorMarket() {
   248  			eng.markets.EXPECT().GetMarketState(p.ID).Times(1).Return(types.MarketStateRejected, errors.New("foo"))
   249  		}
   250  	}
   251  	expState := types.ProposalStateRejected
   252  	expError := types.ProposalErrorInvalidSuccessorMarket
   253  	eng.broker.EXPECT().Send(gomock.Any()).AnyTimes().Do(func(evt events.Event) {
   254  		pe, ok := evt.(*events.Proposal)
   255  		require.True(t, ok)
   256  		prop := pe.Proposal()
   257  		require.Equal(t, expState, prop.State)
   258  		require.NotNil(t, prop.Reason)
   259  		require.EqualValues(t, expError, *prop.Reason)
   260  	})
   261  	eng.OnTick(context.Background(), now.Add(time.Second))
   262  }
   263  
   264  func testRemoveSuccessorsForSucceeded(t *testing.T) {
   265  	eng := getTestEngine(t, time.Now())
   266  
   267  	// given
   268  	party := eng.newValidParty("a-valid-party", 123456789)
   269  	suc := types.SuccessorConfig{
   270  		ParentID:              "parentID",
   271  		InsurancePoolFraction: num.DecimalFromFloat(.5),
   272  	}
   273  	// add 3 proposals for the same parent
   274  	eng.markets.EXPECT().IsSucceeded(suc.ParentID).Times(3).Return(false)
   275  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   276  	filter, binding := produceTimeTriggeredDataSourceSpec(now.Add(3 * 48 * time.Hour))
   277  	proposals := []types.Proposal{
   278  		eng.newProposalForSuccessorMarket(party.Id, now, filter, binding, true, &suc),
   279  		eng.newProposalForSuccessorMarket(party.Id, now, filter, binding, true, &suc),
   280  		eng.newProposalForNewMarket(party.Id, now, filter, binding, true), // non successor just because
   281  		eng.newProposalForSuccessorMarket(party.Id, now, filter, binding, true, &suc),
   282  	}
   283  	first := proposals[0]
   284  	pFuture := first.NewMarket().Changes.GetFuture()
   285  	eng.ensureAllAssetEnabled(t)
   286  	for _, p := range proposals {
   287  		eng.expectOpenProposalEvent(t, party.Id, p.ID)
   288  	}
   289  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(6).Return(
   290  		types.Market{
   291  			TradableInstrument: &types.TradableInstrument{
   292  				Instrument: &types.Instrument{
   293  					Product: &types.InstrumentFuture{
   294  						Future: &types.Future{
   295  							SettlementAsset: pFuture.Future.SettlementAsset,
   296  							QuoteName:       pFuture.Future.SettlementAsset,
   297  						},
   298  					},
   299  				},
   300  			},
   301  		}, true)
   302  
   303  	// submit all proposals
   304  	for _, p := range proposals {
   305  		toSubmit, err := eng.submitProposal(t, p)
   306  
   307  		// then
   308  		require.NoError(t, err)
   309  		require.NotNil(t, toSubmit)
   310  		assert.True(t, toSubmit.IsNewMarket())
   311  		require.NotNil(t, toSubmit.NewMarket().Market())
   312  	}
   313  	// all proposals will be in the active proposals slice, so let's make sure all of them are removed
   314  	first.State = types.ProposalStateEnacted
   315  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
   316  	eng.FinaliseEnactment(context.Background(), &first)
   317  }
   318  
   319  func testSubmittingProposalForFullSuccessorMarketSucceeds(t *testing.T) {
   320  	eng := getTestEngine(t, time.Now())
   321  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   322  
   323  	// given
   324  	party := eng.newValidParty("a-valid-party", 123456789)
   325  	suc := types.SuccessorConfig{
   326  		ParentID:              "parentID",
   327  		InsurancePoolFraction: num.DecimalFromFloat(.5),
   328  	}
   329  	eng.markets.EXPECT().IsSucceeded(suc.ParentID).Times(1).Return(false)
   330  	filter, binding := produceTimeTriggeredDataSourceSpec(now.Add(3 * 48 * time.Hour))
   331  	proposal := eng.newProposalForSuccessorMarket(party.Id, now, filter, binding, true, &suc)
   332  	// returns a pointer directly to the change, but reassign just in case it doesn't
   333  	nm := proposal.NewMarket()
   334  	// ensure price monitoring params are set
   335  	if nm.Changes.PriceMonitoringParameters == nil {
   336  		nm.Changes.PriceMonitoringParameters = &types.PriceMonitoringParameters{
   337  			Triggers: []*types.PriceMonitoringTrigger{
   338  				{
   339  					Horizon:          5,
   340  					HorizonDec:       num.DecimalFromFloat(5),
   341  					Probability:      num.DecimalFromFloat(.95),
   342  					AuctionExtension: 1,
   343  				},
   344  			},
   345  		}
   346  	}
   347  	// ensure risk model params are set
   348  	if nm.Changes.RiskParameters == nil {
   349  		nm.Changes.RiskParameters = &types.NewMarketConfigurationSimple{
   350  			Simple: &types.SimpleModelParams{},
   351  		}
   352  	}
   353  	proposal.Terms.Change = &types.ProposalTermsNewMarket{
   354  		NewMarket: nm,
   355  	}
   356  
   357  	// setup
   358  	eng.ensureAllAssetEnabled(t)
   359  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   360  	// GetMarket will be called in validateChange & intoSubmit
   361  	pFuture := proposal.NewMarket().Changes.GetFuture()
   362  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(2).Return(
   363  		types.Market{
   364  			TradableInstrument: &types.TradableInstrument{
   365  				Instrument: &types.Instrument{
   366  					Product: &types.InstrumentFuture{
   367  						Future: &types.Future{
   368  							SettlementAsset: pFuture.Future.SettlementAsset,
   369  							QuoteName:       pFuture.Future.SettlementAsset,
   370  						},
   371  					},
   372  				},
   373  			},
   374  		}, true)
   375  
   376  	// when
   377  	toSubmit, err := eng.submitProposal(t, proposal)
   378  
   379  	// then
   380  	require.NoError(t, err)
   381  	require.NotNil(t, toSubmit)
   382  	assert.True(t, toSubmit.IsNewMarket())
   383  	require.NotNil(t, toSubmit.NewMarket().Market())
   384  }
   385  
   386  func testRejectSuccessorInvalidInsurancePoolFraction(t *testing.T) {
   387  	eng := getTestEngine(t, time.Now())
   388  
   389  	// given
   390  	party := eng.newValidParty("a-valid-party", 123456789)
   391  	suc := types.SuccessorConfig{
   392  		ParentID:              "parentID",
   393  		InsurancePoolFraction: num.DecimalFromFloat(5), // out of range 0-1
   394  	}
   395  	proposal := eng.newProposalForSuccessorMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &suc)
   396  
   397  	// setup
   398  	eng.ensureAllAssetEnabled(t)
   399  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidSuccessorMarket)
   400  	// GetMarket will only be called once, the second call will never happen due to the insurance pool fraction being invalid
   401  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(1).Return(types.Market{}, true) // market can be empty, we won't access the settlement/quote stuff
   402  
   403  	// when
   404  	toSubmit, err := eng.submitProposal(t, proposal)
   405  
   406  	// then
   407  	require.Error(t, err)
   408  	require.Nil(t, toSubmit)
   409  }
   410  
   411  func testRejectSuccessorProductMismatch(t *testing.T) {
   412  	eng := getTestEngine(t, time.Now())
   413  
   414  	// given
   415  	party := eng.newValidParty("a-valid-party", 123456789)
   416  	suc := types.SuccessorConfig{
   417  		ParentID:              "parentID",
   418  		InsurancePoolFraction: num.DecimalFromFloat(0),
   419  	}
   420  	proposal := eng.newProposalForSuccessorMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, false, &suc)
   421  
   422  	// setup
   423  	eng.ensureAllAssetEnabled(t)
   424  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidSuccessorMarket)
   425  	// GetMarket will only be called once, the second call will never happen due to the product mismatch
   426  	fProduct := proposal.NewMarket().Changes.GetFuture()
   427  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(1).Return(
   428  		types.Market{
   429  			TradableInstrument: &types.TradableInstrument{
   430  				Instrument: &types.Instrument{
   431  					Product: &types.InstrumentFuture{
   432  						Future: &types.Future{
   433  							SettlementAsset: fmt.Sprintf("not%s", fProduct.Future.SettlementAsset),
   434  							QuoteName:       fmt.Sprintf("not%s", fProduct.Future.QuoteName),
   435  						},
   436  					},
   437  				},
   438  			},
   439  		}, true)
   440  
   441  	// when
   442  	toSubmit, err := eng.submitProposal(t, proposal)
   443  
   444  	// then
   445  	require.Error(t, err)
   446  	require.Nil(t, toSubmit)
   447  }
   448  
   449  func testRejectSuccessorNoParent(t *testing.T) {
   450  	eng := getTestEngine(t, time.Now())
   451  
   452  	// given
   453  	party := eng.newValidParty("a-valid-party", 123456789)
   454  	suc := types.SuccessorConfig{
   455  		ParentID:              "parentID",
   456  		InsurancePoolFraction: num.DecimalFromFloat(0),
   457  	}
   458  	proposal := eng.newProposalForSuccessorMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true, &suc)
   459  
   460  	// setup
   461  	eng.ensureAllAssetEnabled(t)
   462  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidSuccessorMarket)
   463  	// only called once, validateChange already flags this error (missing parent)
   464  	eng.markets.EXPECT().GetMarket(suc.ParentID, true).Times(1).Return(types.Market{}, false)
   465  
   466  	// when
   467  	toSubmit, err := eng.submitProposal(t, proposal)
   468  
   469  	// then
   470  	require.Error(t, err)
   471  	require.Nil(t, toSubmit)
   472  }
   473  
   474  func testSubmittingProposalWithInternalTimeTerminationForNewMarketSucceeds(t *testing.T) {
   475  	eng := getTestEngine(t, time.Now())
   476  
   477  	// given
   478  	party := eng.newValidParty("a-valid-party", 123456789)
   479  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, false)
   480  
   481  	// setup
   482  	eng.ensureAllAssetEnabled(t)
   483  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   484  
   485  	// when
   486  	toSubmit, err := eng.submitProposal(t, proposal)
   487  
   488  	// then
   489  	require.NoError(t, err)
   490  	require.NotNil(t, toSubmit)
   491  	assert.True(t, toSubmit.IsNewMarket())
   492  	require.NotNil(t, toSubmit.NewMarket().Market())
   493  }
   494  
   495  func testSubmittingProposalWithInternalTimeSettlingForNewMarketFails(t *testing.T) {
   496  	eng := getTestEngine(t, time.Now())
   497  
   498  	// given
   499  	party := eng.newValidParty("a-valid-party", 123456789)
   500  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   501  	id := eng.newProposalID()
   502  	tm := now.Add(time.Hour * 24 * 365)
   503  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
   504  
   505  	proposal := types.Proposal{
   506  		ID:        id,
   507  		Reference: "ref-" + id,
   508  		Party:     party.Id,
   509  		State:     types.ProposalStateOpen,
   510  		Terms: &types.ProposalTerms{
   511  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
   512  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
   513  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
   514  			Change: &types.ProposalTermsNewMarket{
   515  				NewMarket: &types.NewMarket{
   516  					Changes: &types.NewMarketConfiguration{
   517  						Instrument: &types.InstrumentConfiguration{
   518  							Name: "June 2020 GBP vs VUSD future",
   519  							Code: "CRYPTO:GBPVUSD/JUN20",
   520  							Product: &types.InstrumentConfigurationFuture{
   521  								Future: &types.FutureProduct{
   522  									SettlementAsset: "VUSD",
   523  									QuoteName:       "VUSD",
   524  									DataSourceSpecForSettlementData: *datasource.NewDefinition(
   525  										datasource.ContentTypeOracle,
   526  									).SetTimeTriggerConditionConfig(
   527  										[]*dstypes.SpecCondition{
   528  											{
   529  												Operator: datapb.Condition_OPERATOR_GREATER_THAN_OR_EQUAL,
   530  												Value:    "0",
   531  											},
   532  										},
   533  									),
   534  									DataSourceSpecForTradingTermination: *datasource.NewDefinition(
   535  										datasource.ContentTypeOracle,
   536  									).SetTimeTriggerConditionConfig(
   537  										[]*dstypes.SpecCondition{
   538  											{
   539  												Operator: datapb.Condition_OPERATOR_GREATER_THAN_OR_EQUAL,
   540  												Value:    fmt.Sprintf("%d", tm.UnixNano()),
   541  											},
   542  										}),
   543  									DataSourceSpecBinding: termBinding,
   544  								},
   545  							},
   546  						},
   547  						RiskParameters: &types.NewMarketConfigurationLogNormal{
   548  							LogNormal: &types.LogNormalRiskModel{
   549  								RiskAversionParameter: num.DecimalFromFloat(0.01),
   550  								Tau:                   num.DecimalFromFloat(0.00011407711613050422),
   551  								Params: &types.LogNormalModelParams{
   552  									Mu:    num.DecimalZero(),
   553  									R:     num.DecimalFromFloat(0.016),
   554  									Sigma: num.DecimalFromFloat(0.09),
   555  								},
   556  							},
   557  						},
   558  						Metadata:      []string{"asset_class:fx/crypto", "product:futures"},
   559  						DecimalPlaces: 0,
   560  						LiquiditySLAParameters: &types.LiquiditySLAParams{
   561  							PriceRange: num.DecimalFromFloat(0.95),
   562  						},
   563  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
   564  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
   565  						LiquidationStrategy: &types.LiquidationStrategy{
   566  							DisposalTimeStep:    10 * time.Second,
   567  							DisposalFraction:    num.DecimalFromFloat(0.1),
   568  							FullDisposalSize:    20,
   569  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
   570  							DisposalSlippage:    num.DecimalFromFloat(0.1),
   571  						},
   572  						TickSize: num.UintOne(),
   573  					},
   574  				},
   575  			},
   576  		},
   577  		Rationale: &types.ProposalRationale{
   578  			Description: "some description",
   579  		},
   580  	}
   581  
   582  	// setup
   583  	eng.ensureAllAssetEnabled(t)
   584  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   585  
   586  	// when
   587  	toSubmit, err := eng.submitProposal(t, proposal)
   588  
   589  	// then
   590  	assert.Error(t, err, governance.ErrSettlementWithInternalDataSourceIsNotAllowed)
   591  	require.Nil(t, toSubmit)
   592  }
   593  
   594  func testSubmittingProposalWithEmptySettlingDataForNewMarketFails(t *testing.T) {
   595  	eng := getTestEngine(t, time.Now())
   596  
   597  	// given
   598  	party := eng.newValidParty("a-valid-party", 123456789)
   599  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   600  	id := eng.newProposalID()
   601  	tm := now.Add(time.Hour * 24 * 365)
   602  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
   603  
   604  	proposal := types.Proposal{
   605  		ID:        id,
   606  		Reference: "ref-" + id,
   607  		Party:     party.Id,
   608  		State:     types.ProposalStateOpen,
   609  		Terms: &types.ProposalTerms{
   610  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
   611  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
   612  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
   613  			Change: &types.ProposalTermsNewMarket{
   614  				NewMarket: &types.NewMarket{
   615  					Changes: &types.NewMarketConfiguration{
   616  						Instrument: &types.InstrumentConfiguration{
   617  							Name: "June 2020 GBP vs VUSD future",
   618  							Code: "CRYPTO:GBPVUSD/JUN20",
   619  							Product: &types.InstrumentConfigurationFuture{
   620  								Future: &types.FutureProduct{
   621  									SettlementAsset:                     "VUSD",
   622  									QuoteName:                           "VUSD",
   623  									DataSourceSpecForSettlementData:     dsdefinition.Definition{},
   624  									DataSourceSpecForTradingTermination: dsdefinition.Definition{},
   625  									DataSourceSpecBinding:               termBinding,
   626  								},
   627  							},
   628  						},
   629  						RiskParameters: &types.NewMarketConfigurationLogNormal{
   630  							LogNormal: &types.LogNormalRiskModel{
   631  								RiskAversionParameter: num.DecimalFromFloat(0.01),
   632  								Tau:                   num.DecimalFromFloat(0.00011407711613050422),
   633  								Params: &types.LogNormalModelParams{
   634  									Mu:    num.DecimalZero(),
   635  									R:     num.DecimalFromFloat(0.016),
   636  									Sigma: num.DecimalFromFloat(0.09),
   637  								},
   638  							},
   639  						},
   640  						Metadata:      []string{"asset_class:fx/crypto", "product:futures"},
   641  						DecimalPlaces: 0,
   642  						LiquiditySLAParameters: &types.LiquiditySLAParams{
   643  							PriceRange: num.DecimalFromFloat(0.95),
   644  						},
   645  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
   646  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
   647  						LiquidationStrategy: &types.LiquidationStrategy{
   648  							DisposalTimeStep:    10 * time.Second,
   649  							DisposalFraction:    num.DecimalFromFloat(0.1),
   650  							FullDisposalSize:    20,
   651  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
   652  							DisposalSlippage:    num.DecimalFromFloat(0.1),
   653  						},
   654  						TickSize: num.UintOne(),
   655  					},
   656  				},
   657  			},
   658  		},
   659  		Rationale: &types.ProposalRationale{
   660  			Description: "some description",
   661  		},
   662  	}
   663  
   664  	// setup
   665  	eng.ensureAllAssetEnabled(t)
   666  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   667  
   668  	// when
   669  	toSubmit, err := eng.submitProposal(t, proposal)
   670  
   671  	// then
   672  	assert.Error(t, err, governance.ErrMissingDataSourceSpecForSettlementData)
   673  	require.Nil(t, toSubmit)
   674  }
   675  
   676  func testSubmittingProposalWithEmptyTerminationDataForNewMarketFails(t *testing.T) {
   677  	eng := getTestEngine(t, time.Now())
   678  
   679  	// given
   680  	party := eng.newValidParty("a-valid-party", 123456789)
   681  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   682  	id := eng.newProposalID()
   683  	tm := now.Add(time.Hour * 24 * 365)
   684  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
   685  
   686  	proposal := types.Proposal{
   687  		ID:        id,
   688  		Reference: "ref-" + id,
   689  		Party:     party.Id,
   690  		State:     types.ProposalStateOpen,
   691  		Terms: &types.ProposalTerms{
   692  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
   693  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
   694  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
   695  			Change: &types.ProposalTermsNewMarket{
   696  				NewMarket: &types.NewMarket{
   697  					Changes: &types.NewMarketConfiguration{
   698  						Instrument: &types.InstrumentConfiguration{
   699  							Name: "June 2020 GBP vs VUSD future",
   700  							Code: "CRYPTO:GBPVUSD/JUN20",
   701  							Product: &types.InstrumentConfigurationFuture{
   702  								Future: &types.FutureProduct{
   703  									SettlementAsset: "VUSD",
   704  									QuoteName:       "VUSD",
   705  									DataSourceSpecForSettlementData: *datasource.NewDefinition(
   706  										datasource.ContentTypeInternalTimeTermination,
   707  									).SetTimeTriggerConditionConfig(
   708  										[]*dstypes.SpecCondition{
   709  											{
   710  												Operator: datapb.Condition_OPERATOR_GREATER_THAN_OR_EQUAL,
   711  												Value:    "0",
   712  											},
   713  										},
   714  									),
   715  									DataSourceSpecForTradingTermination: dsdefinition.Definition{},
   716  									DataSourceSpecBinding:               termBinding,
   717  								},
   718  							},
   719  						},
   720  						RiskParameters: &types.NewMarketConfigurationLogNormal{
   721  							LogNormal: &types.LogNormalRiskModel{
   722  								RiskAversionParameter: num.DecimalFromFloat(0.01),
   723  								Tau:                   num.DecimalFromFloat(0.00011407711613050422),
   724  								Params: &types.LogNormalModelParams{
   725  									Mu:    num.DecimalZero(),
   726  									R:     num.DecimalFromFloat(0.016),
   727  									Sigma: num.DecimalFromFloat(0.09),
   728  								},
   729  							},
   730  						},
   731  						Metadata:      []string{"asset_class:fx/crypto", "product:futures"},
   732  						DecimalPlaces: 0,
   733  						LiquiditySLAParameters: &types.LiquiditySLAParams{
   734  							PriceRange: num.DecimalFromFloat(0.95),
   735  						},
   736  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
   737  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
   738  						LiquidationStrategy: &types.LiquidationStrategy{
   739  							DisposalTimeStep:    10 * time.Second,
   740  							DisposalFraction:    num.DecimalFromFloat(0.1),
   741  							FullDisposalSize:    20,
   742  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
   743  							DisposalSlippage:    num.DecimalFromFloat(0.1),
   744  						},
   745  						TickSize: num.UintOne(),
   746  					},
   747  				},
   748  			},
   749  		},
   750  		Rationale: &types.ProposalRationale{
   751  			Description: "some description",
   752  		},
   753  	}
   754  
   755  	// setup
   756  	eng.ensureAllAssetEnabled(t)
   757  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   758  
   759  	// when
   760  	toSubmit, err := eng.submitProposal(t, proposal)
   761  
   762  	// then
   763  	assert.Error(t, err, governance.ErrMissingDataSourceSpecForTradingTermination)
   764  	require.Nil(t, toSubmit)
   765  }
   766  
   767  func testSubmittingProposalWithInternalTimeTerminationWithLessThanEqualConditionForNewMarketFails(t *testing.T) {
   768  	eng := getTestEngine(t, time.Now())
   769  
   770  	// given
   771  	party := eng.newValidParty("a-valid-party", 123456789)
   772  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   773  	id := eng.newProposalID()
   774  	tm := now.Add(time.Hour * 24 * 365)
   775  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
   776  
   777  	settl := datasource.NewDefinition(
   778  		datasource.ContentTypeOracle,
   779  	).SetOracleConfig(
   780  		&signedoracle.SpecConfiguration{
   781  			Signers: []*dstypes.Signer{dstypes.CreateSignerFromString("0xDEADBEEF", dstypes.SignerTypePubKey)},
   782  			Filters: []*dstypes.SpecFilter{
   783  				{
   784  					Key: &dstypes.SpecPropertyKey{
   785  						Name: "prices.ETH.value",
   786  						Type: datapb.PropertyKey_TYPE_INTEGER,
   787  					},
   788  					Conditions: []*dstypes.SpecCondition{
   789  						{
   790  							Operator: datapb.Condition_OPERATOR_GREATER_THAN_OR_EQUAL,
   791  							Value:    "0",
   792  						},
   793  					},
   794  				},
   795  			},
   796  		},
   797  	)
   798  
   799  	term := datasource.NewDefinition(
   800  		datasource.ContentTypeOracle,
   801  	).SetTimeTriggerConditionConfig(
   802  		[]*dstypes.SpecCondition{
   803  			{
   804  				Operator: datapb.Condition_OPERATOR_LESS_THAN,
   805  				Value:    fmt.Sprintf("%d", tm.UnixNano()),
   806  			},
   807  		})
   808  
   809  	riskParameters := types.NewMarketConfigurationLogNormal{
   810  		LogNormal: &types.LogNormalRiskModel{
   811  			RiskAversionParameter: num.DecimalFromFloat(0.01),
   812  			Tau:                   num.DecimalFromFloat(0.00011407711613050422),
   813  			Params: &types.LogNormalModelParams{
   814  				Mu:    num.DecimalZero(),
   815  				R:     num.DecimalFromFloat(0.016),
   816  				Sigma: num.DecimalFromFloat(0.09),
   817  			},
   818  		},
   819  	}
   820  
   821  	proposal := types.Proposal{
   822  		ID:        id,
   823  		Reference: "ref-" + id,
   824  		Party:     party.Id,
   825  		State:     types.ProposalStateOpen,
   826  		Terms: &types.ProposalTerms{
   827  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
   828  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
   829  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
   830  			Change: &types.ProposalTermsNewMarket{
   831  				NewMarket: &types.NewMarket{
   832  					Changes: &types.NewMarketConfiguration{
   833  						Instrument: &types.InstrumentConfiguration{
   834  							Name: "June 2020 GBP vs VUSD future",
   835  							Code: "CRYPTO:GBPVUSD/JUN20",
   836  							Product: &types.InstrumentConfigurationFuture{
   837  								Future: &types.FutureProduct{
   838  									SettlementAsset:                     "VUSD",
   839  									QuoteName:                           "VUSD",
   840  									DataSourceSpecForSettlementData:     *settl,
   841  									DataSourceSpecForTradingTermination: *term,
   842  									DataSourceSpecBinding:               termBinding,
   843  								},
   844  							},
   845  						},
   846  						RiskParameters: &riskParameters,
   847  						Metadata:       []string{"asset_class:fx/crypto", "product:futures"},
   848  						DecimalPlaces:  0,
   849  						LiquiditySLAParameters: &types.LiquiditySLAParams{
   850  							PriceRange: num.DecimalFromFloat(0.95),
   851  						},
   852  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
   853  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
   854  						LiquidationStrategy: &types.LiquidationStrategy{
   855  							DisposalTimeStep:    10 * time.Second,
   856  							DisposalFraction:    num.DecimalFromFloat(0.1),
   857  							FullDisposalSize:    20,
   858  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
   859  							DisposalSlippage:    num.DecimalFromFloat(0.1),
   860  						},
   861  						TickSize: num.UintOne(),
   862  					},
   863  				},
   864  			},
   865  		},
   866  		Rationale: &types.ProposalRationale{
   867  			Description: "some description",
   868  		},
   869  	}
   870  
   871  	// setup
   872  	eng.ensureAllAssetEnabled(t)
   873  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   874  
   875  	// when
   876  	toSubmit, err := eng.submitProposal(t, proposal)
   877  
   878  	// then
   879  	assert.Error(t, err, dserrors.ErrDataSourceSpecHasInvalidTimeCondition)
   880  	require.Nil(t, toSubmit)
   881  
   882  	term = datasource.NewDefinition(
   883  		datasource.ContentTypeOracle,
   884  	).SetTimeTriggerConditionConfig(
   885  		[]*dstypes.SpecCondition{
   886  			{
   887  				Operator: datapb.Condition_OPERATOR_LESS_THAN_OR_EQUAL,
   888  				Value:    fmt.Sprintf("%d", tm.UnixNano()),
   889  			},
   890  		})
   891  
   892  	proposal = types.Proposal{
   893  		ID:        id,
   894  		Reference: "ref-" + id,
   895  		Party:     party.Id,
   896  		State:     types.ProposalStateOpen,
   897  		Terms: &types.ProposalTerms{
   898  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
   899  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
   900  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
   901  			Change: &types.ProposalTermsNewMarket{
   902  				NewMarket: &types.NewMarket{
   903  					Changes: &types.NewMarketConfiguration{
   904  						Instrument: &types.InstrumentConfiguration{
   905  							Name: "June 2020 GBP vs VUSD future",
   906  							Code: "CRYPTO:GBPVUSD/JUN20",
   907  							Product: &types.InstrumentConfigurationFuture{
   908  								Future: &types.FutureProduct{
   909  									SettlementAsset:                     "VUSD",
   910  									QuoteName:                           "VUSD",
   911  									DataSourceSpecForSettlementData:     *settl,
   912  									DataSourceSpecForTradingTermination: *term,
   913  									DataSourceSpecBinding:               termBinding,
   914  								},
   915  							},
   916  						},
   917  						RiskParameters: &riskParameters,
   918  						Metadata:       []string{"asset_class:fx/crypto", "product:futures"},
   919  						DecimalPlaces:  0,
   920  						LiquiditySLAParameters: &types.LiquiditySLAParams{
   921  							PriceRange: num.DecimalFromFloat(0.95),
   922  						},
   923  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
   924  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
   925  						LiquidationStrategy: &types.LiquidationStrategy{
   926  							DisposalTimeStep:    10 * time.Second,
   927  							DisposalFraction:    num.DecimalFromFloat(0.1),
   928  							FullDisposalSize:    20,
   929  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
   930  							DisposalSlippage:    num.DecimalFromFloat(0.1),
   931  						},
   932  						TickSize: num.UintOne(),
   933  					},
   934  				},
   935  			},
   936  		},
   937  		Rationale: &types.ProposalRationale{
   938  			Description: "some description",
   939  		},
   940  	}
   941  
   942  	// setup
   943  	eng.ensureAllAssetEnabled(t)
   944  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
   945  
   946  	// when
   947  	toSubmit, err = eng.submitProposal(t, proposal)
   948  
   949  	// then
   950  	assert.Error(t, err, dserrors.ErrDataSourceSpecHasInvalidTimeCondition)
   951  	require.Nil(t, toSubmit)
   952  }
   953  
   954  func testSubmittingProposalWithExternalWithInternalTimeTerminationKeyForNewMarketSucceeds(t *testing.T) {
   955  	eng := getTestEngine(t, time.Now())
   956  
   957  	// given
   958  	party := eng.newValidParty("a-valid-party", 123456789)
   959  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   960  	filter, binding := produceTimeTriggeredDataSourceSpec(now.Add(3 * 48 * time.Hour))
   961  	proposal := eng.newProposalForNewMarket(party.Id, now.Add(2*time.Hour), filter, binding, true)
   962  
   963  	// setup
   964  	eng.ensureAllAssetEnabled(t)
   965  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
   966  
   967  	// when
   968  	toSubmit, err := eng.submitProposal(t, proposal)
   969  
   970  	// then
   971  	require.NoError(t, err)
   972  	require.NotNil(t, toSubmit)
   973  	assert.True(t, toSubmit.IsNewMarket())
   974  	require.NotNil(t, toSubmit.NewMarket().Market())
   975  }
   976  
   977  func testSubmittingProposalWithInternalTimeTriggerTerminationFails(t *testing.T) {
   978  	eng := getTestEngine(t, time.Now())
   979  
   980  	// given
   981  	party := eng.newValidParty("a-valid-party", 123456789)
   982  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   983  	id := eng.newProposalID()
   984  	tm := now.Add(time.Hour * 24 * 365)
   985  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
   986  
   987  	settl := datasource.NewDefinition(
   988  		datasource.ContentTypeOracle,
   989  	).SetOracleConfig(
   990  		&signedoracle.SpecConfiguration{
   991  			Signers: []*dstypes.Signer{dstypes.CreateSignerFromString("0xDEADBEEF", dstypes.SignerTypePubKey)},
   992  			Filters: []*dstypes.SpecFilter{
   993  				{
   994  					Key: &dstypes.SpecPropertyKey{
   995  						Name: "prices.ETH.value",
   996  						Type: datapb.PropertyKey_TYPE_INTEGER,
   997  					},
   998  					Conditions: []*dstypes.SpecCondition{
   999  						{
  1000  							Operator: datapb.Condition_OPERATOR_GREATER_THAN_OR_EQUAL,
  1001  							Value:    "0",
  1002  						},
  1003  					},
  1004  				},
  1005  			},
  1006  		},
  1007  	)
  1008  
  1009  	term := datasource.NewDefinition(
  1010  		datasource.ContentTypeInternalTimeTriggerTermination,
  1011  	).SetTimeTriggerConditionConfig(
  1012  		[]*dstypes.SpecCondition{
  1013  			{
  1014  				Operator: datapb.Condition_OPERATOR_GREATER_THAN,
  1015  				Value:    fmt.Sprintf("%d", tm.UnixNano()),
  1016  			},
  1017  		})
  1018  
  1019  	riskParameters := types.NewMarketConfigurationLogNormal{
  1020  		LogNormal: &types.LogNormalRiskModel{
  1021  			RiskAversionParameter: num.DecimalFromFloat(0.01),
  1022  			Tau:                   num.DecimalFromFloat(0.00011407711613050422),
  1023  			Params: &types.LogNormalModelParams{
  1024  				Mu:    num.DecimalZero(),
  1025  				R:     num.DecimalFromFloat(0.016),
  1026  				Sigma: num.DecimalFromFloat(0.09),
  1027  			},
  1028  		},
  1029  	}
  1030  
  1031  	proposal := types.Proposal{
  1032  		ID:        id,
  1033  		Reference: "ref-" + id,
  1034  		Party:     party.Id,
  1035  		State:     types.ProposalStateOpen,
  1036  		Terms: &types.ProposalTerms{
  1037  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
  1038  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
  1039  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
  1040  			Change: &types.ProposalTermsNewMarket{
  1041  				NewMarket: &types.NewMarket{
  1042  					Changes: &types.NewMarketConfiguration{
  1043  						Instrument: &types.InstrumentConfiguration{
  1044  							Name: "June 2020 GBP vs VUSD future",
  1045  							Code: "CRYPTO:GBPVUSD/JUN20",
  1046  							Product: &types.InstrumentConfigurationFuture{
  1047  								Future: &types.FutureProduct{
  1048  									SettlementAsset:                     "VUSD",
  1049  									QuoteName:                           "VUSD",
  1050  									DataSourceSpecForSettlementData:     *settl,
  1051  									DataSourceSpecForTradingTermination: *term,
  1052  									DataSourceSpecBinding:               termBinding,
  1053  								},
  1054  							},
  1055  						},
  1056  						RiskParameters:          &riskParameters,
  1057  						Metadata:                []string{"asset_class:fx/crypto", "product:futures"},
  1058  						DecimalPlaces:           0,
  1059  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
  1060  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
  1061  						LiquidationStrategy: &types.LiquidationStrategy{
  1062  							DisposalTimeStep:    10 * time.Second,
  1063  							DisposalFraction:    num.DecimalFromFloat(0.1),
  1064  							FullDisposalSize:    20,
  1065  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
  1066  							DisposalSlippage:    num.DecimalFromFloat(0.1),
  1067  						},
  1068  						TickSize: num.UintOne(),
  1069  					},
  1070  				},
  1071  			},
  1072  		},
  1073  		Rationale: &types.ProposalRationale{
  1074  			Description: "some description",
  1075  		},
  1076  	}
  1077  
  1078  	// setup
  1079  	eng.ensureAllAssetEnabled(t)
  1080  	// expect
  1081  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
  1082  
  1083  	// when
  1084  	toSubmit, err := eng.submitProposal(t, proposal)
  1085  
  1086  	// then
  1087  	assert.Error(t, err, governance.ErrInternalTimeTriggerForFuturesInNotAllowed)
  1088  	require.Nil(t, toSubmit)
  1089  }
  1090  
  1091  func testSubmittingProposalWithInternalTimeTriggerSettlementFails(t *testing.T) {
  1092  	eng := getTestEngine(t, time.Now())
  1093  
  1094  	// given
  1095  	party := eng.newValidParty("a-valid-party", 123456789)
  1096  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
  1097  	id := eng.newProposalID()
  1098  	tm := now.Add(time.Hour * 24 * 365)
  1099  	_, termBinding := produceTimeTriggeredDataSourceSpec(tm)
  1100  
  1101  	settl := datasource.NewDefinition(
  1102  		datasource.ContentTypeInternalTimeTriggerTermination,
  1103  	).SetTimeTriggerConditionConfig(
  1104  		[]*dstypes.SpecCondition{
  1105  			{
  1106  				Operator: datapb.Condition_OPERATOR_GREATER_THAN,
  1107  				Value:    fmt.Sprintf("%d", tm.UnixNano()),
  1108  			},
  1109  		})
  1110  
  1111  	term := datasource.NewDefinition(
  1112  		datasource.ContentTypeOracle,
  1113  	).SetTimeTriggerConditionConfig(
  1114  		[]*dstypes.SpecCondition{
  1115  			{
  1116  				Operator: datapb.Condition_OPERATOR_LESS_THAN,
  1117  				Value:    fmt.Sprintf("%d", tm.UnixNano()),
  1118  			},
  1119  		})
  1120  
  1121  	riskParameters := types.NewMarketConfigurationLogNormal{
  1122  		LogNormal: &types.LogNormalRiskModel{
  1123  			RiskAversionParameter: num.DecimalFromFloat(0.01),
  1124  			Tau:                   num.DecimalFromFloat(0.00011407711613050422),
  1125  			Params: &types.LogNormalModelParams{
  1126  				Mu:    num.DecimalZero(),
  1127  				R:     num.DecimalFromFloat(0.016),
  1128  				Sigma: num.DecimalFromFloat(0.09),
  1129  			},
  1130  		},
  1131  	}
  1132  
  1133  	proposal := types.Proposal{
  1134  		ID:        id,
  1135  		Reference: "ref-" + id,
  1136  		Party:     party.Id,
  1137  		State:     types.ProposalStateOpen,
  1138  		Terms: &types.ProposalTerms{
  1139  			ClosingTimestamp:    now.Add(48 * time.Hour).Unix(),
  1140  			EnactmentTimestamp:  now.Add(2 * 48 * time.Hour).Unix(),
  1141  			ValidationTimestamp: now.Add(1 * time.Hour).Unix(),
  1142  			Change: &types.ProposalTermsNewMarket{
  1143  				NewMarket: &types.NewMarket{
  1144  					Changes: &types.NewMarketConfiguration{
  1145  						Instrument: &types.InstrumentConfiguration{
  1146  							Name: "June 2020 GBP vs VUSD future",
  1147  							Code: "CRYPTO:GBPVUSD/JUN20",
  1148  							Product: &types.InstrumentConfigurationFuture{
  1149  								Future: &types.FutureProduct{
  1150  									SettlementAsset:                     "VUSD",
  1151  									QuoteName:                           "VUSD",
  1152  									DataSourceSpecForSettlementData:     *settl,
  1153  									DataSourceSpecForTradingTermination: *term,
  1154  									DataSourceSpecBinding:               termBinding,
  1155  								},
  1156  							},
  1157  						},
  1158  						RiskParameters:          &riskParameters,
  1159  						Metadata:                []string{"asset_class:fx/crypto", "product:futures"},
  1160  						DecimalPlaces:           0,
  1161  						LinearSlippageFactor:    num.DecimalFromFloat(0.1),
  1162  						QuadraticSlippageFactor: num.DecimalFromFloat(0.1),
  1163  						LiquidationStrategy: &types.LiquidationStrategy{
  1164  							DisposalTimeStep:    10 * time.Second,
  1165  							DisposalFraction:    num.DecimalFromFloat(0.1),
  1166  							FullDisposalSize:    20,
  1167  							MaxFractionConsumed: num.DecimalFromFloat(0.01),
  1168  							DisposalSlippage:    num.DecimalFromFloat(0.1),
  1169  						},
  1170  						TickSize: num.UintOne(),
  1171  					},
  1172  				},
  1173  			},
  1174  		},
  1175  		Rationale: &types.ProposalRationale{
  1176  			Description: "some description",
  1177  		},
  1178  	}
  1179  
  1180  	// setup
  1181  	eng.ensureAllAssetEnabled(t)
  1182  	// expect
  1183  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidFutureProduct)
  1184  
  1185  	// when
  1186  	toSubmit, err := eng.submitProposal(t, proposal)
  1187  
  1188  	// then
  1189  	assert.Error(t, err, governance.ErrInternalTimeTriggerForFuturesInNotAllowed)
  1190  	require.Nil(t, toSubmit)
  1191  }
  1192  
  1193  func testSubmittingDuplicatedProposalForNewMarketFails(t *testing.T) {
  1194  	eng := getTestEngine(t, time.Now())
  1195  
  1196  	// given
  1197  	party := vgrand.RandomStr(5)
  1198  	proposal := eng.newProposalForNewMarket(party, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1199  
  1200  	// setup
  1201  	eng.ensureTokenBalanceForParty(t, party, 1000)
  1202  	eng.ensureAllAssetEnabled(t)
  1203  
  1204  	// expect
  1205  	eng.expectOpenProposalEvent(t, party, proposal.ID)
  1206  
  1207  	// when
  1208  	_, err := eng.submitProposal(t, proposal)
  1209  
  1210  	// then
  1211  	require.NoError(t, err)
  1212  
  1213  	// given
  1214  	duplicatedProposal := proposal
  1215  	duplicatedProposal.Reference = "this-is-a-copy"
  1216  
  1217  	// when
  1218  	_, err = eng.submitProposal(t, duplicatedProposal)
  1219  
  1220  	// then
  1221  	require.Error(t, err)
  1222  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error())
  1223  
  1224  	// given
  1225  	duplicatedProposal = proposal
  1226  	duplicatedProposal.State = types.ProposalStatePassed
  1227  
  1228  	// when
  1229  	_, err = eng.submitProposal(t, duplicatedProposal)
  1230  
  1231  	// then
  1232  	require.Error(t, err)
  1233  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error(), "reject attempt to change state indirectly")
  1234  }
  1235  
  1236  func testSubmittingDuplicatedProposalWithInternalTimeTerminationForNewMarketFails(t *testing.T) {
  1237  	eng := getTestEngine(t, time.Now())
  1238  
  1239  	// given
  1240  	party := vgrand.RandomStr(5)
  1241  	proposal := eng.newProposalForNewMarket(party, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, false)
  1242  
  1243  	// setup
  1244  	eng.ensureTokenBalanceForParty(t, party, 1000)
  1245  	eng.ensureAllAssetEnabled(t)
  1246  
  1247  	// expect
  1248  	eng.expectOpenProposalEvent(t, party, proposal.ID)
  1249  
  1250  	// when
  1251  	_, err := eng.submitProposal(t, proposal)
  1252  
  1253  	// then
  1254  	require.NoError(t, err)
  1255  
  1256  	// given
  1257  	duplicatedProposal := proposal
  1258  	duplicatedProposal.Reference = "this-is-a-copy"
  1259  
  1260  	// when
  1261  	_, err = eng.submitProposal(t, duplicatedProposal)
  1262  
  1263  	// then
  1264  	require.Error(t, err)
  1265  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error())
  1266  
  1267  	// given
  1268  	duplicatedProposal = proposal
  1269  	duplicatedProposal.State = types.ProposalStatePassed
  1270  
  1271  	// when
  1272  	_, err = eng.submitProposal(t, duplicatedProposal)
  1273  
  1274  	// then
  1275  	require.Error(t, err)
  1276  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error(), "reject attempt to change state indirectly")
  1277  }
  1278  
  1279  func testSubmittingProposalForNewMarketWithBadRiskParameterFails(t *testing.T) {
  1280  	eng := getTestEngine(t, time.Now())
  1281  
  1282  	// given
  1283  	party := eng.newValidParty("a-valid-party", 1)
  1284  	eng.ensureAllAssetEnabled(t)
  1285  
  1286  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1287  	proposal.Terms.GetNewMarket().Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{
  1288  		LogNormal: &types.LogNormalRiskModel{
  1289  			Params: nil, // it's nil by zero value, but eh, let's show that's what we test
  1290  		},
  1291  	}
  1292  
  1293  	// setup
  1294  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
  1295  
  1296  	// when
  1297  	_, err := eng.submitProposal(t, proposal)
  1298  
  1299  	// then
  1300  	require.Error(t, err)
  1301  	assert.Contains(t, err.Error(), "invalid risk parameter")
  1302  }
  1303  
  1304  func testSubmittingProposalForNewMarketWithInternalTimeTerminationWithBadRiskParameterFails(t *testing.T) {
  1305  	eng := getTestEngine(t, time.Now())
  1306  
  1307  	// given
  1308  	party := eng.newValidParty("a-valid-party", 1)
  1309  	eng.ensureAllAssetEnabled(t)
  1310  
  1311  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, false)
  1312  	proposal.Terms.GetNewMarket().Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{
  1313  		LogNormal: &types.LogNormalRiskModel{
  1314  			Params: nil, // it's nil by zero value, but eh, let's show that's what we test
  1315  		},
  1316  	}
  1317  
  1318  	// setup
  1319  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
  1320  
  1321  	// when
  1322  	_, err := eng.submitProposal(t, proposal)
  1323  
  1324  	// then
  1325  	require.Error(t, err)
  1326  	assert.Contains(t, err.Error(), "invalid risk parameter")
  1327  }
  1328  
  1329  func testSubmittingProposalWithoutDisposalSlippageFails(t *testing.T) {
  1330  	eng := getTestEngine(t, time.Now())
  1331  	// given
  1332  	party := eng.newValidParty("a-valid-party", 1)
  1333  	eng.ensureAllAssetEnabled(t)
  1334  
  1335  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, false)
  1336  	proposal.Terms.GetNewMarket().Changes.LiquidationStrategy = &types.LiquidationStrategy{
  1337  		DisposalTimeStep:    time.Second * 10,
  1338  		DisposalFraction:    num.DecimalFromFloat(0.2),
  1339  		FullDisposalSize:    10,
  1340  		MaxFractionConsumed: num.DecimalFromFloat(0.5),
  1341  	}
  1342  
  1343  	// setup
  1344  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
  1345  
  1346  	// when
  1347  	_, err := eng.submitProposal(t, proposal)
  1348  
  1349  	// then
  1350  	require.Error(t, err)
  1351  	assert.Contains(t, err.Error(), "liquidation strategy must specify a disposal slippage range > 0")
  1352  }
  1353  
  1354  func testOutOfRangeRiskParamFail(t *testing.T, lnm *types.LogNormalRiskModel) {
  1355  	t.Helper()
  1356  	eng := getTestEngine(t, time.Now())
  1357  
  1358  	// given
  1359  	party := eng.newValidParty("a-valid-party", 1)
  1360  	eng.ensureAllAssetEnabled(t)
  1361  
  1362  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1363  	proposal.Terms.GetNewMarket().Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{LogNormal: lnm}
  1364  
  1365  	// setup
  1366  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
  1367  
  1368  	// when
  1369  	_, err := eng.submitProposal(t, proposal)
  1370  
  1371  	// then
  1372  	require.Error(t, err)
  1373  	assert.Contains(t, err.Error(), "invalid risk parameter")
  1374  }
  1375  
  1376  func TestSubmittingProposalForNewMarketWithOutOfRangeRiskParameterFails(t *testing.T) {
  1377  	lnm := &types.LogNormalRiskModel{}
  1378  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e-8 - 1e-12)
  1379  	testOutOfRangeRiskParamFail(t, lnm)
  1380  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e1 + 1e-12)
  1381  	testOutOfRangeRiskParamFail(t, lnm)
  1382  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e-6)
  1383  	lnm.Tau = num.DecimalFromFloat(1e-8 - 1e-12)
  1384  	testOutOfRangeRiskParamFail(t, lnm)
  1385  	lnm.Tau = num.DecimalFromFloat(1 + 1e-12)
  1386  	testOutOfRangeRiskParamFail(t, lnm)
  1387  	lnm.Tau = num.DecimalOne()
  1388  	lnm.Params = &types.LogNormalModelParams{}
  1389  	lnm.Params.Mu = num.DecimalFromFloat(-1e-6 - 1e-12)
  1390  	testOutOfRangeRiskParamFail(t, lnm)
  1391  	lnm.Params.Mu = num.DecimalFromFloat(1e-6 + 1e-12)
  1392  	testOutOfRangeRiskParamFail(t, lnm)
  1393  	lnm.Params.Mu = num.DecimalFromFloat(0.0)
  1394  	lnm.Params.R = num.DecimalFromFloat(-1 - 1e-12)
  1395  	testOutOfRangeRiskParamFail(t, lnm)
  1396  	lnm.Params.R = num.DecimalFromFloat(1 + 1e-12)
  1397  	testOutOfRangeRiskParamFail(t, lnm)
  1398  	lnm.Params.R = num.DecimalFromFloat(0.0)
  1399  	lnm.Params.Sigma = num.DecimalFromFloat(1e-3 - 1e-12)
  1400  	testOutOfRangeRiskParamFail(t, lnm)
  1401  	lnm.Params.Sigma = num.DecimalFromFloat(50 + 1e-12)
  1402  	testOutOfRangeRiskParamFail(t, lnm)
  1403  	lnm.Params.Sigma = num.DecimalFromFloat(1.0)
  1404  
  1405  	// now all risk params are valid
  1406  	eng := getTestEngine(t, time.Now())
  1407  
  1408  	// given
  1409  	party := eng.newValidParty("a-valid-party", 1)
  1410  	eng.ensureAllAssetEnabled(t)
  1411  
  1412  	proposal := eng.newProposalForNewMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1413  	proposal.Terms.GetNewMarket().Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{LogNormal: lnm}
  1414  
  1415  	// setup
  1416  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
  1417  
  1418  	// when
  1419  	_, err := eng.submitProposal(t, proposal)
  1420  
  1421  	// then
  1422  	require.NoError(t, err)
  1423  }
  1424  
  1425  func testRejectingProposalForNewMarketSucceeds(t *testing.T) {
  1426  	eng := getTestEngine(t, time.Now())
  1427  
  1428  	// given
  1429  	party := vgrand.RandomStr(5)
  1430  	proposal := eng.newProposalForNewMarket(party, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1431  
  1432  	// setup
  1433  	eng.ensureAllAssetEnabled(t)
  1434  	eng.ensureTokenBalanceForParty(t, party, 10000)
  1435  
  1436  	// expect
  1437  	eng.expectOpenProposalEvent(t, party, proposal.ID)
  1438  
  1439  	// when
  1440  	toSubmit, err := eng.submitProposal(t, proposal)
  1441  
  1442  	// then
  1443  	require.NoError(t, err)
  1444  	require.NotNil(t, toSubmit)
  1445  
  1446  	// expect
  1447  	eng.expectRejectedProposalEvent(t, party, proposal.ID, types.ProposalErrorCouldNotInstantiateMarket)
  1448  
  1449  	// when
  1450  	err = eng.RejectProposal(context.Background(), toSubmit.Proposal(), types.ProposalErrorCouldNotInstantiateMarket, assert.AnError)
  1451  
  1452  	// then
  1453  	require.NoError(t, err)
  1454  
  1455  	// when
  1456  	// Just one more time to make sure it was removed from proposals.
  1457  	err = eng.RejectProposal(context.Background(), toSubmit.Proposal(), types.ProposalErrorCouldNotInstantiateMarket, assert.AnError)
  1458  
  1459  	// then
  1460  	assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
  1461  }
  1462  
  1463  func testVotingForNewMarketProposalSucceeds(t *testing.T) {
  1464  	eng := getTestEngine(t, time.Now())
  1465  
  1466  	// given
  1467  	proposer := vgrand.RandomStr(5)
  1468  	proposal := eng.newProposalForNewMarket(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1469  
  1470  	// setup
  1471  	eng.ensureAllAssetEnabled(t)
  1472  	eng.ensureTokenBalanceForParty(t, proposer, 1)
  1473  
  1474  	// expect
  1475  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
  1476  
  1477  	// when
  1478  	_, err := eng.submitProposal(t, proposal)
  1479  
  1480  	// then
  1481  	require.NoError(t, err)
  1482  
  1483  	// given
  1484  	voter := vgrand.RandomStr(5)
  1485  
  1486  	// setup
  1487  	eng.ensureTokenBalanceForParty(t, voter, 1)
  1488  
  1489  	// expect
  1490  	eng.expectVoteEvent(t, voter, proposal.ID)
  1491  
  1492  	// when
  1493  	err = eng.addYesVote(t, voter, proposal.ID)
  1494  
  1495  	// then
  1496  	require.NoError(t, err)
  1497  }
  1498  
  1499  func testVotingWithMajorityOfYesMakesNewMarketProposalPassed(t *testing.T) {
  1500  	eng := getTestEngine(t, time.Now())
  1501  
  1502  	// when
  1503  	proposer := vgrand.RandomStr(5)
  1504  	proposal := eng.newProposalForNewMarket(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1505  
  1506  	// setup
  1507  	eng.ensureStakingAssetTotalSupply(t, 9)
  1508  	eng.ensureAllAssetEnabled(t)
  1509  	eng.ensureTokenBalanceForParty(t, proposer, 1)
  1510  
  1511  	// expect
  1512  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
  1513  
  1514  	// when
  1515  	_, err := eng.submitProposal(t, proposal)
  1516  
  1517  	// then
  1518  	require.NoError(t, err)
  1519  
  1520  	// given
  1521  	voter1 := vgrand.RandomStr(5)
  1522  
  1523  	// setup
  1524  	eng.ensureTokenBalanceForParty(t, voter1, 7)
  1525  
  1526  	// expect
  1527  	eng.expectVoteEvent(t, voter1, proposal.ID)
  1528  
  1529  	// then
  1530  	err = eng.addYesVote(t, voter1, proposal.ID)
  1531  
  1532  	// then
  1533  	require.NoError(t, err)
  1534  
  1535  	// given
  1536  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
  1537  
  1538  	// setup
  1539  	eng.ensureTokenBalanceForParty(t, voter1, 7)
  1540  
  1541  	// expect
  1542  	eng.expectPassedProposalEvent(t, proposal.ID)
  1543  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7")
  1544  	eng.expectGetMarketState(t, proposal.ID)
  1545  
  1546  	// when
  1547  	eng.OnTick(context.Background(), afterClosing)
  1548  
  1549  	// given
  1550  	voter2 := vgrand.RandomStr(5)
  1551  
  1552  	// when
  1553  	err = eng.addNoVote(t, voter2, proposal.ID)
  1554  
  1555  	// then
  1556  	require.Error(t, err)
  1557  	assert.EqualError(t, err, governance.ErrProposalNotOpenForVotes.Error())
  1558  
  1559  	// given
  1560  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
  1561  
  1562  	// when
  1563  	// no calculations, no state change, simply removed from governance engine
  1564  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
  1565  
  1566  	// then
  1567  	require.Len(t, toBeEnacted, 1)
  1568  	assert.Equal(t, proposal.ID, toBeEnacted[0].Proposal().ID)
  1569  
  1570  	// when
  1571  	err = eng.addNoVote(t, voter2, proposal.ID)
  1572  
  1573  	// then
  1574  	require.Error(t, err)
  1575  	assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
  1576  }
  1577  
  1578  func testVotingWithMajorityOfNoMakesNewMarketProposalDeclined(t *testing.T) {
  1579  	eng := getTestEngine(t, time.Now())
  1580  
  1581  	// given
  1582  	proposer := vgrand.RandomStr(5)
  1583  	proposal := eng.newProposalForNewMarket(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1584  
  1585  	// setup
  1586  	eng.ensureAllAssetEnabled(t)
  1587  	eng.ensureStakingAssetTotalSupply(t, 200)
  1588  	eng.ensureTokenBalanceForParty(t, proposer, 100)
  1589  
  1590  	// expect
  1591  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
  1592  
  1593  	// when
  1594  	_, err := eng.submitProposal(t, proposal)
  1595  
  1596  	// then
  1597  	require.NoError(t, err)
  1598  
  1599  	// given
  1600  	voter := vgrand.RandomStr(5)
  1601  
  1602  	// setup
  1603  	eng.ensureTokenBalanceForParty(t, voter, 100)
  1604  
  1605  	// expect
  1606  	eng.expectVoteEvent(t, voter, proposal.ID)
  1607  
  1608  	// when
  1609  	err = eng.addYesVote(t, voter, proposal.ID)
  1610  
  1611  	// then
  1612  	require.NoError(t, err)
  1613  
  1614  	// setup
  1615  	eng.ensureTokenBalanceForParty(t, voter, 100)
  1616  
  1617  	// setup
  1618  	eng.expectVoteEvent(t, voter, proposal.ID)
  1619  
  1620  	// when
  1621  	err = eng.addNoVote(t, voter, proposal.ID)
  1622  
  1623  	// then
  1624  	require.NoError(t, err)
  1625  
  1626  	// given
  1627  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
  1628  
  1629  	// setup
  1630  	eng.ensureTokenBalanceForParty(t, voter, 100)
  1631  
  1632  	// expect
  1633  	eng.expectDeclinedProposalEvent(t, proposal.ID, types.ProposalErrorMajorityThresholdNotReached)
  1634  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "100")
  1635  	eng.expectGetMarketState(t, proposal.ID)
  1636  
  1637  	// when
  1638  	_, voteClosed := eng.OnTick(context.Background(), afterClosing)
  1639  
  1640  	// then
  1641  	require.Len(t, voteClosed, 1)
  1642  	vc := voteClosed[0]
  1643  	require.NotNil(t, vc.NewMarket())
  1644  	assert.True(t, vc.NewMarket().Rejected())
  1645  
  1646  	// given
  1647  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
  1648  
  1649  	// when
  1650  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
  1651  
  1652  	// then
  1653  	assert.Empty(t, toBeEnacted)
  1654  }
  1655  
  1656  func testVotingWithInsufficientParticipationMakesNewMarketProposalDeclined(t *testing.T) {
  1657  	eng := getTestEngine(t, time.Now())
  1658  
  1659  	// given
  1660  	proposer := vgrand.RandomStr(5)
  1661  	proposal := eng.newProposalForNewMarket(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1662  
  1663  	// setup
  1664  	eng.ensureAllAssetEnabled(t)
  1665  	eng.ensureStakingAssetTotalSupply(t, 800)
  1666  	eng.ensureTokenBalanceForParty(t, proposer, 100)
  1667  
  1668  	// expect
  1669  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
  1670  
  1671  	// when
  1672  	_, err := eng.submitProposal(t, proposal)
  1673  
  1674  	// then
  1675  	require.NoError(t, err)
  1676  
  1677  	// given
  1678  	voter := vgrand.RandomStr(5)
  1679  
  1680  	// setup
  1681  	eng.ensureTokenBalanceForParty(t, voter, 100)
  1682  
  1683  	// expect
  1684  	eng.expectVoteEvent(t, voter, proposal.ID)
  1685  
  1686  	// when
  1687  	err = eng.addYesVote(t, voter, proposal.ID)
  1688  
  1689  	// then
  1690  	require.NoError(t, err)
  1691  
  1692  	// given
  1693  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
  1694  
  1695  	// setup
  1696  	eng.ensureTokenBalanceForParty(t, voter, 100)
  1697  
  1698  	// expect
  1699  	eng.expectDeclinedProposalEvent(t, proposal.ID, types.ProposalErrorParticipationThresholdNotReached)
  1700  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "100")
  1701  	eng.expectGetMarketState(t, proposal.ID)
  1702  	// when
  1703  	_, voteClosed := eng.OnTick(context.Background(), afterClosing)
  1704  
  1705  	// then
  1706  	require.Len(t, voteClosed, 1)
  1707  	vc := voteClosed[0]
  1708  	require.NotNil(t, vc.NewMarket())
  1709  	assert.True(t, vc.NewMarket().Rejected())
  1710  
  1711  	// given
  1712  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
  1713  
  1714  	// when
  1715  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
  1716  
  1717  	// then
  1718  	assert.Empty(t, toBeEnacted)
  1719  }
  1720  
  1721  func testSubmittingProposalForNewPerpsMarketSucceeds(t *testing.T) {
  1722  	eng := getTestEngine(t, time.Now())
  1723  	defer eng.ctrl.Finish()
  1724  
  1725  	// given
  1726  	party := eng.newValidParty("a-valid-party", 123456789)
  1727  	proposal := eng.newProposalForNewPerpsMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1728  
  1729  	// setup
  1730  	eng.ensureAllAssetEnabled(t)
  1731  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
  1732  
  1733  	// when
  1734  	toSubmit, err := eng.submitProposal(t, proposal)
  1735  	require.NoError(t, err)
  1736  
  1737  	// the proposal had a nil initial time for the time trigger.
  1738  	// ensure it was set to the enactment time.
  1739  	tt := toSubmit.Proposal().Terms.GetNewMarket().Changes.Instrument.
  1740  		Product.(*types.InstrumentConfigurationPerps).
  1741  		Perps.DataSourceSpecForSettlementSchedule.
  1742  		GetInternalTimeTriggerSpecConfiguration().Triggers[0]
  1743  
  1744  	enactmentTime := toSubmit.Proposal().Terms.EnactmentTimestamp
  1745  
  1746  	assert.NotNil(t, tt.Initial)
  1747  	assert.Equal(t, tt.Initial.Unix(), enactmentTime)
  1748  
  1749  	// then
  1750  	require.NoError(t, err)
  1751  	require.NotNil(t, toSubmit)
  1752  	assert.True(t, toSubmit.IsNewMarket())
  1753  	require.NotNil(t, toSubmit.NewMarket().Market())
  1754  }
  1755  
  1756  func testSubmittingProposalForNewPerpsMarketWithCustomInitialTimeSucceeds(t *testing.T) {
  1757  	eng := getTestEngine(t, time.Now())
  1758  	defer eng.ctrl.Finish()
  1759  
  1760  	// given
  1761  	party := eng.newValidParty("a-valid-party", 123456789)
  1762  	proposal := eng.newProposalForNewPerpsMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1763  
  1764  	// set the time differently to start e.g sometimes after the enactment ti
  1765  	enactAt := proposal.Terms.EnactmentTimestamp
  1766  	proposal.Terms.Change.(*types.ProposalTermsNewMarket).NewMarket.Changes.Instrument.Product.(*types.InstrumentConfigurationPerps).Perps.DataSourceSpecForSettlementSchedule.GetInternalTimeTriggerSpecConfiguration().Triggers[0].Initial = ptr.From(time.Unix(enactAt, 0).Add(60 * time.Minute))
  1767  
  1768  	// setup
  1769  	eng.ensureAllAssetEnabled(t)
  1770  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
  1771  
  1772  	// when
  1773  	toSubmit, err := eng.submitProposal(t, proposal)
  1774  	require.NoError(t, err)
  1775  
  1776  	// the proposal had a nil initial time for the time trigger.
  1777  	// ensure it was set to the enactment time.
  1778  	tt := toSubmit.Proposal().Terms.GetNewMarket().Changes.Instrument.
  1779  		Product.(*types.InstrumentConfigurationPerps).
  1780  		Perps.DataSourceSpecForSettlementSchedule.
  1781  		GetInternalTimeTriggerSpecConfiguration().Triggers[0]
  1782  
  1783  	enactmentTime := toSubmit.Proposal().Terms.EnactmentTimestamp
  1784  
  1785  	assert.NotNil(t, tt.Initial)
  1786  	assert.NotEqual(t, tt.Initial.Unix(), enactmentTime)
  1787  
  1788  	// then
  1789  	require.NoError(t, err)
  1790  	require.NotNil(t, toSubmit)
  1791  	assert.True(t, toSubmit.IsNewMarket())
  1792  	require.NotNil(t, toSubmit.NewMarket().Market())
  1793  }
  1794  
  1795  func testSubmittingProposalForNewPerpsMarketWithPastInitialTimeFails(t *testing.T) {
  1796  	eng := getTestEngine(t, time.Now())
  1797  	defer eng.ctrl.Finish()
  1798  
  1799  	// given
  1800  	party := eng.newValidParty("a-valid-party", 123456789)
  1801  	proposal := eng.newProposalForNewPerpsMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1802  
  1803  	now := eng.tsvc.GetTimeNow()
  1804  	proposal.Terms.Change.(*types.ProposalTermsNewMarket).NewMarket.Changes.Instrument.Product.(*types.InstrumentConfigurationPerps).Perps.DataSourceSpecForSettlementSchedule.GetInternalTimeTriggerSpecConfiguration().Triggers[0].Initial = ptr.From(now.Add(-(60 * time.Second)))
  1805  
  1806  	// setup
  1807  	eng.ensureAllAssetEnabled(t)
  1808  
  1809  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidPerpsProduct)
  1810  
  1811  	// when
  1812  	toSubmit, err := eng.submitProposal(t, proposal)
  1813  
  1814  	// then
  1815  	require.EqualError(t, err, "time trigger starts in the past")
  1816  	require.Nil(t, toSubmit)
  1817  }
  1818  
  1819  func testInvalidDecimalPlace(t *testing.T) {
  1820  	eng := getTestEngine(t, time.Now())
  1821  	defer eng.ctrl.Finish()
  1822  
  1823  	// given
  1824  	party := eng.newValidParty("a-valid-party", 123456789)
  1825  	proposal := eng.newProposalForNewPerpsMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
  1826  
  1827  	proposal.Terms.GetNewMarket().Changes.DecimalPlaces = 12
  1828  	proposal.Terms.GetNewMarket().Changes.PositionDecimalPlaces = 7
  1829  
  1830  	// set the time differently to start e.g sometimes after the enactment ti
  1831  	enactAt := proposal.Terms.EnactmentTimestamp
  1832  	proposal.Terms.Change.(*types.ProposalTermsNewMarket).NewMarket.Changes.Instrument.Product.(*types.InstrumentConfigurationPerps).Perps.DataSourceSpecForSettlementSchedule.GetInternalTimeTriggerSpecConfiguration().Triggers[0].Initial = ptr.From(time.Unix(enactAt, 0).Add(60 * time.Minute))
  1833  
  1834  	// setup
  1835  	eng.ensureAllAssetEnabled(t)
  1836  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorTooManyMarketDecimalPlaces)
  1837  
  1838  	// when
  1839  	_, err := eng.submitProposal(t, proposal)
  1840  	require.Equal(t, "market decimal + position decimals must be less than or equal to asset decimals", err.Error())
  1841  }