github.com/lino-network/lino@v0.6.11/x/vote/manager/manager_test.go (about) 1 package manager 2 3 import ( 4 "io/ioutil" 5 "os" 6 "path/filepath" 7 "testing" 8 "time" 9 10 codec "github.com/cosmos/cosmos-sdk/codec" 11 sdk "github.com/cosmos/cosmos-sdk/types" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/suite" 14 15 parammodel "github.com/lino-network/lino/param" 16 param "github.com/lino-network/lino/param/mocks" 17 "github.com/lino-network/lino/testsuites" 18 "github.com/lino-network/lino/testutils" 19 linotypes "github.com/lino-network/lino/types" 20 accmn "github.com/lino-network/lino/x/account/manager" 21 acc "github.com/lino-network/lino/x/account/mocks" 22 global "github.com/lino-network/lino/x/global/mocks" 23 hk "github.com/lino-network/lino/x/vote/manager/mocks" 24 "github.com/lino-network/lino/x/vote/model" 25 "github.com/lino-network/lino/x/vote/types" 26 ) 27 28 // background: 29 // 3 voters plus a userPendingDuty(with pending duty). 30 // user1 has 2000, staked in on day 0 31 // user2 has 1000, staked in on day 1, 100 interests unsettled. 32 // user3 has 1000 and 1000 frozen, validator 33 // friction day0 888, 34 // friction day1 999. 35 // all units in LINO. 36 37 var ( 38 storeKeyStr = "testVoterStore" 39 kvStoreKey = sdk.NewKVStoreKey(storeKeyStr) 40 ) 41 42 type VoteStoreDumper struct{} 43 44 func (dumper VoteStoreDumper) NewDumper() *testutils.Dumper { 45 return model.NewVoteDumper(model.NewVoteStorage(kvStoreKey)) 46 } 47 48 type VoteManagerTestSuite struct { 49 testsuites.GoldenTestSuite 50 vm VoteManager 51 ph *param.ParamKeeper 52 am *acc.AccountKeeper 53 global *global.GlobalKeeper 54 hooks *hk.StakingHooks 55 56 // for common/3voters.input 57 user1 linotypes.AccountKey 58 user2 linotypes.AccountKey 59 user3 linotypes.AccountKey 60 userNotVoter linotypes.AccountKey 61 userPendingDuty linotypes.AccountKey 62 63 // example data 64 minStakeInAmount linotypes.Coin 65 returnIntervalSec int64 66 returnTimes int64 67 } 68 69 func TestVoteManagerTestSuite(t *testing.T) { 70 suite.Run(t, &VoteManagerTestSuite{ 71 GoldenTestSuite: testsuites.NewGoldenTestSuite(VoteStoreDumper{}, kvStoreKey), 72 }) 73 } 74 75 func (suite *VoteManagerTestSuite) SetupTest() { 76 suite.SetupCtx(linotypes.Upgrade5Update1, time.Unix(0, 0), kvStoreKey) 77 suite.user1 = linotypes.AccountKey("user1") 78 suite.user2 = linotypes.AccountKey("user2") 79 suite.user3 = linotypes.AccountKey("user3") 80 suite.userNotVoter = linotypes.AccountKey("notavoter") 81 suite.userPendingDuty = linotypes.AccountKey("pendingdutyuser") 82 suite.am = &acc.AccountKeeper{} 83 suite.ph = ¶m.ParamKeeper{} 84 suite.global = &global.GlobalKeeper{} 85 suite.hooks = &hk.StakingHooks{} 86 suite.vm = NewVoteManager(kvStoreKey, suite.ph, suite.am, suite.global) 87 suite.vm = *suite.vm.SetHooks(suite.hooks) 88 89 suite.minStakeInAmount = linotypes.NewCoinFromInt64(1000 * linotypes.Decimals) 90 suite.returnIntervalSec = 100 91 suite.returnTimes = 1 92 suite.ph.On("GetVoteParam", mock.Anything).Return(¶mmodel.VoteParam{ 93 MinStakeIn: suite.minStakeInAmount, 94 VoterCoinReturnIntervalSec: suite.returnIntervalSec, 95 VoterCoinReturnTimes: suite.returnTimes, 96 }).Maybe() 97 // set initial stake stats for day 0. 98 suite.vm.InitGenesis(suite.Ctx) 99 } 100 101 func (suite *VoteManagerTestSuite) ResetGlobal() { 102 suite.global = &global.GlobalKeeper{} 103 suite.vm = NewVoteManager(kvStoreKey, suite.ph, suite.am, suite.global) 104 suite.vm = *suite.vm.SetHooks(suite.hooks) 105 } 106 107 func (suite *VoteManagerTestSuite) ResetParam() { 108 suite.ph = ¶m.ParamKeeper{} 109 suite.vm = NewVoteManager(kvStoreKey, suite.ph, suite.am, suite.global) 110 suite.vm = *suite.vm.SetHooks(suite.hooks) 111 } 112 113 func (suite *VoteManagerTestSuite) TestStakeIn() { 114 user1 := linotypes.AccountKey("user1") 115 116 testCases := []struct { 117 testName string 118 username linotypes.AccountKey 119 amount linotypes.Coin 120 lessThanMin bool 121 moveErr sdk.Error 122 atWhen time.Time 123 expectErr sdk.Error 124 expectVoter *model.Voter 125 expetecStats *model.LinoStakeStat 126 }{ 127 { 128 testName: "stake in amount less than minimum requirement", 129 username: user1, 130 amount: suite.minStakeInAmount.Minus(linotypes.NewCoinFromInt64(1)), 131 lessThanMin: true, 132 expectErr: types.ErrInsufficientDeposit(), 133 }, 134 { 135 testName: "stake in with insufficient balance", 136 username: user1, 137 amount: suite.minStakeInAmount, 138 moveErr: linotypes.ErrTestDummyError(), // just a mock, any error is fine 139 expectErr: linotypes.ErrTestDummyError(), 140 }, 141 { 142 testName: "stake in minimum requirement", 143 username: user1, 144 amount: suite.minStakeInAmount, 145 atWhen: time.Unix(100, 0), 146 expectErr: nil, 147 expectVoter: &model.Voter{ 148 Username: user1, 149 LinoStake: suite.minStakeInAmount, 150 Interest: linotypes.NewCoinFromInt64(0), 151 Duty: types.DutyVoter, 152 FrozenAmount: linotypes.NewCoinFromInt64(0), 153 LastPowerChangeAt: 100, 154 }, 155 expetecStats: &model.LinoStakeStat{ 156 TotalConsumptionFriction: linotypes.NewCoinFromInt64(0), 157 UnclaimedFriction: linotypes.NewCoinFromInt64(0), 158 TotalLinoStake: suite.minStakeInAmount, 159 UnclaimedLinoStake: suite.minStakeInAmount, 160 }, 161 }, 162 } 163 164 for _, tc := range testCases { 165 suite.Run(tc.testName, func() { 166 suite.hooks.On("AfterAddingStake", mock.Anything, tc.username).Return(nil).Maybe() 167 suite.NextBlock(tc.atWhen) 168 if !tc.lessThanMin { 169 suite.am.On("MoveToPool", mock.Anything, linotypes.VoteStakeInPool, 170 linotypes.NewAccOrAddrFromAcc(tc.username), tc.amount).Return(tc.moveErr).Once() 171 } 172 suite.global.On("GetPastDay", mock.Anything, tc.atWhen.Unix()).Return(int64(0)).Maybe() 173 err := suite.vm.StakeIn(suite.Ctx, tc.username, tc.amount) 174 suite.Equal(tc.expectErr, err) 175 if tc.expectErr == nil { 176 voter, err := suite.vm.GetVoter(suite.Ctx, tc.username) 177 suite.Nil(err) 178 suite.Equal(tc.expectVoter, voter) 179 stats, err := suite.vm.storage.GetLinoStakeStat(suite.Ctx, 0) 180 suite.Nil(err) 181 suite.Equal(tc.expetecStats, stats) 182 } 183 184 suite.global.AssertExpectations(suite.T()) 185 suite.am.AssertExpectations(suite.T()) 186 187 suite.Golden() 188 }) 189 } 190 } 191 192 func (suite *VoteManagerTestSuite) TestStakeInFor() { 193 user1 := linotypes.AccountKey("user1") 194 user2 := linotypes.AccountKey("user2") 195 196 testCases := []struct { 197 testName string 198 username linotypes.AccountKey 199 stakeInFor linotypes.AccountKey 200 amount linotypes.Coin 201 lessThanMin bool 202 moveErr sdk.Error 203 atWhen time.Time 204 expectErr sdk.Error 205 expectVoter *model.Voter 206 expetecStats *model.LinoStakeStat 207 }{ 208 { 209 testName: "stake in amount less than minimum requirement", 210 username: user1, 211 stakeInFor: user2, 212 amount: suite.minStakeInAmount.Minus(linotypes.NewCoinFromInt64(1)), 213 lessThanMin: true, 214 expectErr: types.ErrInsufficientDeposit(), 215 }, 216 { 217 testName: "stake in with insufficient balance", 218 username: user1, 219 stakeInFor: user2, 220 amount: suite.minStakeInAmount, 221 moveErr: linotypes.ErrTestDummyError(), // just a mock, any error is fine 222 expectErr: linotypes.ErrTestDummyError(), 223 }, 224 { 225 testName: "stake in minimum requirement", 226 username: user1, 227 stakeInFor: user2, 228 amount: suite.minStakeInAmount, 229 atWhen: time.Unix(100, 0), 230 expectErr: nil, 231 expectVoter: &model.Voter{ 232 Username: user2, 233 LinoStake: suite.minStakeInAmount, 234 Interest: linotypes.NewCoinFromInt64(0), 235 Duty: types.DutyVoter, 236 FrozenAmount: linotypes.NewCoinFromInt64(0), 237 LastPowerChangeAt: 100, 238 }, 239 expetecStats: &model.LinoStakeStat{ 240 TotalConsumptionFriction: linotypes.NewCoinFromInt64(0), 241 UnclaimedFriction: linotypes.NewCoinFromInt64(0), 242 TotalLinoStake: suite.minStakeInAmount, 243 UnclaimedLinoStake: suite.minStakeInAmount, 244 }, 245 }, 246 } 247 248 for _, tc := range testCases { 249 suite.Run(tc.testName, func() { 250 suite.hooks.On("AfterAddingStake", mock.Anything, tc.stakeInFor).Return(nil).Maybe() 251 suite.NextBlock(tc.atWhen) 252 if !tc.lessThanMin { 253 suite.am.On("MoveToPool", mock.Anything, linotypes.VoteStakeInPool, 254 linotypes.NewAccOrAddrFromAcc(tc.username), tc.amount).Return(tc.moveErr).Once() 255 } 256 suite.global.On("GetPastDay", mock.Anything, int64(100)).Return(int64(0)).Maybe() 257 err := suite.vm.StakeInFor(suite.Ctx, tc.username, tc.stakeInFor, tc.amount) 258 suite.Equal(tc.expectErr, err) 259 if tc.expectErr == nil { 260 _, err := suite.vm.GetVoter(suite.Ctx, tc.username) 261 suite.NotNil(err) 262 voter, err := suite.vm.GetVoter(suite.Ctx, tc.stakeInFor) 263 suite.Nil(err) 264 suite.Equal(tc.expectVoter, voter) 265 stats, err := suite.vm.storage.GetLinoStakeStat(suite.Ctx, 0) 266 suite.Nil(err) 267 suite.Equal(tc.expetecStats, stats) 268 } 269 270 suite.global.AssertExpectations(suite.T()) 271 suite.am.AssertExpectations(suite.T()) 272 273 suite.Golden() 274 }) 275 } 276 277 } 278 279 // TestMultipleStakeInWithConsumption 280 // script: (0.claim. 1.stake-in 2.consumption 3.end of day) 281 // user1 user2 consumption user1-calim user2-claim 282 // day0 1000 0 222 x x 283 // day1 2000 2000 444 222 x 284 // day2 -2000 -1000 888 x x 285 // day3 0 0 100 x x 286 // day4 2000 2000 500 760 672 287 // day5 0 1000 300 250 250 // all claimed 288 // day6 2000 2000 0 x 171 // 300 * (4/7) 289 // day7 0 0 300 129 0 // 1 - 300 * (4/7) 290 func (suite *VoteManagerTestSuite) TestMultipleStakeInWithConsumption() { 291 // setup global for PastDay calculation 292 suite.ResetGlobal() 293 for i := 0; i <= 100; i++ { 294 suite.global.On("GetPastDay", mock.Anything, int64(i)).Return(int64(i)) 295 } 296 suite.global.On("RegisterEventAtTime", 297 mock.Anything, mock.Anything, mock.Anything).Return(nil) 298 299 suite.ResetParam() 300 suite.ph.On("GetVoteParam", mock.Anything).Return(¶mmodel.VoteParam{ 301 MinStakeIn: linotypes.NewCoinFromInt64(1), 302 VoterCoinReturnIntervalSec: 100, 303 VoterCoinReturnTimes: 1, 304 }).Maybe() 305 306 user1 := linotypes.AccountKey("user1") 307 user2 := linotypes.AccountKey("user2") 308 suite.hooks.On("AfterAddingStake", mock.Anything, mock.Anything).Return(nil).Maybe() 309 suite.hooks.On("AfterSubtractingStake", mock.Anything, mock.Anything).Return(nil).Maybe() 310 suite.am.On("AddFrozenMoney", mock.Anything, mock.Anything, 311 mock.Anything, mock.Anything, int64(100), int64(1)).Return(nil) 312 // linotypes.VoteReturnCoin, linotypes.VoteStakeReturnPool 313 suite.am.On("MoveToPool", mock.Anything, linotypes.VoteStakeInPool, 314 mock.Anything, mock.Anything).Return(nil) 315 suite.am.On("MoveBetweenPools", mock.Anything, 316 linotypes.VoteStakeInPool, linotypes.VoteStakeReturnPool, mock.Anything).Return(nil) 317 318 for i, tc := range []struct { 319 user1claim *linotypes.Coin 320 user2claim *linotypes.Coin 321 user1stakein *linotypes.Coin 322 user2stakein *linotypes.Coin 323 consumption linotypes.Coin 324 }{ 325 { 326 // day0 327 user1stakein: newCoin(1000), 328 consumption: *newCoin(222), 329 }, 330 { 331 // day1 332 user1claim: newCoin(222), 333 user1stakein: newCoin(2000), 334 user2stakein: newCoin(2000), 335 consumption: *newCoin(444), 336 }, 337 { 338 // day2 339 user1stakein: newCoin(-2000), 340 user2stakein: newCoin(-1000), 341 consumption: *newCoin(888), 342 }, 343 { 344 // day3 345 consumption: *newCoin(100), 346 }, 347 { 348 349 // day4 350 user1claim: newCoin(760), 351 user2claim: newCoin(672), 352 user1stakein: newCoin(2000), 353 user2stakein: newCoin(2000), 354 consumption: *newCoin(500), 355 }, 356 { 357 // day5 358 user1claim: newCoin(250), 359 user2claim: newCoin(250), 360 user2stakein: newCoin(1000), 361 consumption: *newCoin(300), 362 }, 363 { 364 // day6 365 user2claim: newCoin(171), 366 user1stakein: newCoin(2000), 367 user2stakein: newCoin(2000), 368 consumption: *newCoin(0), 369 }, 370 { 371 // day7 372 user1claim: newCoin(129), 373 consumption: *newCoin(300), 374 }, 375 } { 376 suite.NextBlock(time.Unix(int64(i), 0)) 377 if i != 0 { 378 err := suite.vm.DailyAdvanceLinoStakeStats(suite.Ctx) 379 suite.Nil(err) 380 } 381 if tc.user1claim != nil { 382 suite.am.On("MoveFromPool", mock.Anything, linotypes.VoteFrictionPool, 383 linotypes.NewAccOrAddrFromAcc(user1), *tc.user1claim).Return(nil).Once() 384 err := suite.vm.ClaimInterest(suite.Ctx, user1) 385 suite.Nil(err) 386 } 387 if tc.user2claim != nil { 388 suite.am.On("MoveFromPool", mock.Anything, linotypes.VoteFrictionPool, 389 linotypes.NewAccOrAddrFromAcc(user2), *tc.user2claim).Return(nil).Once() 390 err := suite.vm.ClaimInterest(suite.Ctx, user2) 391 suite.Nil(err) 392 } 393 if tc.user1stakein != nil { 394 if tc.user1stakein.IsPositive() { 395 err := suite.vm.StakeIn(suite.Ctx, user1, *tc.user1stakein) 396 suite.Nil(err) 397 } else { 398 err := suite.vm.StakeOut(suite.Ctx, user1, tc.user1stakein.Neg()) 399 suite.Nil(err) 400 } 401 } 402 if tc.user2stakein != nil { 403 if tc.user2stakein.IsPositive() { 404 err := suite.vm.StakeIn(suite.Ctx, user2, *tc.user2stakein) 405 suite.Nil(err) 406 } else { 407 err := suite.vm.StakeOut(suite.Ctx, user2, tc.user2stakein.Neg()) 408 suite.Nil(err) 409 } 410 } 411 err := suite.vm.RecordFriction(suite.Ctx, tc.consumption) 412 suite.Nil(err) 413 } 414 suite.Golden() 415 } 416 417 func (suite *VoteManagerTestSuite) TestStakeOut() { 418 testCases := []struct { 419 testName string 420 username linotypes.AccountKey 421 amount linotypes.Coin 422 atWhen time.Time 423 expectErr sdk.Error 424 expectVoter *model.Voter 425 }{ 426 { 427 testName: "stake out from user without stake", 428 username: suite.userNotVoter, 429 amount: suite.minStakeInAmount, 430 atWhen: time.Unix(1, 0), 431 expectErr: types.ErrVoterNotFound(), 432 }, 433 { 434 testName: "stake out amount more than user has", 435 username: suite.user2, 436 amount: linotypes.NewCoinFromInt64(1000*linotypes.Decimals + 1), 437 atWhen: time.Unix(1, 0), 438 expectErr: types.ErrInsufficientStake(), 439 }, 440 { 441 testName: "stake out from user with stakes not enough due to fronzen", 442 username: suite.user3, 443 amount: suite.minStakeInAmount.Plus(linotypes.NewCoinFromInt64(1)), 444 atWhen: time.Unix(1, 0), 445 expectErr: types.ErrInsufficientStake(), 446 }, 447 { 448 testName: "stake out from user with sufficient stake", 449 username: suite.user1, 450 amount: suite.minStakeInAmount, 451 atWhen: time.Unix(1, 0), 452 expectErr: nil, 453 expectVoter: &model.Voter{ 454 Username: suite.user1, 455 LinoStake: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 456 Interest: linotypes.NewCoinFromInt64(888 * linotypes.Decimals), 457 Duty: types.DutyVoter, 458 FrozenAmount: linotypes.NewCoinFromInt64(0), 459 LastPowerChangeAt: 1, 460 }, 461 }, 462 } 463 464 for _, tc := range testCases { 465 suite.Run(tc.testName, func() { 466 suite.SetupTest() 467 suite.LoadState(false, "3voters") 468 suite.hooks.On("AfterSubtractingStake", 469 mock.Anything, mock.Anything).Return(nil).Maybe() 470 for i := int64(0); i <= tc.atWhen.Unix(); i++ { 471 suite.global.On("GetPastDay", mock.Anything, i).Return(i).Maybe() 472 } 473 suite.NextBlock(tc.atWhen) 474 475 if tc.expectErr == nil { 476 suite.am.On("MoveBetweenPools", mock.Anything, 477 linotypes.VoteStakeInPool, linotypes.VoteStakeReturnPool, 478 tc.amount).Return(nil).Once() 479 480 suite.am.On("AddFrozenMoney", mock.Anything, 481 tc.username, tc.amount, tc.atWhen.Unix(), 482 suite.returnIntervalSec, suite.returnTimes).Return(nil).Once() 483 suite.global.On( 484 "RegisterEventAtTime", mock.Anything, 485 tc.atWhen.Unix()+suite.returnIntervalSec, 486 accmn.ReturnCoinEvent{ 487 Username: tc.username, 488 Amount: tc.amount, 489 ReturnType: linotypes.VoteReturnCoin, 490 FromPool: linotypes.VoteStakeReturnPool, 491 At: tc.atWhen.Unix() + suite.returnIntervalSec, 492 }).Return(nil).Once() 493 } 494 err := suite.vm.StakeOut(suite.Ctx, tc.username, tc.amount) 495 suite.Equal(tc.expectErr, err, "%s", tc.testName) 496 if tc.expectVoter != nil { 497 voter, err := suite.vm.GetVoter(suite.Ctx, tc.username) 498 suite.Nil(err) 499 suite.Equal(tc.expectVoter, voter) 500 } 501 suite.am.AssertExpectations(suite.T()) 502 suite.global.AssertExpectations(suite.T()) 503 }) 504 } 505 } 506 507 type claim struct { 508 username linotypes.AccountKey 509 atWhen int64 510 expectErr sdk.Error 511 expectAmount *linotypes.Coin 512 expectVoter *model.Voter 513 } 514 515 func (suite *VoteManagerTestSuite) TestClaimInterest() { 516 testCases := []struct { 517 testName string 518 maxDay int64 519 claims []claim 520 }{ 521 { 522 testName: "voter not exists", 523 maxDay: 1, 524 claims: []claim{ 525 { 526 username: suite.userNotVoter, 527 atWhen: 1, 528 expectErr: types.ErrVoterNotFound(), 529 }, 530 }, 531 }, 532 { 533 testName: "claim interest for day0", 534 maxDay: 1, 535 claims: []claim{ 536 { 537 username: suite.user1, 538 atWhen: 1, 539 expectAmount: newCoin(888 * linotypes.Decimals), 540 expectVoter: &model.Voter{ 541 Username: suite.user1, 542 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 543 Interest: linotypes.NewCoinFromInt64(0), 544 Duty: types.DutyVoter, 545 FrozenAmount: linotypes.NewCoinFromInt64(0), 546 LastPowerChangeAt: 1, 547 }, 548 }, 549 { // claim again, no interest. 550 username: suite.user1, 551 atWhen: 1, 552 expectAmount: newCoin(0), 553 expectVoter: &model.Voter{ 554 Username: suite.user1, 555 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 556 Interest: linotypes.NewCoinFromInt64(0), 557 Duty: types.DutyVoter, 558 FrozenAmount: linotypes.NewCoinFromInt64(0), 559 LastPowerChangeAt: 1, 560 }, 561 }, 562 }, 563 }, 564 { 565 testName: "claim interest for all past days", 566 maxDay: 2, 567 claims: []claim{ 568 { 569 username: suite.user1, 570 atWhen: 2, 571 expectAmount: newCoin( 572 888*linotypes.Decimals + (999*linotypes.Decimals)/5*2), 573 expectVoter: &model.Voter{ 574 Username: suite.user1, 575 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 576 Interest: linotypes.NewCoinFromInt64(0), 577 Duty: types.DutyVoter, 578 FrozenAmount: linotypes.NewCoinFromInt64(0), 579 LastPowerChangeAt: 2, 580 }, 581 }, 582 }, 583 }, 584 { 585 testName: "claim interest from user with interest in voter struct", 586 maxDay: 2, 587 claims: []claim{ 588 { 589 username: suite.user2, 590 atWhen: 2, 591 expectAmount: newCoin((999*linotypes.Decimals)/5 + 100*linotypes.Decimals), 592 expectVoter: &model.Voter{ 593 Username: suite.user2, 594 LinoStake: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 595 Interest: linotypes.NewCoinFromInt64(0), 596 Duty: types.DutyVoter, 597 FrozenAmount: linotypes.NewCoinFromInt64(0), 598 LastPowerChangeAt: 2, 599 }, 600 }, 601 }, 602 }, 603 { 604 testName: "all claimed", 605 maxDay: 2, 606 claims: []claim{ 607 { 608 username: suite.user1, 609 atWhen: 2, 610 expectAmount: newCoin( 611 888*linotypes.Decimals + (999*linotypes.Decimals)/5*2), 612 expectVoter: &model.Voter{ 613 Username: suite.user1, 614 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 615 Interest: linotypes.NewCoinFromInt64(0), 616 Duty: types.DutyVoter, 617 FrozenAmount: linotypes.NewCoinFromInt64(0), 618 LastPowerChangeAt: 2, 619 }, 620 }, 621 { 622 username: suite.user2, 623 atWhen: 2, 624 expectAmount: newCoin((999*linotypes.Decimals)/5 + 100*linotypes.Decimals), 625 expectVoter: &model.Voter{ 626 Username: suite.user2, 627 LinoStake: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 628 Interest: linotypes.NewCoinFromInt64(0), 629 Duty: types.DutyVoter, 630 FrozenAmount: linotypes.NewCoinFromInt64(0), 631 LastPowerChangeAt: 2, 632 }, 633 }, 634 { 635 username: suite.user3, 636 atWhen: 2, 637 expectAmount: newCoin((999 * linotypes.Decimals) / 5 * 2), 638 expectVoter: &model.Voter{ 639 Username: suite.user3, 640 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 641 Interest: linotypes.NewCoinFromInt64(0), 642 Duty: types.DutyValidator, 643 FrozenAmount: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 644 LastPowerChangeAt: 2, 645 }, 646 }, 647 }, 648 }, 649 } 650 651 for _, tc := range testCases { 652 suite.Run(tc.testName, func() { 653 suite.SetupTest() 654 suite.LoadState(false, "3voters") 655 for i := int64(0); i <= tc.maxDay; i++ { 656 suite.global.On("GetPastDay", mock.Anything, i).Return(i).Maybe() 657 } 658 for _, claim := range tc.claims { 659 suite.NextBlock(time.Unix(claim.atWhen, 0)) 660 if claim.expectErr == nil { 661 suite.am.On("MoveFromPool", mock.Anything, linotypes.VoteFrictionPool, 662 linotypes.NewAccOrAddrFromAcc(claim.username), 663 *claim.expectAmount).Return(nil).Once() 664 } 665 err := suite.vm.ClaimInterest(suite.Ctx, claim.username) 666 suite.Equal(claim.expectErr, err) 667 if claim.expectVoter != nil { 668 voter, err := suite.vm.GetVoter(suite.Ctx, claim.username) 669 suite.Nil(err) 670 suite.Equal(claim.expectVoter, voter) 671 } 672 } 673 suite.am.AssertExpectations(suite.T()) 674 suite.global.AssertExpectations(suite.T()) 675 suite.Golden() //ensures that stake-stats are all correct. 676 }) 677 } 678 } 679 680 func (suite *VoteManagerTestSuite) TestAssignDuty() { 681 testCases := []struct { 682 testName string 683 username linotypes.AccountKey 684 duty types.VoterDuty 685 frozenAmount linotypes.Coin 686 expectErr sdk.Error 687 expectVoter *model.Voter 688 }{ 689 { 690 testName: "assign duty to user without stake", 691 username: suite.userNotVoter, 692 duty: types.DutyValidator, 693 frozenAmount: linotypes.NewCoinFromInt64(1), 694 expectErr: types.ErrVoterNotFound(), 695 }, 696 { 697 testName: "assign duty to user with other duty", 698 username: suite.user3, 699 duty: types.DutyValidator, 700 frozenAmount: linotypes.NewCoinFromInt64(1), 701 expectErr: types.ErrNotAVoterOrHasDuty(), 702 }, 703 { 704 testName: "negative frozen amount", 705 username: suite.user1, 706 duty: types.DutyValidator, 707 frozenAmount: linotypes.NewCoinFromInt64(-1), 708 expectErr: types.ErrNegativeFrozenAmount(), 709 }, 710 { 711 testName: "frozen money larger than stake", 712 username: suite.user1, 713 duty: types.DutyValidator, 714 frozenAmount: *newCoin(2000*linotypes.Decimals + 1), 715 expectErr: types.ErrInsufficientStake(), 716 }, 717 { 718 testName: "assign duty successfully", 719 username: suite.user1, 720 duty: types.DutyValidator, 721 frozenAmount: *newCoin(1000 * linotypes.Decimals), 722 expectErr: nil, 723 expectVoter: &model.Voter{ 724 Username: suite.user1, 725 LinoStake: *newCoin(2000 * linotypes.Decimals), 726 Interest: linotypes.NewCoinFromInt64(0), 727 Duty: types.DutyValidator, 728 FrozenAmount: *newCoin(1000 * linotypes.Decimals), 729 }, 730 }, 731 } 732 733 for _, tc := range testCases { 734 suite.Run(tc.testName, func() { 735 suite.SetupTest() 736 suite.LoadState(false, "3voters") 737 err := suite.vm.AssignDuty(suite.Ctx, tc.username, tc.duty, tc.frozenAmount) 738 suite.Equal(tc.expectErr, err, "%s", tc.testName) 739 if tc.expectVoter != nil { 740 voter, _ := suite.vm.GetVoter(suite.Ctx, tc.username) 741 suite.Equal(tc.expectVoter, voter, "%s", tc.testName) 742 } 743 suite.Golden() 744 }) 745 } 746 } 747 748 func (suite *VoteManagerTestSuite) TestUnassignDuty() { 749 testCases := []struct { 750 testName string 751 username linotypes.AccountKey 752 expectErr sdk.Error 753 expectVoter *model.Voter 754 }{ 755 { 756 testName: "unassign duty from user without stake", 757 username: suite.userNotVoter, 758 expectErr: types.ErrVoterNotFound(), 759 }, 760 { 761 testName: "unassign duty from user doesnt have duty", 762 username: suite.user2, 763 expectErr: types.ErrNoDuty(), 764 }, 765 { 766 testName: "unassign duty from user has pending duty", 767 username: suite.userPendingDuty, 768 expectErr: types.ErrNoDuty(), 769 }, 770 { 771 testName: "unassign duty from user who has validator duty", 772 username: suite.user3, 773 expectVoter: &model.Voter{ 774 Username: suite.user3, 775 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 776 Interest: linotypes.NewCoinFromInt64(0), 777 Duty: types.DutyPending, 778 FrozenAmount: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 779 LastPowerChangeAt: 1, 780 }, 781 }, 782 } 783 784 waitingPeriodSec := int64(100) 785 for _, tc := range testCases { 786 suite.Run(tc.testName, func() { 787 suite.LoadState(false, "3voters") 788 suite.NextBlock(time.Unix(1, 0)) 789 if tc.expectErr == nil { 790 suite.global.On("RegisterEventAtTime", mock.Anything, 791 1+waitingPeriodSec, 792 types.UnassignDutyEvent{Username: tc.username}).Return(nil).Once() 793 } 794 err := suite.vm.UnassignDuty(suite.Ctx, tc.username, waitingPeriodSec) 795 suite.Equal(tc.expectErr, err) 796 if tc.expectVoter != nil { 797 voter, err := suite.vm.GetVoter(suite.Ctx, tc.username) 798 suite.Nil(err) 799 suite.Equal(tc.expectVoter, voter) 800 } 801 suite.global.AssertExpectations(suite.T()) 802 suite.Golden() 803 }) 804 } 805 } 806 807 func (suite *VoteManagerTestSuite) TestSlashStake() { 808 testCases := []struct { 809 testName string 810 username linotypes.AccountKey 811 amount linotypes.Coin 812 expectErr sdk.Error 813 expectSlashedAmount linotypes.Coin 814 expectVoter *model.Voter 815 }{ 816 { 817 testName: "slash stake from user without stake", 818 username: suite.userNotVoter, 819 amount: linotypes.NewCoinFromInt64(1), 820 expectErr: types.ErrVoterNotFound(), 821 expectSlashedAmount: linotypes.NewCoinFromInt64(0), 822 }, 823 { 824 testName: "slash more than user stake", 825 username: suite.user2, 826 amount: *newCoin(1000*linotypes.Decimals + 1), 827 expectSlashedAmount: *newCoin(1000 * linotypes.Decimals), 828 expectVoter: &model.Voter{ 829 Username: suite.user2, 830 LinoStake: linotypes.NewCoinFromInt64(0), 831 Interest: linotypes.NewCoinFromInt64( 832 (999*linotypes.Decimals)/5 + 100*linotypes.Decimals), 833 Duty: types.DutyVoter, 834 FrozenAmount: linotypes.NewCoinFromInt64(0), 835 LastPowerChangeAt: 2, 836 }, 837 }, 838 { 839 testName: "slash users stake with frozen", 840 username: suite.user3, 841 amount: *newCoin(1500 * linotypes.Decimals), 842 expectSlashedAmount: *newCoin(1500 * linotypes.Decimals), 843 expectVoter: &model.Voter{ 844 Username: suite.user3, 845 LinoStake: linotypes.NewCoinFromInt64(500 * linotypes.Decimals), 846 Interest: linotypes.NewCoinFromInt64(39960000), 847 Duty: types.DutyValidator, 848 FrozenAmount: suite.minStakeInAmount, 849 LastPowerChangeAt: 2, 850 }, 851 }, 852 } 853 854 // all cases are assumed to happen at day2 to test poping interests upon slash. 855 var destPool linotypes.PoolName = "dest" 856 for _, tc := range testCases { 857 suite.Run(tc.testName, func() { 858 suite.SetupTest() 859 suite.LoadState(false, "3voters") 860 for i := int64(0); i <= 2; i++ { 861 suite.global.On("GetPastDay", mock.Anything, i).Return(i).Maybe() 862 } 863 suite.NextBlock(time.Unix(2, 0)) 864 err := suite.vm.DailyAdvanceLinoStakeStats(suite.Ctx) 865 suite.Nil(err) 866 if tc.expectErr == nil { 867 suite.hooks.On("AfterSlashing", mock.Anything, tc.username).Return(nil).Once() 868 suite.am.On("MoveBetweenPools", mock.Anything, 869 linotypes.VoteStakeInPool, destPool, tc.expectSlashedAmount).Return(nil).Once() 870 } 871 872 amount, err := suite.vm.SlashStake(suite.Ctx, tc.username, tc.amount, destPool) 873 suite.Equal(tc.expectErr, err) 874 suite.Equal(tc.expectSlashedAmount, amount, "%s vs %s", tc.expectSlashedAmount, amount) 875 if tc.expectVoter != nil { 876 voter, _ := suite.vm.GetVoter(suite.Ctx, tc.username) 877 suite.Equal(tc.expectVoter, voter) 878 } 879 880 suite.am.AssertExpectations(suite.T()) 881 suite.hooks.AssertExpectations(suite.T()) 882 suite.Golden() // ensure stake-stats are correct. 883 }) 884 } 885 } 886 887 func (suite *VoteManagerTestSuite) TestExecUnassignDutyEvent() { 888 testCases := []struct { 889 testName string 890 event types.UnassignDutyEvent 891 expectErr sdk.Error 892 expectVoter *model.Voter 893 }{ 894 { 895 testName: "execute event on non exist voter", 896 event: types.UnassignDutyEvent{Username: suite.userNotVoter}, 897 expectErr: types.ErrVoterNotFound(), 898 expectVoter: nil, 899 }, 900 { 901 testName: "execute event on voter with validator duty", 902 event: types.UnassignDutyEvent{Username: suite.user3}, 903 expectErr: nil, 904 expectVoter: &model.Voter{ 905 Username: suite.user3, 906 LinoStake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 907 Interest: linotypes.NewCoinFromInt64(0), 908 Duty: types.DutyVoter, 909 FrozenAmount: linotypes.NewCoinFromInt64(0), 910 LastPowerChangeAt: 1, 911 }, 912 }, 913 } 914 915 for _, tc := range testCases { 916 suite.Run(tc.testName, func() { 917 suite.LoadState(false, "3voters") 918 err := suite.vm.ExecUnassignDutyEvent(suite.Ctx, tc.event) 919 suite.Equal(tc.expectErr, err, "%s", tc.testName) 920 voter, _ := suite.vm.GetVoter(suite.Ctx, tc.event.Username) 921 suite.Equal(tc.expectVoter, voter, "%s", tc.testName) 922 }) 923 } 924 } 925 926 func (suite *VoteManagerTestSuite) TestGetLinoStakeAndDuty() { 927 testCases := []struct { 928 username linotypes.AccountKey 929 stake linotypes.Coin 930 duty types.VoterDuty 931 }{ 932 { 933 username: suite.user2, 934 stake: linotypes.NewCoinFromInt64(1000 * linotypes.Decimals), 935 duty: types.DutyVoter, 936 }, 937 { 938 username: suite.user3, 939 stake: linotypes.NewCoinFromInt64(2000 * linotypes.Decimals), 940 duty: types.DutyValidator, 941 }, 942 } 943 944 suite.LoadState(false, "3voters") 945 for _, tc := range testCases { 946 stake, err := suite.vm.GetLinoStake(suite.Ctx, tc.username) 947 suite.Nil(err) 948 suite.Equal(stake, tc.stake) 949 duty, err := suite.vm.GetVoterDuty(suite.Ctx, tc.username) 950 suite.Nil(err) 951 suite.Equal(duty, tc.duty) 952 } 953 } 954 955 func (suite *VoteManagerTestSuite) TestDailyAdvanceLinoStakeStats() { 956 // the skipped day case 957 suite.global.On("GetPastDay", mock.Anything, int64(0)).Return(int64(0)).Once() 958 err := suite.vm.RecordFriction(suite.Ctx, linotypes.NewCoinFromInt64(100)) 959 suite.Nil(err) 960 t := time.Unix(3600*8, 0) 961 suite.NextBlock(t) 962 suite.global.On("GetPastDay", mock.Anything, t.Unix()).Return(int64(8)).Once() 963 err = suite.vm.DailyAdvanceLinoStakeStats(suite.Ctx) 964 suite.Nil(err) 965 // should see 8 days of consumption 100. 966 suite.Golden() 967 } 968 969 func (suite *VoteManagerTestSuite) TestImportExport() { 970 // background data 971 suite.vm.storage.SetVoter(suite.Ctx, &model.Voter{ 972 Username: "voter1", 973 LinoStake: *newCoin(1234), 974 LastPowerChangeAt: 123, 975 Interest: *newCoin(2345), 976 Duty: types.DutyValidator, 977 FrozenAmount: *newCoin(999), 978 }) 979 suite.vm.storage.SetVoter(suite.Ctx, &model.Voter{ 980 Username: "voter2", 981 LinoStake: *newCoin(567), 982 LastPowerChangeAt: 3, 983 Interest: *newCoin(0), 984 Duty: types.DutyVoter, 985 FrozenAmount: *newCoin(0), 986 }) 987 suite.vm.storage.SetLinoStakeStat(suite.Ctx, 0, &model.LinoStakeStat{ 988 TotalConsumptionFriction: *newCoin(123), 989 UnclaimedFriction: *newCoin(456), 990 TotalLinoStake: *newCoin(789), 991 UnclaimedLinoStake: *newCoin(1230), 992 }) 993 suite.vm.storage.SetLinoStakeStat(suite.Ctx, 1, &model.LinoStakeStat{ 994 TotalConsumptionFriction: *newCoin(1123), 995 UnclaimedFriction: *newCoin(1456), 996 TotalLinoStake: *newCoin(1789), 997 UnclaimedLinoStake: *newCoin(11230), 998 }) 999 1000 cdc := codec.New() 1001 dir, err2 := ioutil.TempDir("", "test") 1002 suite.Require().Nil(err2) 1003 defer os.RemoveAll(dir) // clean up 1004 1005 tmpfn := filepath.Join(dir, "tmpfile") 1006 err2 = suite.vm.ExportToFile(suite.Ctx, cdc, tmpfn) 1007 suite.Nil(err2) 1008 1009 // reset all state. 1010 suite.SetupTest() 1011 err2 = suite.vm.ImportFromFile(suite.Ctx, cdc, tmpfn) 1012 suite.Nil(err2) 1013 1014 suite.Golden() 1015 } 1016 1017 func newCoin(n int64) *linotypes.Coin { 1018 coin := linotypes.NewCoinFromInt64(n) 1019 return &coin 1020 }