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 }