code.vegaprotocol.io/vega@v0.79.0/core/governance/engine_new_asset_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/assets"
    24  	"code.vegaprotocol.io/vega/core/assets/builtin"
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/governance"
    27  	"code.vegaprotocol.io/vega/core/netparams"
    28  	"code.vegaprotocol.io/vega/core/types"
    29  	"code.vegaprotocol.io/vega/core/validators"
    30  	"code.vegaprotocol.io/vega/libs/num"
    31  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    32  
    33  	"github.com/golang/mock/gomock"
    34  	"github.com/stretchr/testify/assert"
    35  	"github.com/stretchr/testify/require"
    36  )
    37  
    38  func TestProposalForNewAsset(t *testing.T) {
    39  	t.Run("Submitting a proposal for new asset succeeds", testSubmittingProposalForNewAssetSucceeds)
    40  	t.Run("Submitting a proposal for new asset with closing time before validation time fails", testSubmittingProposalForNewAssetWithClosingTimeBeforeValidationTimeFails)
    41  	t.Run("Voting during validation of proposal for new asset succeeds", testVotingDuringValidationOfProposalForNewAssetSucceeds)
    42  	t.Run("Rejects erc20 proposals for address already used", testRejectsERC20ProposalForAddressAlreadyUsed)
    43  	t.Run("Rejects erc20 proposals for unknown chain id", testRejectsERC20ProposalForUnknownChainID)
    44  }
    45  
    46  func testRejectsERC20ProposalForAddressAlreadyUsed(t *testing.T) {
    47  	eng := getTestEngine(t, time.Now())
    48  
    49  	// given
    50  	party := eng.newValidParty("a-valid-party", 123456789)
    51  	proposal := eng.newProposalForNewAsset(party.Id, eng.tsvc.GetTimeNow().Add(48*time.Hour))
    52  
    53  	newAssetERC20 := newAssetTerms()
    54  	newAssetERC20.NewAsset.Changes.Source = &types.AssetDetailsErc20{
    55  		ERC20: &types.ERC20{
    56  			ContractAddress:   "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990",
    57  			LifetimeLimit:     num.NewUint(1),
    58  			WithdrawThreshold: num.NewUint(1),
    59  			ChainID:           "1",
    60  		},
    61  	}
    62  	proposal.Terms.Change = newAssetERC20
    63  
    64  	// setup
    65  	eng.assets.EXPECT().ValidateEthereumAddress("0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990", "1").Times(1).Return(assets.ErrErc20AddressAlreadyInUse)
    66  
    67  	// setup
    68  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorERC20AddressAlreadyInUse)
    69  
    70  	// when
    71  	toSubmit, err := eng.submitProposal(t, proposal)
    72  
    73  	require.EqualError(t, err, assets.ErrErc20AddressAlreadyInUse.Error())
    74  	require.Nil(t, toSubmit)
    75  }
    76  
    77  func testSubmittingProposalForNewAssetSucceeds(t *testing.T) {
    78  	eng := getTestEngine(t, time.Now())
    79  
    80  	// given
    81  	party := eng.newValidParty("a-valid-party", 123456789)
    82  	proposal := eng.newProposalForNewAsset(party.Id, eng.tsvc.GetTimeNow().Add(2*time.Hour))
    83  
    84  	// setup
    85  	eng.assets.EXPECT().NewAsset(gomock.Any(), proposal.ID, gomock.Any()).Times(1).Return(proposal.ID, nil)
    86  	eng.witness.EXPECT().StartCheck(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil)
    87  
    88  	// expect
    89  	eng.expectProposalWaitingForNodeVoteEvent(t, party.Id, proposal.ID)
    90  
    91  	// when
    92  	toSubmit, err := eng.submitProposal(t, proposal)
    93  
    94  	// then
    95  	require.NoError(t, err)
    96  	require.NotNil(t, toSubmit)
    97  	assert.False(t, toSubmit.IsNewMarket())
    98  	require.Nil(t, toSubmit.NewMarket())
    99  }
   100  
   101  func testSubmittingProposalForNewAssetWithClosingTimeBeforeValidationTimeFails(t *testing.T) {
   102  	eng := getTestEngine(t, time.Now())
   103  
   104  	// given
   105  	party := vgrand.RandomStr(5)
   106  	proposal := eng.newProposalForNewAsset(party, eng.tsvc.GetTimeNow().Add(48*time.Hour))
   107  	proposal.Terms.ValidationTimestamp = proposal.Terms.ClosingTimestamp + 10
   108  
   109  	// setup
   110  	eng.expectRejectedProposalEvent(t, party, proposal.ID, types.ProposalErrorIncompatibleTimestamps)
   111  
   112  	// when
   113  	_, err := eng.submitProposal(t, proposal)
   114  
   115  	// then
   116  	require.Error(t, err)
   117  	assert.Contains(t, err.Error(), "proposal closing time cannot be before validation time, expected >")
   118  }
   119  
   120  func testVotingDuringValidationOfProposalForNewAssetSucceeds(t *testing.T) {
   121  	eng := getTestEngine(t, time.Now())
   122  
   123  	// when
   124  	proposer := vgrand.RandomStr(5)
   125  	proposal := eng.newProposalForNewAsset(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour))
   126  
   127  	// setup
   128  	var bAsset *assets.Asset
   129  	var fcheck func(interface{}, bool)
   130  	var rescheck validators.Resource
   131  	eng.assets.EXPECT().NewAsset(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, ref string, assetDetails *types.AssetDetails) (string, error) {
   132  		bAsset = assets.NewAsset(builtin.New(ref, assetDetails))
   133  		return ref, nil
   134  	})
   135  	eng.assets.EXPECT().Get(gomock.Any()).Times(1).DoAndReturn(func(id string) (*assets.Asset, error) {
   136  		return bAsset, nil
   137  	})
   138  	eng.witness.EXPECT().StartCheck(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Do(func(r validators.Resource, f func(interface{}, bool), _ time.Time) error {
   139  		fcheck = f
   140  		rescheck = r
   141  		return nil
   142  	})
   143  	eng.ensureStakingAssetTotalSupply(t, 9)
   144  	eng.ensureAllAssetEnabled(t)
   145  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   146  
   147  	// expect
   148  	eng.expectProposalWaitingForNodeVoteEvent(t, proposer, proposal.ID)
   149  
   150  	// when
   151  	_, err := eng.submitProposal(t, proposal)
   152  
   153  	// then
   154  	require.NoError(t, err)
   155  
   156  	// given
   157  	voter1 := vgrand.RandomStr(5)
   158  
   159  	// setup
   160  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   161  
   162  	// expect
   163  	eng.expectVoteEvent(t, voter1, proposal.ID)
   164  
   165  	// then
   166  	err = eng.addYesVote(t, voter1, proposal.ID)
   167  
   168  	// call success on the validation
   169  	fcheck(rescheck, true)
   170  
   171  	// then
   172  	require.NoError(t, err)
   173  	afterValidation := time.Unix(proposal.Terms.ValidationTimestamp, 0).Add(time.Second)
   174  
   175  	// setup
   176  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   177  
   178  	// expect
   179  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   180  	eng.expectGetMarketState(t, proposal.ID)
   181  
   182  	// when
   183  	eng.OnTick(context.Background(), afterValidation)
   184  
   185  	// given
   186  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   187  
   188  	// expect
   189  	eng.expectPassedProposalEvent(t, proposal.ID)
   190  	eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7")
   191  	eng.assets.EXPECT().SetPendingListing(gomock.Any(), proposal.ID).Times(1)
   192  
   193  	// when
   194  	eng.OnTick(context.Background(), afterClosing)
   195  
   196  	// given
   197  	voter2 := vgrand.RandomStr(5)
   198  
   199  	// when
   200  	err = eng.addNoVote(t, voter2, proposal.ID)
   201  
   202  	// then
   203  	require.Error(t, err)
   204  	assert.EqualError(t, err, governance.ErrProposalNotOpenForVotes.Error())
   205  
   206  	// given
   207  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
   208  
   209  	// when
   210  	// no calculations, no state change, simply removed from governance engine
   211  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
   212  
   213  	// then
   214  	require.Len(t, toBeEnacted, 1)
   215  	assert.Equal(t, proposal.ID, toBeEnacted[0].Proposal().ID)
   216  
   217  	// when
   218  	err = eng.addNoVote(t, voter2, proposal.ID)
   219  
   220  	// then
   221  	require.Error(t, err)
   222  	assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
   223  }
   224  
   225  func TestVotingDuringValidationOfProposalForNewAssetInBatchSucceeds(t *testing.T) {
   226  	eng := getTestEngine(t, time.Now())
   227  
   228  	now := eng.tsvc.GetTimeNow().Add(2 * time.Hour)
   229  
   230  	// when
   231  	proposer := vgrand.RandomStr(5)
   232  	batchID := eng.newProposalID()
   233  
   234  	newAssetProposal := eng.newProposalForNewAsset(proposer, now)
   235  
   236  	// setup
   237  	var bAsset *assets.Asset
   238  	var fcheck func(interface{}, bool)
   239  	var rescheck validators.Resource
   240  	eng.assets.EXPECT().NewAsset(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, ref string, assetDetails *types.AssetDetails) (string, error) {
   241  		bAsset = assets.NewAsset(builtin.New(ref, assetDetails))
   242  		return ref, nil
   243  	})
   244  	eng.assets.EXPECT().Get(gomock.Any()).Times(1).DoAndReturn(func(id string) (*assets.Asset, error) {
   245  		return bAsset, nil
   246  	})
   247  	eng.witness.EXPECT().StartCheck(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Do(func(r validators.Resource, f func(interface{}, bool), _ time.Time) error {
   248  		fcheck = f
   249  		rescheck = r
   250  		return nil
   251  	})
   252  	eng.ensureStakingAssetTotalSupply(t, 9)
   253  	eng.ensureAllAssetEnabled(t)
   254  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   255  
   256  	// expect
   257  	eng.expectProposalWaitingForNodeVoteEvent(t, proposer, batchID)
   258  
   259  	// eng.expectOpenProposalEvent(t, proposer, batchID)
   260  	eng.expectProposalEvents(t, []expectedProposal{
   261  		{
   262  			partyID:    proposer,
   263  			proposalID: newAssetProposal.ID,
   264  			state:      types.ProposalStateWaitingForNodeVote,
   265  			reason:     types.ProposalErrorUnspecified,
   266  		},
   267  	})
   268  
   269  	batchClosingTime := now.Add(48 * time.Hour)
   270  
   271  	// when
   272  	_, err := eng.submitBatchProposal(t, eng.newBatchSubmission(
   273  		batchClosingTime.Unix(),
   274  		newAssetProposal,
   275  	), batchID, proposer)
   276  
   277  	assert.NoError(t, err)
   278  
   279  	// given
   280  	voter1 := vgrand.RandomStr(5)
   281  
   282  	// setup
   283  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   284  
   285  	// expect
   286  	eng.expectVoteEvent(t, voter1, batchID)
   287  
   288  	// then
   289  	err = eng.addYesVote(t, voter1, batchID)
   290  	require.NoError(t, err)
   291  
   292  	_ = fcheck
   293  	_ = rescheck
   294  	// call success on the validation
   295  	fcheck(rescheck, true)
   296  
   297  	// then
   298  	afterValidation := time.Unix(newAssetProposal.Terms.ValidationTimestamp, 0).Add(time.Second)
   299  
   300  	// setup
   301  	eng.ensureTokenBalanceForParty(t, voter1, 7)
   302  
   303  	// expect
   304  	eng.expectOpenProposalEvent(t, proposer, batchID)
   305  	// and the inner proposals
   306  	eng.broker.EXPECT().SendBatch(gomock.Any()).Times(1)
   307  	// eng.expectGetMarketState(t, ID)
   308  
   309  	// when
   310  	eng.OnTick(context.Background(), afterValidation)
   311  
   312  	// given
   313  	afterClosing := time.Unix(newAssetProposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   314  
   315  	// // expect
   316  	eng.expectPassedProposalEvent(t, batchID)
   317  	// and the inner proposals
   318  	eng.broker.EXPECT().SendBatch(gomock.Any()).Times(1)
   319  
   320  	// // when
   321  	eng.OnTick(context.Background(), afterClosing)
   322  
   323  	eng.assets.EXPECT().SetPendingListing(gomock.Any(), newAssetProposal.ID).Times(1)
   324  	eng.OnTick(context.Background(), afterClosing.Add(1*time.Minute))
   325  
   326  	// when
   327  	toBeEnacted, _ := eng.OnTick(context.Background(), afterClosing.Add(48*time.Hour))
   328  	require.Len(t, toBeEnacted, 1)
   329  }
   330  
   331  func TestNoVotesAnd0RequiredFails(t *testing.T) {
   332  	eng := getTestEngine(t, time.Now())
   333  
   334  	ctx := context.Background()
   335  	eng.broker.EXPECT().Send(events.NewNetworkParameterEvent(ctx, netparams.GovernanceProposalAssetRequiredParticipation, "0")).Times(1)
   336  	assert.NoError(t,
   337  		eng.netp.Update(ctx,
   338  			"governance.proposal.asset.requiredParticipation",
   339  			"0",
   340  		),
   341  	)
   342  
   343  	// when
   344  	proposer := vgrand.RandomStr(5)
   345  	proposal := eng.newProposalForNewAsset(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour))
   346  
   347  	// setup
   348  	var fcheck func(interface{}, bool)
   349  	var rescheck validators.Resource
   350  	eng.assets.EXPECT().NewAsset(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, ref string, assetDetails *types.AssetDetails) (string, error) {
   351  		return ref, nil
   352  	})
   353  	eng.witness.EXPECT().StartCheck(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Do(func(r validators.Resource, f func(interface{}, bool), _ time.Time) error {
   354  		fcheck = f
   355  		rescheck = r
   356  		return nil
   357  	})
   358  	eng.ensureStakingAssetTotalSupply(t, 9)
   359  	eng.ensureAllAssetEnabled(t)
   360  	eng.ensureTokenBalanceForParty(t, proposer, 1)
   361  
   362  	// expect
   363  	eng.expectProposalWaitingForNodeVoteEvent(t, proposer, proposal.ID)
   364  
   365  	// when
   366  	_, err := eng.submitProposal(t, proposal)
   367  
   368  	// then
   369  	require.NoError(t, err)
   370  
   371  	// call success on the validation
   372  	fcheck(rescheck, true)
   373  
   374  	// then
   375  	require.NoError(t, err)
   376  	afterValidation := time.Unix(proposal.Terms.ValidationTimestamp, 0).Add(time.Second)
   377  
   378  	// setup
   379  	// eng.ensureTokenBalanceForParty(t, voter1, 7)
   380  
   381  	// expect
   382  	eng.expectOpenProposalEvent(t, proposer, proposal.ID)
   383  	eng.expectGetMarketState(t, proposal.ID)
   384  
   385  	// when
   386  	eng.OnTick(context.Background(), afterValidation)
   387  
   388  	// given
   389  	afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)
   390  
   391  	// expect
   392  	eng.expectDeclinedProposalEvent(t, proposal.ID, types.ProposalErrorParticipationThresholdNotReached)
   393  	// empty list of votes
   394  	eng.broker.EXPECT().SendBatch(gomock.Any()).Times(1)
   395  
   396  	eng.assets.EXPECT().SetRejected(gomock.Any(), proposal.ID).Times(1)
   397  
   398  	// when
   399  	eng.OnTick(context.Background(), afterClosing)
   400  
   401  	// given
   402  	afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)
   403  
   404  	// when
   405  	// no calculations, no state change, simply removed from governance engine
   406  	toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)
   407  
   408  	// then
   409  	require.Len(t, toBeEnacted, 0)
   410  }
   411  
   412  func testRejectsERC20ProposalForUnknownChainID(t *testing.T) {
   413  	eng := getTestEngine(t, time.Now())
   414  
   415  	// given
   416  	party := eng.newValidParty("a-valid-party", 123456789)
   417  	proposal := eng.newProposalForNewAsset(party.Id, eng.tsvc.GetTimeNow().Add(48*time.Hour))
   418  
   419  	newAssetERC20 := newAssetTerms()
   420  	newAssetERC20.NewAsset.Changes.Source = &types.AssetDetailsErc20{
   421  		ERC20: &types.ERC20{
   422  			ContractAddress:   "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990",
   423  			LifetimeLimit:     num.NewUint(1),
   424  			WithdrawThreshold: num.NewUint(1),
   425  			ChainID:           "666",
   426  		},
   427  	}
   428  	proposal.Terms.Change = newAssetERC20
   429  
   430  	// setup
   431  	eng.assets.EXPECT().ValidateEthereumAddress("0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990", "666").Times(1).Return(assets.ErrUnknownChainID)
   432  
   433  	// setup
   434  	eng.expectRejectedProposalEvent(t, party.Id, proposal.ID, types.ProposalErrorInvalidAssetDetails)
   435  
   436  	// when
   437  	toSubmit, err := eng.submitProposal(t, proposal)
   438  
   439  	require.EqualError(t, err, assets.ErrUnknownChainID.Error())
   440  	require.Nil(t, toSubmit)
   441  }