code.vegaprotocol.io/vega@v0.79.0/core/governance/engine_new_spot_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  	"testing"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/governance"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  func TesSpottProposalForNewMarket(t *testing.T) {
    34  	t.Helper()
    35  	t.Run("Submitting a proposal for new spot market succeeds", testSubmittingProposalForNewSpotMarketSucceeds)
    36  	t.Run("Submitting a duplicated proposal for new spot market fails", testSubmittingDuplicatedProposalForNewSpotMarketFails)
    37  	t.Run("Submitting a proposal for new spot market with bad risk parameter fails", testSubmittingProposalForNewSpotMarketWithBadRiskParameterFails)
    38  	t.Run("Rejecting a proposal for new spot market succeeds", testRejectingProposalForNewSpotMarketSucceeds)
    39  	t.Run("Voting for a new spot market proposal succeeds", testVotingForNewSpotMarketProposalSucceeds)
    40  	t.Run("Voting with a majority of 'yes' makes the new spot market proposal passed", testVotingWithMajorityOfYesMakesNewSpotMarketProposalPassed)
    41  	t.Run("Voting with a majority of 'no' makes the new spot market proposal declined", testVotingWithMajorityOfNoMakesNewSpotMarketProposalDeclined)
    42  	t.Run("Voting with insufficient participation makes the new spot market proposal declined", testVotingWithInsufficientParticipationMakesNewSpotMarketProposalDeclined)
    43  	t.Run("Invalid combination of spot decimals for market", testInvalidSpotDecimalPlace)
    44  	t.Run("Invalid combination of position decimals for base asset", testInvalidSpotPositionDecimalPlace)
    45  }
    46  
    47  func testSubmittingProposalForNewSpotMarketSucceeds(t *testing.T) {
    48  	eng := getTestEngine(t, time.Now())
    49  
    50  	// given
    51  	party := eng.newValidParty("a-valid-party", 123456789)
    52  	proposal := eng.newProposalForNewSpotMarket(party.Id, eng.tsvc.GetTimeNow())
    53  
    54  	// setup
    55  	eng.ensureAllAssetEnabled(t)
    56  	eng.expectOpenProposalEvent(t, party.Id, proposal.ID)
    57  
    58  	// when
    59  	toSubmit, err := eng.submitProposal(t, proposal)
    60  
    61  	// then
    62  	require.NoError(t, err)
    63  	require.NotNil(t, toSubmit)
    64  	assert.True(t, toSubmit.IsNewSpotMarket())
    65  	require.NotNil(t, toSubmit.NewSpotMarket().Market())
    66  }
    67  
    68  func testSubmittingDuplicatedProposalForNewSpotMarketFails(t *testing.T) {
    69  	eng := getTestEngine(t, time.Now())
    70  
    71  	// given
    72  	party := vgrand.RandomStr(5)
    73  	proposal := eng.newProposalForNewSpotMarket(party, eng.tsvc.GetTimeNow())
    74  
    75  	// setup
    76  	eng.ensureTokenBalanceForParty(t, party, 1000)
    77  	eng.ensureAllAssetEnabled(t)
    78  
    79  	// expect
    80  	eng.expectOpenProposalEvent(t, party, proposal.ID)
    81  
    82  	// when
    83  	_, err := eng.submitProposal(t, proposal)
    84  
    85  	// then
    86  	require.NoError(t, err)
    87  
    88  	// given
    89  	duplicatedProposal := proposal
    90  	duplicatedProposal.Reference = "this-is-a-copy"
    91  
    92  	// when
    93  	_, err = eng.submitProposal(t, duplicatedProposal)
    94  
    95  	// then
    96  	require.Error(t, err)
    97  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error())
    98  
    99  	// given
   100  	duplicatedProposal = proposal
   101  	duplicatedProposal.State = types.ProposalStatePassed
   102  
   103  	// when
   104  	_, err = eng.submitProposal(t, duplicatedProposal)
   105  
   106  	// then
   107  	require.Error(t, err)
   108  	assert.EqualError(t, governance.ErrProposalIsDuplicate, err.Error(), "reject attempt to change state indirectly")
   109  }
   110  
   111  func testSubmittingProposalForNewSpotMarketWithBadRiskParameterFails(t *testing.T) {
   112  	eng := getTestEngine(t, time.Now())
   113  
   114  	// given
   115  	party := eng.newValidParty("a-valid-party", 1)
   116  	eng.ensureAllAssetEnabled(t)
   117  
   118  	proposal := eng.newProposalForNewSpotMarket(party.Id, eng.tsvc.GetTimeNow())
   119  	proposal.Terms.GetNewMarket().Changes.RiskParameters = &types.NewMarketConfigurationLogNormal{
   120  		LogNormal: &types.LogNormalRiskModel{
   121  			Params: nil, // it's nil by zero value, but eh, let's show that's what we test
   122  		},
   123  	}
   124  
   125  	// setup
   126  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
   127  
   128  	// when
   129  	_, err := eng.submitProposal(t, proposal)
   130  
   131  	// then
   132  	require.Error(t, err)
   133  	assert.Contains(t, err.Error(), "invalid risk parameter")
   134  }
   135  
   136  func TestSubmittingProposalForNewSpotMarketWithOutOfRangeRiskParameterFails(t *testing.T) {
   137  	lnm := &types.LogNormalRiskModel{}
   138  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e-8 - 1e-12)
   139  	testOutOfRangeRiskParamFail(t, lnm)
   140  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e1 + 1e-12)
   141  	testOutOfRangeRiskParamFail(t, lnm)
   142  	lnm.RiskAversionParameter = num.DecimalFromFloat(1e-6)
   143  	lnm.Tau = num.DecimalFromFloat(1e-8 - 1e-12)
   144  	testOutOfRangeRiskParamFail(t, lnm)
   145  	lnm.Tau = num.DecimalFromFloat(1 + 1e-12)
   146  	testOutOfRangeRiskParamFail(t, lnm)
   147  	lnm.Tau = num.DecimalOne()
   148  	lnm.Params = &types.LogNormalModelParams{}
   149  	lnm.Params.Mu = num.DecimalFromFloat(-1e-6 - 1e-12)
   150  	testOutOfRangeRiskParamFail(t, lnm)
   151  	lnm.Params.Mu = num.DecimalFromFloat(1e-6 + 1e-12)
   152  	testOutOfRangeRiskParamFail(t, lnm)
   153  	lnm.Params.Mu = num.DecimalFromFloat(0.0)
   154  	lnm.Params.R = num.DecimalFromFloat(-1 - 1e-12)
   155  	testOutOfRangeRiskParamFail(t, lnm)
   156  	lnm.Params.R = num.DecimalFromFloat(1 + 1e-12)
   157  	testOutOfRangeRiskParamFail(t, lnm)
   158  	lnm.Params.R = num.DecimalFromFloat(0.0)
   159  	lnm.Params.Sigma = num.DecimalFromFloat(1e-3 - 1e-12)
   160  	testOutOfRangeRiskParamFail(t, lnm)
   161  	lnm.Params.Sigma = num.DecimalFromFloat(50 + 1e-12)
   162  	testOutOfRangeRiskParamFail(t, lnm)
   163  	lnm.Params.Sigma = num.DecimalFromFloat(1.0)
   164  
   165  	// now all risk params are valid
   166  	eng := getTestEngine(t, time.Now())
   167  
   168  	// given
   169  	party := eng.newValidParty("a-valid-party", 1)
   170  	eng.ensureAllAssetEnabled(t)
   171  
   172  	proposal := eng.newProposalForNewSpotMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour))
   173  	proposal.Terms.GetNewSpotMarket().Changes.RiskParameters = &types.NewSpotMarketConfigurationLogNormal{LogNormal: lnm}
   174  
   175  	// setup
   176  	eng.broker.EXPECT().Send(gomock.Any()).Times(1)
   177  
   178  	// when
   179  	_, err := eng.submitProposal(t, proposal)
   180  
   181  	// then
   182  	require.NoError(t, err)
   183  }
   184  
   185  func testRejectingProposalForNewSpotMarketSucceeds(t *testing.T) {
   186  	eng := getTestEngine(t, time.Now())
   187  
   188  	// given
   189  	party := vgrand.RandomStr(5)
   190  	proposal := eng.newProposalForNewSpotMarket(party, eng.tsvc.GetTimeNow())
   191  
   192  	// setup
   193  	eng.ensureAllAssetEnabled(t)
   194  	eng.ensureTokenBalanceForParty(t, party, 10000)
   195  
   196  	// expect
   197  	eng.expectOpenProposalEvent(t, party, proposal.ID)
   198  
   199  	// when
   200  	toSubmit, err := eng.submitProposal(t, proposal)
   201  
   202  	// then
   203  	require.NoError(t, err)
   204  	require.NotNil(t, toSubmit)
   205  
   206  	// expect
   207  	eng.expectRejectedProposalEvent(t, party, proposal.ID, types.ProposalErrorCouldNotInstantiateMarket)
   208  
   209  	// when
   210  	err = eng.RejectProposal(context.Background(), toSubmit.Proposal(), types.ProposalErrorCouldNotInstantiateMarket, assert.AnError)
   211  
   212  	// then
   213  	require.NoError(t, err)
   214  
   215  	// when
   216  	// Just one more time to make sure it was removed from proposals.
   217  	err = eng.RejectProposal(context.Background(), toSubmit.Proposal(), types.ProposalErrorCouldNotInstantiateMarket, assert.AnError)
   218  
   219  	// then
   220  	assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
   221  }
   222  
   223  func testVotingForNewSpotMarketProposalSucceeds(t *testing.T) {
   224  	eng := getTestEngine(t, time.Now())
   225  
   226  	// given
   227  	proposer := vgrand.RandomStr(5)
   228  	proposal := eng.newProposalForNewSpotMarket(proposer, eng.tsvc.GetTimeNow())
   229  
   230  	// setup
   231  	eng.ensureAllAssetEnabled(t)
   232  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   233  
   234  	// expect
   235  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   236  
   237  	// when
   238  	_, err := eng.submitProposal(t, proposal)
   239  
   240  	// then
   241  	require.NoError(t, err)
   242  
   243  	// given
   244  	voter := vgrand.RandomStr(5)
   245  
   246  	// setup
   247  	eng.ensureTokenBalanceForParty(t, voter, 1)
   248  
   249  	// expect
   250  	eng.expectVoteEvent(t, voter, proposal.ID)
   251  
   252  	// when
   253  	err = eng.addYesVote(t, voter, proposal.ID)
   254  
   255  	// then
   256  	require.NoError(t, err)
   257  }
   258  
   259  func testVotingWithMajorityOfYesMakesNewSpotMarketProposalPassed(t *testing.T) {
   260  	eng := getTestEngine(t, time.Now())
   261  
   262  	// when
   263  	proposer := vgrand.RandomStr(5)
   264  	proposal := eng.newProposalForNewSpotMarket(proposer, eng.tsvc.GetTimeNow())
   265  
   266  	// setup
   267  	eng.ensureStakingAssetTotalSupply(t, 9)
   268  	eng.ensureAllAssetEnabled(t)
   269  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   270  
   271  	// expect
   272  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   273  
   274  	// when
   275  	_, err := eng.submitProposal(t, proposal)
   276  
   277  	// then
   278  	require.NoError(t, err)
   279  
   280  	// given
   281  	voter1 := vgrand.RandomStr(5)
   282  
   283  	// setup
   284  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   285  
   286  	// expect
   287  	eng.expectVoteEvent(t, voter1, proposal.ID)
   288  
   289  	// then
   290  	err = eng.addYesVote(t, voter1, proposal.ID)
   291  
   292  	// then
   293  	require.NoError(t, err)
   294  
   295  	// given
   296  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   297  
   298  	// setup
   299  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   300  
   301  	// expect
   302  	eng.expectPassedProposalEvent(t, proposal.ID)
   303  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7")
   304  	eng.expectGetMarketState(t, proposal.ID)
   305  
   306  	// when
   307  	eng.OnTick(context.Background(), afterClosing)
   308  
   309  	// given
   310  	voter2 := vgrand.RandomStr(5)
   311  
   312  	// when
   313  	err = eng.addNoVote(t, voter2, proposal.ID)
   314  
   315  	// then
   316  	require.Error(t, err)
   317  	assert.EqualError(t, err, governance.ErrProposalNotOpenForVotes.Error())
   318  
   319  	// given
   320  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
   321  
   322  	// when
   323  	// no calculations, no state change, simply removed from governance engine
   324  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
   325  
   326  	// then
   327  	require.Len(t, toBeEnacted, 1)
   328  	assert.Equal(t, proposal.ID, toBeEnacted[0].Proposal().ID)
   329  
   330  	// when
   331  	err = eng.addNoVote(t, voter2, proposal.ID)
   332  
   333  	// then
   334  	require.Error(t, err)
   335  	assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
   336  }
   337  
   338  func testVotingWithMajorityOfNoMakesNewSpotMarketProposalDeclined(t *testing.T) {
   339  	eng := getTestEngine(t, time.Now())
   340  
   341  	// given
   342  	proposer := vgrand.RandomStr(5)
   343  	proposal := eng.newProposalForNewSpotMarket(proposer, eng.tsvc.GetTimeNow())
   344  
   345  	// setup
   346  	eng.ensureAllAssetEnabled(t)
   347  	eng.ensureStakingAssetTotalSupply(t, 200)
   348  	eng.ensureTokenBalanceForParty(t, proposer, 100)
   349  
   350  	// expect
   351  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   352  
   353  	// when
   354  	_, err := eng.submitProposal(t, proposal)
   355  
   356  	// then
   357  	require.NoError(t, err)
   358  
   359  	// given
   360  	voter := vgrand.RandomStr(5)
   361  
   362  	// setup
   363  	eng.ensureTokenBalanceForParty(t, voter, 100)
   364  
   365  	// expect
   366  	eng.expectVoteEvent(t, voter, proposal.ID)
   367  
   368  	// when
   369  	err = eng.addYesVote(t, voter, proposal.ID)
   370  
   371  	// then
   372  	require.NoError(t, err)
   373  
   374  	// setup
   375  	eng.ensureTokenBalanceForParty(t, voter, 100)
   376  
   377  	// setup
   378  	eng.expectVoteEvent(t, voter, proposal.ID)
   379  
   380  	// when
   381  	err = eng.addNoVote(t, voter, proposal.ID)
   382  
   383  	// then
   384  	require.NoError(t, err)
   385  
   386  	// given
   387  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   388  
   389  	// setup
   390  	eng.ensureTokenBalanceForParty(t, voter, 100)
   391  
   392  	// expect
   393  	eng.expectDeclinedProposalEvent(t, proposal.ID, types.ProposalErrorMajorityThresholdNotReached)
   394  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "100")
   395  	eng.expectGetMarketState(t, proposal.ID)
   396  
   397  	// when
   398  	_, voteClosed := eng.OnTick(context.Background(), afterClosing)
   399  
   400  	// then
   401  	require.Len(t, voteClosed, 1)
   402  	vc := voteClosed[0]
   403  	require.NotNil(t, vc.NewMarket())
   404  	assert.True(t, vc.NewMarket().Rejected())
   405  
   406  	// given
   407  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
   408  
   409  	// when
   410  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
   411  
   412  	// then
   413  	assert.Empty(t, toBeEnacted)
   414  }
   415  
   416  func testVotingWithInsufficientParticipationMakesNewSpotMarketProposalDeclined(t *testing.T) {
   417  	eng := getTestEngine(t, time.Now())
   418  
   419  	// given
   420  	proposer := vgrand.RandomStr(5)
   421  	proposal := eng.newProposalForNewSpotMarket(proposer, eng.tsvc.GetTimeNow())
   422  
   423  	// setup
   424  	eng.ensureAllAssetEnabled(t)
   425  	eng.ensureStakingAssetTotalSupply(t, 800)
   426  	eng.ensureTokenBalanceForParty(t, proposer, 100)
   427  
   428  	// expect
   429  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   430  
   431  	// when
   432  	_, err := eng.submitProposal(t, proposal)
   433  
   434  	// then
   435  	require.NoError(t, err)
   436  
   437  	// given
   438  	voter := vgrand.RandomStr(5)
   439  
   440  	// setup
   441  	eng.ensureTokenBalanceForParty(t, voter, 100)
   442  
   443  	// expect
   444  	eng.expectVoteEvent(t, voter, proposal.ID)
   445  
   446  	// when
   447  	err = eng.addYesVote(t, voter, proposal.ID)
   448  
   449  	// then
   450  	require.NoError(t, err)
   451  
   452  	// given
   453  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   454  
   455  	// setup
   456  	eng.ensureTokenBalanceForParty(t, voter, 100)
   457  
   458  	// expect
   459  	eng.expectDeclinedProposalEvent(t, proposal.ID, types.ProposalErrorParticipationThresholdNotReached)
   460  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "100")
   461  	eng.expectGetMarketState(t, proposal.ID)
   462  	// when
   463  	_, voteClosed := eng.OnTick(context.Background(), afterClosing)
   464  
   465  	// then
   466  	require.Len(t, voteClosed, 1)
   467  	vc := voteClosed[0]
   468  	require.NotNil(t, vc.NewMarket())
   469  	assert.True(t, vc.NewMarket().Rejected())
   470  
   471  	// given
   472  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
   473  
   474  	// when
   475  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
   476  
   477  	// then
   478  	assert.Empty(t, toBeEnacted)
   479  }
   480  
   481  func testInvalidSpotDecimalPlace(t *testing.T) {
   482  	eng := getTestEngine(t, time.Now())
   483  	defer eng.ctrl.Finish()
   484  
   485  	// given
   486  	party := eng.newValidParty("a-valid-party", 123456789)
   487  	proposal := eng.newProposalForNewSpotMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour))
   488  
   489  	proposal.Terms.GetNewSpotMarket().Changes.PriceDecimalPlaces = 12
   490  	proposal.Terms.GetNewSpotMarket().Changes.SizeDecimalPlaces = 7
   491  
   492  	// setup
   493  	eng.ensureAllAssetEnabled(t)
   494  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorTooManyMarketDecimalPlaces)
   495  
   496  	// when
   497  	_, err := eng.submitProposal(t, proposal)
   498  	require.Equal(t, "market decimal + position decimals must be less than or equal to asset decimals", err.Error())
   499  }
   500  
   501  func testInvalidSpotPositionDecimalPlace(t *testing.T) {
   502  	eng := getTestEngine(t, time.Now())
   503  	defer eng.ctrl.Finish()
   504  
   505  	// given
   506  	party := eng.newValidParty("a-valid-party", 123456789)
   507  	proposal := eng.newProposalForNewSpotMarket(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour))
   508  
   509  	proposal.Terms.GetNewSpotMarket().Changes.PriceDecimalPlaces = 0
   510  	proposal.Terms.GetNewSpotMarket().Changes.SizeDecimalPlaces = 6
   511  
   512  	// setup
   513  	spot := proposal.Terms.GetNewSpotMarket().Changes.Instrument.IntoProto().GetSpot()
   514  	eng.ensureAllAssetEnabledWithDP(t, spot.BaseAsset, spot.QuoteAsset, 5, 10)
   515  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidSizeDecimalPlaces)
   516  
   517  	// when
   518  	_, err := eng.submitProposal(t, proposal)
   519  	require.Equal(t, "number of position decimal places must be less than or equal to the number base asset decimal places", err.Error())
   520  }