code.vegaprotocol.io/vega@v0.79.0/core/governance/engine_batch_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/events"
    25  	"code.vegaprotocol.io/vega/core/governance"
    26  	"code.vegaprotocol.io/vega/core/netparams"
    27  	"code.vegaprotocol.io/vega/core/types"
    28  	"code.vegaprotocol.io/vega/libs/num"
    29  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  func TestSubmitBatchProposals(t *testing.T) {
    37  	t.Run("Submitted batch proposal is declined", testSubmittingBatchProposalDeclined)
    38  	t.Run("Submitted batch proposal has passed", testSubmittingBatchProposalPassed)
    39  	t.Run("Submitting batch fails if any of the terms fails validation", testSubmittingBatchProposalFailsWhenTermValidationFails)
    40  
    41  	t.Run("Voting with non-existing account fails", testVotingOnBatchWithNonExistingAccountFails)
    42  	t.Run("Voting without token fails", testVotingOnBatchWithoutTokenFails)
    43  }
    44  
    45  func testSubmittingBatchProposalDeclined(t *testing.T) {
    46  	eng := getTestEngine(t, time.Now())
    47  
    48  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
    49  	party := vgrand.RandomStr(5)
    50  
    51  	eng.ensureAllAssetEnabled(t)
    52  	eng.ensureTokenBalanceForParty(t, party, 1)
    53  
    54  	batchID := eng.newProposalID()
    55  
    56  	newFormProposal := eng.newFreeformProposal(party, now)
    57  	newNetParamProposal := eng.newProposalForNetParam(party, netparams.MarketAuctionMaximumDuration, "10h", now)
    58  	newMarketProposal := eng.newProposalForNewMarket(party, now, nil, nil, true)
    59  
    60  	// expect
    61  	eng.expectOpenProposalEvent(t, party, batchID)
    62  	eng.expectProposalEvents(t, []expectedProposal{
    63  		{
    64  			partyID:    party,
    65  			proposalID: newFormProposal.ID,
    66  			state:      types.ProposalStateOpen,
    67  			reason:     types.ProposalErrorUnspecified,
    68  		},
    69  		{
    70  			partyID:    party,
    71  			proposalID: newNetParamProposal.ID,
    72  			state:      types.ProposalStateOpen,
    73  			reason:     types.ProposalErrorUnspecified,
    74  		},
    75  		{
    76  			partyID:    party,
    77  			proposalID: newMarketProposal.ID,
    78  			state:      types.ProposalStateOpen,
    79  			reason:     types.ProposalErrorUnspecified,
    80  		},
    81  	})
    82  
    83  	batchClosingTime := now.Add(48 * time.Hour)
    84  
    85  	// when
    86  	_, err := eng.submitBatchProposal(t, eng.newBatchSubmission(
    87  		batchClosingTime.Unix(),
    88  		newFormProposal,
    89  		newNetParamProposal,
    90  		newMarketProposal,
    91  	), batchID, party)
    92  
    93  	assert.NoError(t, err)
    94  	ctx := context.Background()
    95  
    96  	eng.expectDeclinedProposalEvent(t, batchID, types.ProposalErrorProposalInBatchDeclined)
    97  	eng.expectProposalEvents(t, []expectedProposal{
    98  		{
    99  			partyID:    party,
   100  			proposalID: newFormProposal.ID,
   101  			state:      types.ProposalStateDeclined,
   102  			reason:     types.ProposalErrorParticipationThresholdNotReached,
   103  		},
   104  		{
   105  			partyID:    party,
   106  			proposalID: newNetParamProposal.ID,
   107  			state:      types.ProposalStateDeclined,
   108  			reason:     types.ProposalErrorParticipationThresholdNotReached,
   109  		},
   110  		{
   111  			partyID:    party,
   112  			proposalID: newMarketProposal.ID,
   113  			state:      types.ProposalStateDeclined,
   114  			reason:     types.ProposalErrorParticipationThresholdNotReached,
   115  		},
   116  	})
   117  
   118  	eng.accounts.EXPECT().GetStakingAssetTotalSupply().AnyTimes().Return(num.NewUint(200))
   119  	eng.OnTick(ctx, batchClosingTime.Add(1*time.Second))
   120  }
   121  
   122  func testSubmittingBatchProposalPassed(t *testing.T) {
   123  	eng := getTestEngine(t, time.Now())
   124  
   125  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   126  	party := vgrand.RandomStr(5)
   127  
   128  	eng.ensureAllAssetEnabled(t)
   129  	eng.ensureTokenBalanceForParty(t, party, 1)
   130  
   131  	batchID := eng.newProposalID()
   132  
   133  	newFormProposal := eng.newFreeformProposal(party, now)
   134  	newNetParamProposal := eng.newProposalForNetParam(party, netparams.MarketAuctionMaximumDuration, "10h", now)
   135  	newMarketProposal := eng.newProposalForNewMarket(party, now, nil, nil, true)
   136  
   137  	// expect
   138  	eng.expectOpenProposalEvent(t, party, batchID)
   139  	eng.expectProposalEvents(t, []expectedProposal{
   140  		{
   141  			partyID:    party,
   142  			proposalID: newFormProposal.ID,
   143  			state:      types.ProposalStateOpen,
   144  			reason:     types.ProposalErrorUnspecified,
   145  		},
   146  		{
   147  			partyID:    party,
   148  			proposalID: newNetParamProposal.ID,
   149  			state:      types.ProposalStateOpen,
   150  			reason:     types.ProposalErrorUnspecified,
   151  		},
   152  		{
   153  			partyID:    party,
   154  			proposalID: newMarketProposal.ID,
   155  			state:      types.ProposalStateOpen,
   156  			reason:     types.ProposalErrorUnspecified,
   157  		},
   158  	})
   159  
   160  	batchClosingTime := now.Add(48 * time.Hour)
   161  
   162  	// when
   163  	_, err := eng.submitBatchProposal(t, eng.newBatchSubmission(
   164  		batchClosingTime.Unix(),
   165  		newFormProposal,
   166  		newNetParamProposal,
   167  		newMarketProposal,
   168  	), batchID, party)
   169  
   170  	assert.NoError(t, err)
   171  	ctx := context.Background()
   172  
   173  	eng.accounts.EXPECT().GetStakingAssetTotalSupply().AnyTimes().Return(num.NewUint(200))
   174  
   175  	for i := 0; i < 10; i++ {
   176  		partyID := fmt.Sprintf("party-%d", i)
   177  		eng.ensureTokenBalanceForParty(t, partyID, 20)
   178  		eng.expectVoteEvent(t, partyID, batchID)
   179  		err = eng.addYesVote(t, partyID, batchID)
   180  		assert.NoError(t, err)
   181  	}
   182  
   183  	eng.expectPassedProposalEvent(t, batchID)
   184  
   185  	expectedProposals := []expectedProposal{
   186  		{
   187  			partyID:    party,
   188  			proposalID: newFormProposal.ID,
   189  			state:      types.ProposalStatePassed,
   190  		},
   191  		{
   192  			partyID:    party,
   193  			proposalID: newNetParamProposal.ID,
   194  			state:      types.ProposalStatePassed,
   195  		},
   196  		{
   197  			partyID:    party,
   198  			proposalID: newMarketProposal.ID,
   199  			state:      types.ProposalStatePassed,
   200  		},
   201  	}
   202  
   203  	eng.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(func(evts []events.Event) {
   204  		i := 0
   205  		for _, evt := range evts {
   206  			switch e := evt.(type) {
   207  			case *events.Proposal:
   208  				p := e.Proposal()
   209  				assert.Equal(t, expectedProposals[i].proposalID, p.Id)
   210  				assert.Equal(t, expectedProposals[i].partyID, p.PartyId)
   211  				assert.Equal(t, expectedProposals[i].state.String(), p.State.String())
   212  				i++
   213  			}
   214  		}
   215  	})
   216  
   217  	eng.OnTick(ctx, batchClosingTime.Add(1*time.Second))
   218  }
   219  
   220  func testSubmittingBatchProposalFailsWhenTermValidationFails(t *testing.T) {
   221  	eng := getTestEngine(t, time.Now())
   222  
   223  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   224  	party := vgrand.RandomStr(5)
   225  
   226  	newFormProposal := eng.newFreeformProposal(party, now)
   227  	newNetParamProposal := eng.newProposalForNetParam(party, netparams.MarketAuctionMaximumDuration, "10h", now)
   228  	newMarketProposal := eng.newProposalForNewMarket(party, now, nil, nil, true)
   229  	newMarketProposal.Terms.EnactmentTimestamp = time.Now().Unix()
   230  
   231  	newFormProposal2 := eng.newFreeformProposal(party, now)
   232  	newNetParamProposal2 := eng.newProposalForNetParam(party, netparams.MarketAuctionMaximumDuration, "10h", now)
   233  	newNetParamProposal2.Terms.EnactmentTimestamp = now.Add(24 * 365 * time.Hour).Unix()
   234  	newMarketProposal2 := eng.newProposalForNewMarket(party, now, nil, nil, true)
   235  
   236  	batchClosingTime := now.Add(48 * time.Hour).Unix()
   237  
   238  	cases := []struct {
   239  		msg            string
   240  		submission     types.BatchProposalSubmission
   241  		expectProposal []expectedProposal
   242  		containsError  string
   243  	}{
   244  		{
   245  			msg:           "New market rejected and other proposals with it",
   246  			containsError: "proposal enactment time too soon",
   247  			submission: eng.newBatchSubmission(
   248  				batchClosingTime,
   249  				newFormProposal,
   250  				newNetParamProposal,
   251  				newMarketProposal,
   252  			),
   253  			expectProposal: []expectedProposal{
   254  				{
   255  					partyID:    party,
   256  					proposalID: newFormProposal.ID,
   257  					state:      types.ProposalStateRejected,
   258  					reason:     types.ProposalErrorProposalInBatchRejected,
   259  				},
   260  				{
   261  					partyID:    party,
   262  					proposalID: newNetParamProposal.ID,
   263  					state:      types.ProposalStateRejected,
   264  					reason:     types.ProposalErrorProposalInBatchRejected,
   265  				},
   266  				{
   267  					partyID:    party,
   268  					proposalID: newMarketProposal.ID,
   269  					state:      types.ProposalStateRejected,
   270  					reason:     types.ProposalErrorEnactTimeTooSoon,
   271  				},
   272  			},
   273  		},
   274  		{
   275  			msg:           "Net parameter is rejected and the whole batch with it",
   276  			containsError: "proposal enactment time too late",
   277  			submission: eng.newBatchSubmission(
   278  				batchClosingTime,
   279  				newNetParamProposal2,
   280  				newFormProposal2,
   281  				newMarketProposal2,
   282  			),
   283  			expectProposal: []expectedProposal{
   284  				{
   285  					partyID:    party,
   286  					proposalID: newNetParamProposal2.ID,
   287  					state:      types.ProposalStateRejected,
   288  					reason:     types.ProposalErrorEnactTimeTooLate,
   289  				},
   290  				{
   291  					partyID:    party,
   292  					proposalID: newFormProposal2.ID,
   293  					state:      types.ProposalStateRejected,
   294  					reason:     types.ProposalErrorProposalInBatchRejected,
   295  				},
   296  				{
   297  					partyID:    party,
   298  					proposalID: newMarketProposal2.ID,
   299  					state:      types.ProposalStateRejected,
   300  					reason:     types.ProposalErrorProposalInBatchRejected,
   301  				},
   302  			},
   303  		},
   304  	}
   305  
   306  	for _, tc := range cases {
   307  		t.Run(tc.msg, func(tt *testing.T) {
   308  			// setup
   309  			eng.ensureAllAssetEnabled(tt)
   310  			eng.ensureTokenBalanceForParty(t, party, 1)
   311  
   312  			batchID := eng.newProposalID()
   313  
   314  			// expect
   315  			eng.expectRejectedProposalEvent(tt, party, batchID, types.ProposalErrorProposalInBatchRejected)
   316  			eng.expectProposalEvents(tt, tc.expectProposal)
   317  
   318  			// when
   319  			_, err := eng.submitBatchProposal(tt, tc.submission, batchID, party)
   320  
   321  			// then
   322  			require.Error(tt, err)
   323  			if tc.containsError != "" {
   324  				assert.Contains(tt, err.Error(), tc.containsError)
   325  			}
   326  		})
   327  	}
   328  }
   329  
   330  func testVotingOnBatchWithNonExistingAccountFails(t *testing.T) {
   331  	eng := getTestEngine(t, time.Now())
   332  
   333  	// given
   334  	proposer := vgrand.RandomStr(5)
   335  	proposal := eng.newProposalForNewMarket(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
   336  
   337  	// setup
   338  	eng.ensureAllAssetEnabled(t)
   339  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   340  
   341  	batchID := eng.newProposalID()
   342  
   343  	// expect
   344  	eng.expectOpenProposalEvent(t, proposer, batchID)
   345  	eng.expectProposalEvents(t, []expectedProposal{
   346  		{
   347  			partyID:    proposer,
   348  			proposalID: proposal.ID,
   349  			state:      types.ProposalStateOpen,
   350  		},
   351  	})
   352  
   353  	// when
   354  	sub := eng.newBatchSubmission(
   355  		proposal.Terms.ClosingTimestamp,
   356  		proposal,
   357  	)
   358  	_, err := eng.submitBatchProposal(t, sub, batchID, proposer)
   359  
   360  	// then
   361  	require.NoError(t, err)
   362  
   363  	// given
   364  	voterWithoutAccount := "voter-no-account"
   365  
   366  	// setup
   367  	eng.ensureNoAccountForParty(t, voterWithoutAccount)
   368  
   369  	// when
   370  	err = eng.addYesVote(t, voterWithoutAccount, batchID)
   371  
   372  	// then
   373  	require.Error(t, err)
   374  	assert.ErrorContains(t, err, "no balance for party")
   375  }
   376  
   377  func testVotingOnBatchWithoutTokenFails(t *testing.T) {
   378  	eng := getTestEngine(t, time.Now())
   379  
   380  	// given
   381  	proposer := eng.newValidParty("proposer", 1)
   382  	proposal := eng.newProposalForNewMarket(proposer.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour), nil, nil, true)
   383  
   384  	// setup
   385  	batchID := eng.newProposalID()
   386  
   387  	eng.ensureAllAssetEnabled(t)
   388  	eng.expectOpenProposalEvent(t, proposer.Id, batchID)
   389  	eng.expectProposalEvents(t, []expectedProposal{
   390  		{
   391  			partyID:    proposer.Id,
   392  			proposalID: proposal.ID,
   393  			state:      types.ProposalStateOpen,
   394  		},
   395  	})
   396  
   397  	// when
   398  	sub := eng.newBatchSubmission(
   399  		proposal.Terms.ClosingTimestamp,
   400  		proposal,
   401  	)
   402  	_, err := eng.submitBatchProposal(t, sub, batchID, proposer.Id)
   403  
   404  	// then
   405  	require.NoError(t, err)
   406  
   407  	// given
   408  	voterWithEmptyAccount := vgrand.RandomStr(5)
   409  
   410  	// setup
   411  	eng.ensureTokenBalanceForParty(t, voterWithEmptyAccount, 0)
   412  
   413  	// when
   414  	err = eng.addYesVote(t, voterWithEmptyAccount, batchID)
   415  
   416  	// then
   417  	require.Error(t, err)
   418  	assert.ErrorContains(t, err, governance.ErrVoterInsufficientTokens.Error())
   419  }