diff --git a/x/farming/keeper/genesis_test.go b/x/farming/keeper/genesis_test.go index 951fda1a..11c0e70c 100644 --- a/x/farming/keeper/genesis_test.go +++ b/x/farming/keeper/genesis_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "fmt" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -287,12 +289,18 @@ func (suite *KeeperTestSuite) TestExportGenesis() { { "HistoricalRewards", func() { - suite.Require().Len(genState.HistoricalRewardsRecords, 2) + suite.Require().Len(genState.HistoricalRewardsRecords, 4) for _, record := range genState.HistoricalRewardsRecords { - suite.Require().Equal(uint64(0), record.Epoch) suite.Require().Contains([]string{denom1, denom2}, record.StakingCoinDenom) - suite.Require().False(record.HistoricalRewards.CumulativeUnitRewards.IsZero()) - // TODO: need to check actual value? + switch record.Epoch { + case 0: + suite.Require().True(record.HistoricalRewards.CumulativeUnitRewards.IsZero()) + case 1: + // TODO: need to check actual value? + suite.Require().False(record.HistoricalRewards.CumulativeUnitRewards.IsZero()) + default: + panic(fmt.Sprintf("unexpected epoch %d", record.Epoch)) + } } }, }, @@ -319,7 +327,7 @@ func (suite *KeeperTestSuite) TestExportGenesis() { func() { suite.Require().Len(genState.CurrentEpochRecords, 2) for _, record := range genState.CurrentEpochRecords { - suite.Require().Equal(uint64(1), record.CurrentEpoch) + suite.Require().Equal(uint64(2), record.CurrentEpoch) } }, }, diff --git a/x/farming/keeper/grpc_query_test.go b/x/farming/keeper/grpc_query_test.go index f1e2ea1a..4fb88e04 100644 --- a/x/farming/keeper/grpc_query_test.go +++ b/x/farming/keeper/grpc_query_test.go @@ -421,8 +421,7 @@ func (suite *KeeperTestSuite) TestGRPCRewards() { nil, }, } { - cacheCtx, _ := suite.ctx.CacheContext() // TODO: can we omit the 'cached' context? - resp, err := suite.querier.Rewards(sdk.WrapSDKContext(cacheCtx), tc.req) + resp, err := suite.querier.Rewards(sdk.WrapSDKContext(suite.ctx), tc.req) if tc.expectErr { suite.Require().Error(err) } else { diff --git a/x/farming/keeper/invariants_test.go b/x/farming/keeper/invariants_test.go index 9821e0e4..857fb064 100644 --- a/x/farming/keeper/invariants_test.go +++ b/x/farming/keeper/invariants_test.go @@ -133,7 +133,7 @@ func (suite *KeeperTestSuite) TestRemainingRewardsAmountInvariant() { // Withdrawable rewards amount in the store > balance of rewards reserve acc. // Should not be OK. - k.SetHistoricalRewards(ctx, denom1, 1, types.HistoricalRewards{ + k.SetHistoricalRewards(ctx, denom1, 2, types.HistoricalRewards{ CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 3)), }) _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) @@ -141,14 +141,14 @@ func (suite *KeeperTestSuite) TestRemainingRewardsAmountInvariant() { // Withdrawable rewards amount in the store <= balance of rewards reserve acc. // Should be OK. - k.SetHistoricalRewards(ctx, denom1, 1, types.HistoricalRewards{ + k.SetHistoricalRewards(ctx, denom1, 2, types.HistoricalRewards{ CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1)), }) _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) suite.Require().False(broken) // Reset. - k.SetHistoricalRewards(ctx, denom1, 1, types.HistoricalRewards{ + k.SetHistoricalRewards(ctx, denom1, 2, types.HistoricalRewards{ CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 2)), }) _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) diff --git a/x/farming/keeper/reward.go b/x/farming/keeper/reward.go index a6467cdd..b2cf0576 100644 --- a/x/farming/keeper/reward.go +++ b/x/farming/keeper/reward.go @@ -38,6 +38,17 @@ func (k Keeper) DeleteHistoricalRewards(ctx sdk.Context, stakingCoinDenom string store.Delete(types.GetHistoricalRewardsKey(stakingCoinDenom, epoch)) } +// DeleteAllHistoricalRewards deletes all historical rewards for a +// staking coin denom. +func (k Keeper) DeleteAllHistoricalRewards(ctx sdk.Context, stakingCoinDenom string) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.GetHistoricalRewardsPrefix(stakingCoinDenom)) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} + // IterateHistoricalRewards iterates through all historical rewards // stored in the store and invokes callback function for each item. // Stops the iteration when the callback function returns true. @@ -74,6 +85,13 @@ func (k Keeper) SetCurrentEpoch(ctx sdk.Context, stakingCoinDenom string, curren store.Set(types.GetCurrentEpochKey(stakingCoinDenom), bz) } +// DeleteCurrentEpoch deletes current epoch info for a given +// staking coin denom. +func (k Keeper) DeleteCurrentEpoch(ctx sdk.Context, stakingCoinDenom string) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.GetCurrentEpochKey(stakingCoinDenom)) +} + // IterateCurrentEpochs iterates through all current epoch infos // stored in the store and invokes callback function for each item. // Stops the iteration when the callback function returns true. @@ -139,7 +157,10 @@ func (k Keeper) IterateOutstandingRewards(ctx sdk.Context, cb func(stakingCoinDe // IncreaseOutstandingRewards increases outstanding rewards for a given // staking coin denom by given amount. func (k Keeper) IncreaseOutstandingRewards(ctx sdk.Context, stakingCoinDenom string, amount sdk.DecCoins) { - outstanding, _ := k.GetOutstandingRewards(ctx, stakingCoinDenom) + outstanding, found := k.GetOutstandingRewards(ctx, stakingCoinDenom) + if !found { + panic("outstanding rewards not found") + } outstanding.Rewards = outstanding.Rewards.Add(amount...) k.SetOutstandingRewards(ctx, stakingCoinDenom, outstanding) } @@ -153,12 +174,8 @@ func (k Keeper) DecreaseOutstandingRewards(ctx sdk.Context, stakingCoinDenom str if !found { panic("outstanding rewards not found") } - if outstanding.Rewards.IsEqual(amount) { - k.DeleteOutstandingRewards(ctx, stakingCoinDenom) - } else { - outstanding.Rewards = outstanding.Rewards.Sub(amount) - k.SetOutstandingRewards(ctx, stakingCoinDenom, outstanding) - } + outstanding.Rewards = outstanding.Rewards.Sub(amount) + k.SetOutstandingRewards(ctx, stakingCoinDenom, outstanding) } // CalculateRewards returns rewards accumulated until endingEpoch @@ -209,7 +226,6 @@ func (k Keeper) WithdrawRewards(ctx sdk.Context, farmerAcc sdk.AccAddress, staki } currentEpoch := k.GetCurrentEpoch(ctx, stakingCoinDenom) - // TODO: handle if currentEpoch is 0 rewards := k.CalculateRewards(ctx, farmerAcc, stakingCoinDenom, currentEpoch-1) truncatedRewards, _ := rewards.TruncateDecimal() diff --git a/x/farming/keeper/reward_test.go b/x/farming/keeper/reward_test.go index e54cadc8..b51310d5 100644 --- a/x/farming/keeper/reward_test.go +++ b/x/farming/keeper/reward_test.go @@ -347,12 +347,69 @@ func (suite *KeeperTestSuite) TestHistoricalRewards() { count++ return false }) - suite.Require().Equal(count, 3) + suite.Require().Equal(4, count) // Next, check if cumulative unit rewards is correct. - for i := uint64(0); i < 3; i++ { + for i := uint64(1); i <= 3; i++ { historical, found := suite.keeper.GetHistoricalRewards(suite.ctx, denom1, i) suite.Require().True(found) - suite.Require().True(decCoinsEq(sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, int64((i+1)*2))), historical.CumulativeUnitRewards)) + suite.Require().True(decCoinsEq(sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, int64(i*2))), historical.CumulativeUnitRewards)) } } + +// Test if initialization and pruning of staking coin info work properly. +func (suite *KeeperTestSuite) TestInitializeAndPruneStakingCoinInfo() { + suite.SetFixedAmountPlan(1, suite.addrs[4], map[string]string{denom1: "1"}, map[string]int64{denom3: 1000000}) + + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000000))) + + suite.Require().Equal(uint64(0), suite.keeper.GetCurrentEpoch(suite.ctx, denom1)) + _, found := suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 0) + suite.Require().False(found) + _, found = suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 1) + suite.Require().False(found) + _, found = suite.keeper.GetOutstandingRewards(suite.ctx, denom1) + suite.Require().False(found) + + suite.AdvanceEpoch() + + suite.Require().Equal(uint64(1), suite.keeper.GetCurrentEpoch(suite.ctx, denom1)) + historical, found := suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 0) + suite.Require().True(found) + suite.Require().True(decCoinsEq(sdk.DecCoins{}, historical.CumulativeUnitRewards)) + outstanding, found := suite.keeper.GetOutstandingRewards(suite.ctx, denom1) + suite.Require().True(found) + suite.Require().True(decCoinsEq(sdk.DecCoins{}, outstanding.Rewards)) + + suite.AdvanceEpoch() + + suite.Require().Equal(uint64(2), suite.keeper.GetCurrentEpoch(suite.ctx, denom1)) + historical, found = suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 1) + suite.Require().True(found) + suite.Require().True(decCoinsEq(sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1)), historical.CumulativeUnitRewards)) + outstanding, found = suite.keeper.GetOutstandingRewards(suite.ctx, denom1) + suite.Require().True(found) + suite.Require().True(decCoinsEq(sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1000000)), outstanding.Rewards)) + // Historical rewards for epoch 2 must not be present at this point, + // since current epoch is 2, and it has not ended yet. + _, found = suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 2) + suite.Require().False(found) + + // Unstake most of the coins. This should not delete any info + // about the staking coin yet. + suite.Unstake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 999999))) + suite.Require().Equal(uint64(2), suite.keeper.GetCurrentEpoch(suite.ctx, denom1)) + _, found = suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 1) + suite.Require().True(found) + _, found = suite.keeper.GetOutstandingRewards(suite.ctx, denom1) + suite.Require().True(found) + + // Now unstake the rest of the coins. This should delete info + // about the staking coin. + suite.Unstake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))) + suite.Require().Equal(uint64(0), suite.keeper.GetCurrentEpoch(suite.ctx, denom1)) + _, found = suite.keeper.GetHistoricalRewards(suite.ctx, denom1, 1) + suite.Require().False(found) + _, found = suite.keeper.GetOutstandingRewards(suite.ctx, denom1) + suite.Require().False(found) +} diff --git a/x/farming/keeper/staking.go b/x/farming/keeper/staking.go index 0f7c55c2..99467306 100644 --- a/x/farming/keeper/staking.go +++ b/x/farming/keeper/staking.go @@ -185,6 +185,11 @@ func (k Keeper) IncreaseTotalStakings(ctx sdk.Context, stakingCoinDenom string, } totalStaking.Amount = totalStaking.Amount.Add(amount) k.SetTotalStakings(ctx, stakingCoinDenom, totalStaking) + if totalStaking.Amount.Equal(amount) { + if err := k.afterStakingCoinAdded(ctx, stakingCoinDenom); err != nil { + panic(err) + } + } } // DecreaseTotalStakings decreases total stakings for given staking coin denom @@ -196,6 +201,9 @@ func (k Keeper) DecreaseTotalStakings(ctx sdk.Context, stakingCoinDenom string, } if totalStaking.Amount.Equal(amount) { k.DeleteTotalStakings(ctx, stakingCoinDenom) + if err := k.afterStakingCoinRemoved(ctx, stakingCoinDenom); err != nil { + panic(err) + } } else { totalStaking.Amount = totalStaking.Amount.Sub(amount) k.SetTotalStakings(ctx, stakingCoinDenom, totalStaking) @@ -235,6 +243,37 @@ func (k Keeper) ReleaseStakingCoins(ctx sdk.Context, farmerAcc sdk.AccAddress, u return nil } +// afterStakingCoinAdded is called after a new staking coin denom appeared +// during ProcessQueuedCoins. +func (k Keeper) afterStakingCoinAdded(ctx sdk.Context, stakingCoinDenom string) error { + k.SetHistoricalRewards(ctx, stakingCoinDenom, 0, types.HistoricalRewards{CumulativeUnitRewards: sdk.DecCoins{}}) + k.SetCurrentEpoch(ctx, stakingCoinDenom, 1) + k.SetOutstandingRewards(ctx, stakingCoinDenom, types.OutstandingRewards{Rewards: sdk.DecCoins{}}) + return nil +} + +// afterStakingCoinRemoved is called after a staking coin denom got removed +// during Unstake. +func (k Keeper) afterStakingCoinRemoved(ctx sdk.Context, stakingCoinDenom string) error { + // Send remaining outstanding rewards to the farming fee collector. + // A staking coin is removed only after there is no farmers + // have rewards. + // Note that there should never be any remaining integral rewards + // in general situations, so this exists for confidence. + outstanding, _ := k.GetOutstandingRewards(ctx, stakingCoinDenom) + coins, _ := outstanding.Rewards.TruncateDecimal() // Ignore remainder, since it cannot be sent. + if !coins.IsZero() { + if err := k.bankKeeper.SendCoins(ctx, k.GetRewardsReservePoolAcc(ctx), k.GetFarmingFeeCollectorAcc(ctx), coins); err != nil { + return err + } + } + + k.DeleteOutstandingRewards(ctx, stakingCoinDenom) + k.DeleteAllHistoricalRewards(ctx, stakingCoinDenom) + k.DeleteCurrentEpoch(ctx, stakingCoinDenom) + return nil +} + // Stake stores staking coins to queued coins, and it will be processed in the next epoch. func (k Keeper) Stake(ctx sdk.Context, farmerAcc sdk.AccAddress, amount sdk.Coins) error { if err := k.ReserveStakingCoins(ctx, farmerAcc, amount); err != nil { @@ -350,13 +389,12 @@ func (k Keeper) ProcessQueuedCoins(ctx sdk.Context) { } k.DeleteQueuedStaking(ctx, stakingCoinDenom, farmerAcc) + k.IncreaseTotalStakings(ctx, stakingCoinDenom, queuedStaking.Amount) k.SetStaking(ctx, stakingCoinDenom, farmerAcc, types.Staking{ Amount: staking.Amount.Add(queuedStaking.Amount), StartingEpoch: k.GetCurrentEpoch(ctx, stakingCoinDenom), }) - k.IncreaseTotalStakings(ctx, stakingCoinDenom, queuedStaking.Amount) - return false }) } diff --git a/x/farming/types/keys.go b/x/farming/types/keys.go index ec8d6c8a..486204b1 100644 --- a/x/farming/types/keys.go +++ b/x/farming/types/keys.go @@ -87,6 +87,12 @@ func GetHistoricalRewardsKey(stakingCoinDenom string, epoch uint64) []byte { return append(append(HistoricalRewardsKeyPrefix, LengthPrefixString(stakingCoinDenom)...), sdk.Uint64ToBigEndian(epoch)...) } +// GetHistoricalRewardsPrefix returns a key prefix used to iterate +// historical rewards by a staking coin denom. +func GetHistoricalRewardsPrefix(stakingCoinDenom string) []byte { + return append(HistoricalRewardsKeyPrefix, LengthPrefixString(stakingCoinDenom)...) +} + // GetCurrentEpochKey returns a key for a current epoch info. func GetCurrentEpochKey(stakingCoinDenom string) []byte { return append(CurrentEpochKeyPrefix, []byte(stakingCoinDenom)...)