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 = &param.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(&parammodel.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 = &param.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(&parammodel.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  }