diff --git a/tests/distribution_test.go b/tests/distribution_test.go new file mode 100644 index 0000000000..90d4bc4cc9 --- /dev/null +++ b/tests/distribution_test.go @@ -0,0 +1,34 @@ +package tests + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/msgs" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" + "github.com/stretchr/testify/require" +) + +func TestDistribution(t *testing.T) { + app := processblock.NewTestApp() + processblock.CommonPreset(app) + signer1 := app.NewSignableAccount("signer1") + app.FundAccount(signer1, 100000000) + alice := app.NewAccount() + + sendAliceMsg := msgs.Send(signer1, alice, 1000) + tx1 := app.Sign(signer1, []sdk.Msg{sendAliceMsg}, 20000) + + // block T (no distribution yet since this is the first block) + block := []signing.Tx{tx1} + blockRunner := func() []uint32 { return app.RunBlock(block) } + require.Equal(t, []uint32{0}, blockRunner()) + + // block T+1 (distribution of fees from T) + blockRunner = func() []uint32 { return app.RunBlock([]signing.Tx{}) } + blockRunner = verify.Allocation(t, app, blockRunner) + + require.Equal(t, []uint32{}, blockRunner()) +} diff --git a/tests/epoch_test.go b/tests/epoch_test.go new file mode 100644 index 0000000000..3e0fb7fce7 --- /dev/null +++ b/tests/epoch_test.go @@ -0,0 +1,29 @@ +package tests + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" + "github.com/stretchr/testify/require" +) + +func TestEpoch(t *testing.T) { + app := processblock.NewTestApp() + processblock.CommonPreset(app) + app.FastEpoch() + + blockRunner := func() []uint32 { return app.RunBlock([]signing.Tx{}) } + blockRunner = verify.Epoch(t, app, blockRunner) + + require.Equal(t, []uint32{}, blockRunner()) + + time.Sleep(6 * time.Second) + + blockRunner = func() []uint32 { return app.RunBlock([]signing.Tx{}) } + blockRunner = verify.Epoch(t, app, blockRunner) + + require.Equal(t, []uint32{}, blockRunner()) +} diff --git a/tests/mint_test.go b/tests/mint_test.go new file mode 100644 index 0000000000..6a4caa6a0d --- /dev/null +++ b/tests/mint_test.go @@ -0,0 +1,30 @@ +package tests + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" + "github.com/stretchr/testify/require" +) + +func TestMint(t *testing.T) { + app := processblock.NewTestApp() + processblock.CommonPreset(app) + app.NewMinter(1000000) + app.FastEpoch() + + blockRunner := func() []uint32 { return app.RunBlock([]signing.Tx{}) } + blockRunner = verify.MintRelease(t, app, blockRunner) + + require.Equal(t, []uint32{}, blockRunner()) + + time.Sleep(6 * time.Second) + + blockRunner = func() []uint32 { return app.RunBlock([]signing.Tx{}) } + blockRunner = verify.MintRelease(t, app, blockRunner) + + require.Equal(t, []uint32{}, blockRunner()) +} diff --git a/testutil/processblock/common.go b/testutil/processblock/common.go index fc915a63ea..a71c4998cd 100644 --- a/testutil/processblock/common.go +++ b/testutil/processblock/common.go @@ -90,15 +90,7 @@ func (a *App) RunBlock(txs []signing.Tx) (resultCodes []uint32) { }), DecidedLastCommit: types.CommitInfo{ Round: 0, - Votes: utils.Map(a.GetAllValidators(), func(v stakingtypes.Validator) types.VoteInfo { - return types.VoteInfo{ - Validator: types.Validator{ - Address: getValAddress(v), - Power: 1, - }, - SignedLastBlock: true, - } - }), + Votes: a.GetVotes(), }, ByzantineValidators: []types.Misbehavior{}, Hash: []byte("abc"), // no needed for application logic @@ -112,6 +104,18 @@ func (a *App) RunBlock(txs []signing.Tx) (resultCodes []uint32) { return utils.Map(res.TxResults, func(r *types.ExecTxResult) uint32 { return r.Code }) } +func (a *App) GetVotes() []types.VoteInfo { + return utils.Map(a.GetAllValidators(), func(v stakingtypes.Validator) types.VoteInfo { + return types.VoteInfo{ + Validator: types.Validator{ + Address: getValAddress(v), + Power: 1, + }, + SignedLastBlock: true, + } + }) +} + func (a *App) GetAllValidators() []stakingtypes.Validator { return a.StakingKeeper.GetAllValidators(a.Ctx()) } diff --git a/testutil/processblock/genesisepoch.go b/testutil/processblock/genesisepoch.go new file mode 100644 index 0000000000..e67b18422b --- /dev/null +++ b/testutil/processblock/genesisepoch.go @@ -0,0 +1,9 @@ +package processblock + +import "time" + +func (a *App) FastEpoch() { + epoch := a.EpochKeeper.GetEpoch(a.Ctx()) + epoch.EpochDuration = 5 * time.Second + a.EpochKeeper.SetEpoch(a.Ctx(), epoch) +} diff --git a/testutil/processblock/genesismint.go b/testutil/processblock/genesismint.go new file mode 100644 index 0000000000..e4beda3827 --- /dev/null +++ b/testutil/processblock/genesismint.go @@ -0,0 +1,19 @@ +package processblock + +import ( + "time" + + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" +) + +func (a *App) NewMinter(amount uint64) { + today := time.Now() + dayAfterTomorrow := today.Add(48 * time.Hour) + a.MintKeeper.SetMinter(a.Ctx(), minttypes.Minter{ + StartDate: today.Format(minttypes.TokenReleaseDateFormat), + EndDate: dayAfterTomorrow.Format(minttypes.TokenReleaseDateFormat), + Denom: "usei", + TotalMintAmount: amount, + RemainingMintAmount: amount, + }) +} diff --git a/testutil/processblock/verify/distribution.go b/testutil/processblock/verify/distribution.go new file mode 100644 index 0000000000..8439291fd4 --- /dev/null +++ b/testutil/processblock/verify/distribution.go @@ -0,0 +1,88 @@ +package verify + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/utils" + "github.com/stretchr/testify/require" +) + +// assuming only `usei` will get distributed +func Allocation(t *testing.T, app *processblock.App, f BlockRunnable) BlockRunnable { + return func() []uint32 { + // fees collected in T-1 are allocated in T's BeginBlock, so we can simply + // query fee collector's balance since this function is called between T-1 + // and T. + feeCollector := app.AccountKeeper.GetModuleAccount(app.Ctx(), authtypes.FeeCollectorName) + feesCollectedInt := app.BankKeeper.GetBalance(app.Ctx(), feeCollector.GetAddress(), "usei") + feesCollected := sdk.NewDecCoinFromCoin(feesCollectedInt) + + prevProposer := sdk.ValAddress(app.DistrKeeper.GetPreviousProposerConsAddr(app.Ctx())).String() + votedValidators := utils.Map(app.GetAllValidators(), func(v stakingtypes.Validator) string { + return v.GetOperator().String() + }) + expectedOutstandingRewards := getOutstandingRewards(app) + + baseProposerReward := app.DistrKeeper.GetBaseProposerReward(app.Ctx()) + bonusProposerReward := app.DistrKeeper.GetBonusProposerReward(app.Ctx()) + proposerMultiplier := baseProposerReward.Add(bonusProposerReward.MulTruncate(sdk.OneDec())) // in test, every val always signs + proposerReward := sdk.DecCoin{ + Denom: "usei", + Amount: feesCollected.Amount.MulTruncate(proposerMultiplier), + } + expectedOutstandingRewards[prevProposer] = expectedOutstandingRewards[prevProposer].Add(proposerReward) + + communityTax := app.DistrKeeper.GetCommunityTax(app.Ctx()) + voteMultiplier := sdk.OneDec().Sub(proposerMultiplier).Sub(communityTax).QuoInt(sdk.NewInt(int64(len(votedValidators)))) + voterReward := sdk.DecCoin{ + Denom: "usei", + Amount: feesCollected.Amount.MulTruncate(voteMultiplier), + } + + for _, validator := range votedValidators { + expectedOutstandingRewards[validator] = expectedOutstandingRewards[validator].Add(voterReward) + } + + res := f() + + actualOutstandingRewards := getOutstandingRewards(app) + + require.Equal(t, len(expectedOutstandingRewards), len(actualOutstandingRewards)) + + for val, reward := range expectedOutstandingRewards { + require.True(t, reward.Equal(actualOutstandingRewards[val])) + } + + return res + } +} + +func getOutstandingRewards(app *processblock.App) map[string]sdk.DecCoin { + outstandingRewards := map[string]sdk.DecCoin{} + for _, val := range app.GetAllValidators() { + outstandingRewards[val.GetOperator().String()] = sdk.NewDecCoin("usei", sdk.NewInt(0)) + } + app.DistrKeeper.IterateValidatorOutstandingRewards( + app.Ctx(), + func(val sdk.ValAddress, rewards types.ValidatorOutstandingRewards) (stop bool) { + if len(rewards.Rewards) == 0 { + return false + } + if len(rewards.Rewards) > 1 { + panic("expecting only usei as validator reward denom but found multiple") + } + if rewards.Rewards[0].Denom != "usei" { + panic(fmt.Sprintf("expecting only usei as validator reward denom but found %s", rewards.Rewards[0].Denom)) + } + outstandingRewards[val.String()] = rewards.Rewards[0] + return false + }, + ) + return outstandingRewards +} diff --git a/testutil/processblock/verify/epoch.go b/testutil/processblock/verify/epoch.go new file mode 100644 index 0000000000..c27f9669c3 --- /dev/null +++ b/testutil/processblock/verify/epoch.go @@ -0,0 +1,20 @@ +package verify + +import ( + "testing" + + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/stretchr/testify/require" +) + +func Epoch(t *testing.T, app *processblock.App, f BlockRunnable) BlockRunnable { + return func() []uint32 { + oldEpoch := app.EpochKeeper.GetEpoch(app.Ctx()) + res := f() + if app.Ctx().BlockTime().Sub(oldEpoch.CurrentEpochStartTime) > oldEpoch.EpochDuration { + newPoch := app.EpochKeeper.GetEpoch(app.Ctx()) + require.Equal(t, oldEpoch.CurrentEpoch+1, newPoch.CurrentEpoch) + } + return res + } +} diff --git a/testutil/processblock/verify/mint.go b/testutil/processblock/verify/mint.go new file mode 100644 index 0000000000..0c9e552b2b --- /dev/null +++ b/testutil/processblock/verify/mint.go @@ -0,0 +1,39 @@ +package verify + +import ( + "testing" + "time" + + "github.com/sei-protocol/sei-chain/testutil/processblock" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" + "github.com/stretchr/testify/require" +) + +func MintRelease(t *testing.T, app *processblock.App, f BlockRunnable) BlockRunnable { + return func() []uint32 { + oldMinter := app.MintKeeper.GetMinter(app.Ctx()) + oldEpoch := app.EpochKeeper.GetEpoch(app.Ctx()) + oldSupply := app.BankKeeper.GetSupply(app.Ctx(), "usei") + res := f() + // if minter minted, it must be a new epoch, but not the other way around + newMinter := app.MintKeeper.GetMinter(app.Ctx()) + if newMinter.RemainingMintAmount == oldMinter.RemainingMintAmount { + return res + } + newPoch := app.EpochKeeper.GetEpoch(app.Ctx()) + require.Equal(t, oldEpoch.CurrentEpoch+1, newPoch.CurrentEpoch) + startDate, err := time.Parse(minttypes.TokenReleaseDateFormat, oldMinter.StartDate) + if err != nil { + panic(err) + } + endDate, err := time.Parse(minttypes.TokenReleaseDateFormat, oldMinter.EndDate) + if err != nil { + panic(err) + } + expectedMintedAmount := oldMinter.TotalMintAmount / uint64(endDate.Sub(startDate)/(24*time.Hour)) + require.Equal(t, expectedMintedAmount, oldMinter.RemainingMintAmount-newMinter.RemainingMintAmount) + newSupply := app.BankKeeper.GetSupply(app.Ctx(), "usei") + require.Equal(t, expectedMintedAmount, uint64(newSupply.Amount.Int64()-oldSupply.Amount.Int64())) + return res + } +}