diff --git a/x/farming/keeper/genesis.go b/x/farming/keeper/genesis.go index cdd2e18d..23a5fe8d 100644 --- a/x/farming/keeper/genesis.go +++ b/x/farming/keeper/genesis.go @@ -99,7 +99,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) { stakingReserveCoins, genState.StakingReserveCoins)) } - if err := k.ValidateOutstandingRewards(ctx); err != nil { + if err := k.ValidateOutstandingRewardsAmount(ctx); err != nil { panic(err) } diff --git a/x/farming/keeper/invariants.go b/x/farming/keeper/invariants.go index 7f8944da..c4990a00 100644 --- a/x/farming/keeper/invariants.go +++ b/x/farming/keeper/invariants.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/farming/x/farming/types" @@ -8,20 +10,87 @@ import ( // RegisterInvariants registers all farming invariants. func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { - ir.RegisterRoute(types.ModuleName, "staking-reserved", + ir.RegisterRoute(types.ModuleName, "positive-staking-amount", + PositiveStakingAmountInvariant(k)) + ir.RegisterRoute(types.ModuleName, "positive-queued-staking-amount", + PositiveQueuedStakingAmountInvariant(k)) + ir.RegisterRoute(types.ModuleName, "staking-reserved-amount", StakingReservedAmountInvariant(k)) - ir.RegisterRoute(types.ModuleName, "remaining-rewards", + ir.RegisterRoute(types.ModuleName, "remaining-rewards-amount", RemainingRewardsAmountInvariant(k)) + ir.RegisterRoute(types.ModuleName, "non-negative-outstanding-rewards", + NonNegativeOutstandingRewardsInvariant(k)) + ir.RegisterRoute(types.ModuleName, "outstanding-rewards-amount", + OutstandingRewardsAmountInvariant(k)) + ir.RegisterRoute(types.ModuleName, "non-negative-historical-rewards", + NonNegativeHistoricalRewardsInvariant(k)) + ir.RegisterRoute(types.ModuleName, "positive-total-stakings-amount", + PositiveTotalStakingsAmountInvariant(k)) + ir.RegisterRoute(types.ModuleName, "plan-termination-status", + PlanTerminationStatusInvariant(k)) } // AllInvariants runs all invariants of the farming module. func AllInvariants(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { - res, stop := StakingReservedAmountInvariant(k)(ctx) - if stop { - return res, stop + for _, inv := range []func(Keeper) sdk.Invariant{ + PositiveStakingAmountInvariant, + StakingReservedAmountInvariant, + RemainingRewardsAmountInvariant, + NonNegativeOutstandingRewardsInvariant, + OutstandingRewardsAmountInvariant, + NonNegativeHistoricalRewardsInvariant, + PositiveTotalStakingsAmountInvariant, + PlanTerminationStatusInvariant, + } { + res, stop := inv(k)(ctx) + if stop { + return res, stop + } } - return RemainingRewardsAmountInvariant(k)(ctx) + return "", false + } +} + +// PositiveStakingAmountInvariant checks that the amount of staking coins is positive. +func PositiveStakingAmountInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IterateStakings(ctx, func(stakingCoinDenom string, farmerAcc sdk.AccAddress, staking types.Staking) (stop bool) { + if !staking.Amount.IsPositive() { + msg += fmt.Sprintf("\t%v has non-positive staking amount: %v\n", + farmerAcc, sdk.Coin{Denom: stakingCoinDenom, Amount: staking.Amount}) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "positive staking amount", + fmt.Sprintf("found %d staking coins with non-positive amount\n%s", count, msg), + ), broken + } +} + +// PositiveQueuedStakingAmountInvariant checks that the amount of queued staking coins is positive. +func PositiveQueuedStakingAmountInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IterateQueuedStakings(ctx, func(stakingCoinDenom string, farmerAcc sdk.AccAddress, queuedStaking types.QueuedStaking) (stop bool) { + if !queuedStaking.Amount.IsPositive() { + msg += fmt.Sprintf("\t%v has non-positive queued staking amount: %v\n", + farmerAcc, sdk.Coin{Denom: stakingCoinDenom, Amount: queuedStaking.Amount}) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "positive queued staking amount", + fmt.Sprintf("found %d queued staking coins with non-positive amount\n%s", count, msg), + ), broken } } @@ -30,8 +99,9 @@ func StakingReservedAmountInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { err := k.ValidateStakingReservedAmount(ctx) broken := err != nil - return sdk.FormatInvariant(types.ModuleName, "staking reserved amount invariant broken", - "the balance of StakingReserveAcc less than the amount of staked, Queued coins in all staking objects"), broken + return sdk.FormatInvariant(types.ModuleName, "staking reserved amount", + "the balance of StakingReserveAcc less than the amount of staked, queued coins in all staking objects", + ), broken } } @@ -40,7 +110,118 @@ func RemainingRewardsAmountInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { err := k.ValidateRemainingRewardsAmount(ctx) broken := err != nil - return sdk.FormatInvariant(types.ModuleName, "remaining rewards amount invariant broken", - "the balance of the RewardPoolAddresses of all plans less than the total amount of unwithdrawn reward coins in all reward objects"), broken + return sdk.FormatInvariant(types.ModuleName, "remaining rewards amount", + "the balance of the RewardPoolAddresses of all plans less than the total amount of unwithdrawn reward coins in all reward objects", + ), broken + } +} + +// NonNegativeOutstandingRewardsInvariant checks that all OutstandingRewards are +// non-negative. +func NonNegativeOutstandingRewardsInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IterateOutstandingRewards(ctx, func(stakingCoinDenom string, rewards types.OutstandingRewards) (stop bool) { + if rewards.Rewards.IsAnyNegative() { + msg += fmt.Sprintf("\t%v has negative outstanding rewards: %v\n", stakingCoinDenom, rewards.Rewards) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "non-negative outstanding rewards", + fmt.Sprintf("found %d staking coin with negative outstanding rewards\n%s", count, msg), + ), broken + } +} + +// OutstandingRewardsAmountInvariant checks that OutstandingRewards are +// consistent with rewards that can be withdrawn. +func OutstandingRewardsAmountInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + totalRewards := sdk.DecCoins{} + k.IterateOutstandingRewards(ctx, func(stakingCoinDenom string, rewards types.OutstandingRewards) (stop bool) { + totalRewards = totalRewards.Add(rewards.Rewards...) + return false + }) + balances := k.bankKeeper.GetAllBalances(ctx, k.GetRewardsReservePoolAcc(ctx)) + _, hasNeg := sdk.NewDecCoinsFromCoins(balances...).SafeSub(totalRewards) + broken := hasNeg + return sdk.FormatInvariant( + types.ModuleName, "wrong outstanding rewards", + fmt.Sprintf("balance of rewards reserve pool is less than outstanding rewards\n"+ + "\texpected minimum amount of balance: %s\n"+ + "\tbalance: %s", totalRewards, balances, + ), + ), broken + } +} + +// NonNegativeHistoricalRewardsInvariant checks that all HistoricalRewards are +// non-negative. +func NonNegativeHistoricalRewardsInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IterateHistoricalRewards(ctx, func(stakingCoinDenom string, epoch uint64, rewards types.HistoricalRewards) (stop bool) { + if rewards.CumulativeUnitRewards.IsAnyNegative() { + msg += fmt.Sprintf("\t%v has negative historical rewards at epoch %d: %v\n", + stakingCoinDenom, epoch, rewards.CumulativeUnitRewards) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "non-negative historical rewards", + fmt.Sprintf("found %d staking coin with negative historical rewards\n%s", count, msg), + ), broken + } +} + +// PositiveTotalStakingsAmountInvariant checks that all TotalStakings +// have positive amount. +func PositiveTotalStakingsAmountInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IterateTotalStakings(ctx, func(stakingCoinDenom string, totalStakings types.TotalStakings) (stop bool) { + if !totalStakings.Amount.IsPositive() { + msg += fmt.Sprintf("\t%v has non-positive total staking amount: %v\n", + stakingCoinDenom, totalStakings.Amount) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "positive total stakings amount", + fmt.Sprintf("found %d total stakings with non-positive amount\n%s", count, msg), + ), broken + } +} + +// PlanTerminationStatusInvariant checks that all plans that should have been +// terminated have been terminated. +func PlanTerminationStatusInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + msg := "" + count := 0 + k.IteratePlans(ctx, func(plan types.PlanI) (stop bool) { + expected := ctx.BlockTime().After(plan.GetEndTime()) + terminated := plan.GetTerminated() + if terminated != expected { + msg += fmt.Sprintf("\tplan %d should have been terminated but not\n", plan.GetId()) + count++ + } + return false + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "plan termination status", + fmt.Sprintf("found %d plans have not been terminated\n%s", count, msg), + ), broken } } diff --git a/x/farming/keeper/invariants_test.go b/x/farming/keeper/invariants_test.go index 5cd64947..9821e0e4 100644 --- a/x/farming/keeper/invariants_test.go +++ b/x/farming/keeper/invariants_test.go @@ -1,95 +1,282 @@ package keeper_test -//func (suite *KeeperTestSuite) TestStakingReservedAmountInvariant() { -// plans := []types.PlanI{ -// types.NewFixedAmountPlan( -// types.NewBasePlan( -// 1, -// "", -// types.PlanTypePrivate, -// suite.addrs[0].String(), -// suite.addrs[0].String(), -// sdk.NewDecCoins( -// sdk.NewDecCoinFromDec(denom1, sdk.NewDecWithPrec(3, 1)), -// sdk.NewDecCoinFromDec(denom2, sdk.NewDecWithPrec(7, 1))), -// types.ParseTime("2021-07-30T00:00:00Z"), -// types.ParseTime("2021-08-30T00:00:00Z"), -// ), -// sdk.NewCoins(sdk.NewInt64Coin(denom3, 1_000_000))), -// } -// for _, plan := range plans { -// suite.keeper.SetPlan(suite.ctx, plan) -// } -// -// suite.Stake(suite.addrs[1], sdk.NewCoins( -// sdk.NewInt64Coin(denom1, 1_000_000), -// sdk.NewInt64Coin(denom2, 1_000_000))) -// -// // invariants was not broken -// keeper.AllInvariants(suite.keeper) -// invariant := keeper.AllInvariants(suite.keeper) -// _, broken := invariant(suite.ctx) -// suite.False(broken) -// -// // manipulate staking reserved amount -// stakings := suite.keeper.GetAllStakings(suite.ctx) -// stakings[0].QueuedCoins = stakings[0].QueuedCoins.Add(sdk.NewInt64Coin(denom1, 1)) -// suite.keeper.SetStaking(suite.ctx, stakings[0]) -// -// // invariants was broken -// keeper.AllInvariants(suite.keeper) -// invariant = keeper.AllInvariants(suite.keeper) -// _, broken = invariant(suite.ctx) -// suite.True(broken) -//} -// -//func (suite *KeeperTestSuite) TestRemainingRewardsAmountInvariant() { -// plans := []types.PlanI{ -// types.NewFixedAmountPlan( -// types.NewBasePlan( -// 1, -// "", -// types.PlanTypePrivate, -// suite.addrs[0].String(), -// suite.addrs[0].String(), -// sdk.NewDecCoins( -// sdk.NewDecCoinFromDec(denom1, sdk.NewDecWithPrec(3, 1)), -// sdk.NewDecCoinFromDec(denom2, sdk.NewDecWithPrec(7, 1))), -// types.ParseTime("2021-07-30T00:00:00Z"), -// types.ParseTime("2021-08-30T00:00:00Z"), -// ), -// sdk.NewCoins(sdk.NewInt64Coin(denom3, 1_000_000))), -// } -// for _, plan := range plans { -// suite.keeper.SetPlan(suite.ctx, plan) -// } -// -// suite.Stake(suite.addrs[1], sdk.NewCoins( -// sdk.NewInt64Coin(denom1, 1_000_000), -// sdk.NewInt64Coin(denom2, 1_000_000))) -// -// suite.keeper.ProcessQueuedCoins(suite.ctx) -// -// suite.ctx = suite.ctx.WithBlockTime(types.ParseTime("2021-07-31T00:00:00Z")) -// err := suite.keeper.AllocateRewards(suite.ctx) -// suite.Require().NoError(err) -// -// rewards := suite.keeper.GetRewardsByFarmer(suite.ctx, suite.addrs[1]) -// suite.Require().Len(rewards, 2) -// -// // invariants was not broken -// keeper.AllInvariants(suite.keeper) -// invariant := keeper.AllInvariants(suite.keeper) -// _, broken := invariant(suite.ctx) -// suite.False(broken) -// -// // manipulate reward coins -// rewards[0].RewardCoins = rewards[0].RewardCoins.Add(sdk.NewInt64Coin(denom3, 1)) -// suite.keeper.SetReward(suite.ctx, rewards[0].StakingCoinDenom, suite.addrs[1], rewards[0].RewardCoins) -// -// // invariants was broken -// keeper.AllInvariants(suite.keeper) -// invariant = keeper.AllInvariants(suite.keeper) -// _, broken = invariant(suite.ctx) -// suite.True(broken) -//} +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + farmingkeeper "github.com/tendermint/farming/x/farming/keeper" + "github.com/tendermint/farming/x/farming/types" +) + +func (suite *KeeperTestSuite) TestPositiveStakingAmountInvariant() { + k, ctx := suite.keeper, suite.ctx + + // Normal staking + k.SetStaking(ctx, denom1, suite.addrs[0], types.Staking{ + Amount: sdk.NewInt(1000000), + StartingEpoch: 1, + }) + _, broken := farmingkeeper.PositiveStakingAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Zero-amount staking + k.SetStaking(ctx, denom1, suite.addrs[1], types.Staking{ + Amount: sdk.ZeroInt(), + StartingEpoch: 1, + }) + _, broken = farmingkeeper.PositiveStakingAmountInvariant(k)(ctx) + suite.Require().True(broken) + + // Negative-amount staking + k.SetStaking(ctx, denom1, suite.addrs[1], types.Staking{ + Amount: sdk.NewInt(-1), + StartingEpoch: 1, + }) + _, broken = farmingkeeper.PositiveStakingAmountInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestPositiveQueuedStakingAmountInvariant() { + k, ctx := suite.keeper, suite.ctx + + // Normal queued staking + k.SetQueuedStaking(ctx, denom1, suite.addrs[0], types.QueuedStaking{ + Amount: sdk.NewInt(1000000), + }) + _, broken := farmingkeeper.PositiveQueuedStakingAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Zero-amount queued staking + k.SetQueuedStaking(ctx, denom1, suite.addrs[1], types.QueuedStaking{ + Amount: sdk.ZeroInt(), + }) + _, broken = farmingkeeper.PositiveQueuedStakingAmountInvariant(k)(ctx) + suite.Require().True(broken) + + // Negative-amount queued staking + k.SetQueuedStaking(ctx, denom1, suite.addrs[1], types.QueuedStaking{ + Amount: sdk.NewInt(-1), + }) + _, broken = farmingkeeper.PositiveQueuedStakingAmountInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestStakingReservedAmountInvariant() { + k, ctx := suite.keeper, suite.ctx + + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000000))) + suite.AdvanceEpoch() + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 500000))) + + // Check staked/queued coin amounts. + suite.Require().True(coinsEq( + sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000000)), + k.GetAllStakedCoinsByFarmer(ctx, suite.addrs[0]), + )) + suite.Require().True(coinsEq( + sdk.NewCoins(sdk.NewInt64Coin(denom1, 500000)), + k.GetAllQueuedCoinsByFarmer(ctx, suite.addrs[0]), + )) + + // This is normal state, must not be broken. + _, broken := farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().False(broken) + + staking, _ := k.GetStaking(ctx, denom1, suite.addrs[0]) + + // Staking amount in the store <= balance of staking reserve acc. This should be OK. + staking.Amount = sdk.NewInt(999999) + k.SetStaking(ctx, denom1, suite.addrs[0], staking) + _, broken = farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Staking amount in the store > balance of staking reserve acc. This shouldn't be OK. + staking.Amount = sdk.NewInt(1000001) + k.SetStaking(ctx, denom1, suite.addrs[0], staking) + _, broken = farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().True(broken) + + // Reset to the original state. + staking.Amount = sdk.NewInt(1000000) + k.SetStaking(ctx, denom1, suite.addrs[0], staking) + _, broken = farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins into the staking reserve acc. + // Staking amount in the store <= balance of staking reserve acc. This should be OK. + err := suite.app.BankKeeper.SendCoins( + ctx, suite.addrs[1], k.GetStakingReservePoolAcc(ctx), sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))) + suite.Require().NoError(err) + _, broken = farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins from staking reserve acc to another acc. + // Staking amount in the store < balance of staking reserve acc. This shouldn't be OK. + err = suite.app.BankKeeper.SendCoins( + ctx, k.GetStakingReservePoolAcc(ctx), suite.addrs[1], sdk.NewCoins(sdk.NewInt64Coin(denom1, 2))) + suite.Require().NoError(err) + _, broken = farmingkeeper.StakingReservedAmountInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestRemainingRewardsAmountInvariant() { + k, ctx := suite.keeper, suite.ctx + + 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.AdvanceEpoch() + suite.AdvanceEpoch() + suite.AdvanceEpoch() + + _, broken := farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Withdrawable rewards amount in the store > balance of rewards reserve acc. + // Should not be OK. + k.SetHistoricalRewards(ctx, denom1, 1, types.HistoricalRewards{ + CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 3)), + }) + _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) + suite.Require().True(broken) + + // Withdrawable rewards amount in the store <= balance of rewards reserve acc. + // Should be OK. + k.SetHistoricalRewards(ctx, denom1, 1, 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{ + CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 2)), + }) + _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins into the rewards reserve acc. + // Should be OK. + err := suite.app.BankKeeper.SendCoins( + ctx, suite.addrs[1], k.GetRewardsReservePoolAcc(ctx), sdk.NewCoins(sdk.NewInt64Coin(denom3, 1))) + suite.Require().NoError(err) + _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins from the rewards reserve acc to another acc. + // Should not be OK. + err = suite.app.BankKeeper.SendCoins( + ctx, k.GetRewardsReservePoolAcc(ctx), suite.addrs[1], sdk.NewCoins(sdk.NewInt64Coin(denom3, 2))) + suite.Require().NoError(err) + _, broken = farmingkeeper.RemainingRewardsAmountInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestNonNegativeOutstandingRewardsInvariant() { + k, ctx := suite.keeper, suite.ctx + + k.SetOutstandingRewards(ctx, denom1, types.OutstandingRewards{ + Rewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1000000)), + }) + _, broken := farmingkeeper.NonNegativeOutstandingRewardsInvariant(k)(ctx) + suite.Require().False(broken) + + // Zero-amount outstanding rewards + // It's acceptable, and for the initial epoch, the outstanding rewards is set to 0. + k.SetOutstandingRewards(ctx, denom2, types.OutstandingRewards{ + Rewards: sdk.DecCoins{}, + }) + _, broken = farmingkeeper.NonNegativeOutstandingRewardsInvariant(k)(ctx) + suite.Require().False(broken) + + // Delete the zero-amount outstanding rewards. + k.DeleteOutstandingRewards(ctx, denom2) + + // Negative-amount outstanding rewards + // This should not be OK. + k.SetOutstandingRewards(ctx, denom2, types.OutstandingRewards{ + Rewards: sdk.DecCoins{sdk.DecCoin{Denom: denom3, Amount: sdk.NewDec(-1)}}, + }) + _, broken = farmingkeeper.NonNegativeOutstandingRewardsInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestOutstandingRewardsAmountInvariant() { + k, ctx := suite.keeper, suite.ctx + + 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.AdvanceEpoch() + suite.AdvanceEpoch() + + _, broken := farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Outstanding rewards amount > balance of rewards reserve acc. + // Should not be OK. + k.SetOutstandingRewards(ctx, denom1, types.OutstandingRewards{ + Rewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1000001)), + }) + _, broken = farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().True(broken) + + // Outstanding rewards amount <= balance of rewards reserve acc. + // Should be OK. + k.SetOutstandingRewards(ctx, denom1, types.OutstandingRewards{ + Rewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 999999)), + }) + _, broken = farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Reset. + k.SetOutstandingRewards(ctx, denom1, types.OutstandingRewards{ + Rewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1000000)), + }) + _, broken = farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins into the rewards reserve acc. Should be OK. + err := suite.app.BankKeeper.SendCoins( + ctx, suite.addrs[1], k.GetRewardsReservePoolAcc(ctx), sdk.NewCoins(sdk.NewInt64Coin(denom3, 1))) + suite.Require().NoError(err) + _, broken = farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().False(broken) + + // Send coins from the rewards reserve acc to another acc. Should not be OK. + err = suite.app.BankKeeper.SendCoins( + ctx, k.GetRewardsReservePoolAcc(ctx), suite.addrs[1], sdk.NewCoins(sdk.NewInt64Coin(denom3, 2))) + suite.Require().NoError(err) + _, broken = farmingkeeper.OutstandingRewardsAmountInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestNonNegativeHistoricalRewardsInvariant() { + k, ctx := suite.keeper, suite.ctx + + // This is normal. + k.SetHistoricalRewards(ctx, denom1, 1, types.HistoricalRewards{ + CumulativeUnitRewards: sdk.NewDecCoins(sdk.NewInt64DecCoin(denom3, 1000000)), + }) + _, broken := farmingkeeper.NonNegativeHistoricalRewardsInvariant(k)(ctx) + suite.Require().False(broken) + + // Zero-amount historical rewards + k.SetHistoricalRewards(ctx, denom2, 1, types.HistoricalRewards{ + CumulativeUnitRewards: sdk.DecCoins{}, + }) + _, broken = farmingkeeper.NonNegativeHistoricalRewardsInvariant(k)(ctx) + suite.Require().False(broken) + + // Negative-amount historical rewards + k.SetHistoricalRewards(ctx, denom2, 1, types.HistoricalRewards{ + CumulativeUnitRewards: sdk.DecCoins{sdk.DecCoin{Denom: denom3, Amount: sdk.NewDec(-1)}}, + }) + _, broken = farmingkeeper.NonNegativeHistoricalRewardsInvariant(k)(ctx) + suite.Require().True(broken) +} + +func (suite *KeeperTestSuite) TestPositiveTotalStakingsAmountInvariant() { +} + +func (suite *KeeperTestSuite) TestPlanTerminationStatusInvariant() { +} diff --git a/x/farming/keeper/reward.go b/x/farming/keeper/reward.go index 898ede55..a6467cdd 100644 --- a/x/farming/keeper/reward.go +++ b/x/farming/keeper/reward.go @@ -444,10 +444,10 @@ func (k Keeper) ValidateRemainingRewardsAmount(ctx sdk.Context) error { return nil } -// ValidateOutstandingRewards checks that the balance of the +// ValidateOutstandingRewardsAmount checks that the balance of the // rewards reserve pool is greater than the total amount of // outstanding rewards. -func (k Keeper) ValidateOutstandingRewards(ctx sdk.Context) error { +func (k Keeper) ValidateOutstandingRewardsAmount(ctx sdk.Context) error { totalOutstandingRewards := sdk.NewDecCoins() k.IterateOutstandingRewards(ctx, func(stakingCoinDenom string, rewards types.OutstandingRewards) (stop bool) { totalOutstandingRewards = totalOutstandingRewards.Add(rewards.Rewards...) diff --git a/x/farming/keeper/staking.go b/x/farming/keeper/staking.go index 9f42a8eb..0f7c55c2 100644 --- a/x/farming/keeper/staking.go +++ b/x/farming/keeper/staking.go @@ -202,6 +202,23 @@ func (k Keeper) DecreaseTotalStakings(ctx sdk.Context, stakingCoinDenom string, } } +// IterateTotalStakings iterates through all total stakings +// stored in the store and invokes callback function for each item. +// Stops the iteration when the callback function returns true. +func (k Keeper) IterateTotalStakings(ctx sdk.Context, cb func(stakingCoinDenom string, totalStakings types.TotalStakings) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.TotalStakingKeyPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var totalStakings types.TotalStakings + k.cdc.MustUnmarshal(iter.Value(), &totalStakings) + stakingCoinDenom := types.ParseTotalStakingsKey(iter.Key()) + if cb(stakingCoinDenom, totalStakings) { + break + } + } +} + // ReserveStakingCoins sends staking coins to the staking reserve account. func (k Keeper) ReserveStakingCoins(ctx sdk.Context, farmerAcc sdk.AccAddress, stakingCoins sdk.Coins) error { if err := k.bankKeeper.SendCoins(ctx, farmerAcc, k.GetStakingReservePoolAcc(ctx), stakingCoins); err != nil { diff --git a/x/farming/types/keys.go b/x/farming/types/keys.go index 8ab0872b..ec8d6c8a 100644 --- a/x/farming/types/keys.go +++ b/x/farming/types/keys.go @@ -141,6 +141,15 @@ func ParseQueuedStakingIndexKey(key []byte) (farmerAcc sdk.AccAddress, stakingCo return } +// ParseTotalStakingsKey parses a total stakings key. +func ParseTotalStakingsKey(key []byte) (stakingCoinDenom string) { + if !bytes.HasPrefix(key, TotalStakingKeyPrefix) { + panic("key does not have proper prefix") + } + stakingCoinDenom = string(key[1:]) + return +} + // ParseHistoricalRewardsKey parses a historical rewards key. func ParseHistoricalRewardsKey(key []byte) (stakingCoinDenom string, epoch uint64) { if !bytes.HasPrefix(key, HistoricalRewardsKeyPrefix) { diff --git a/x/farming/types/keys_test.go b/x/farming/types/keys_test.go index 07e0bbd4..7e92f11b 100644 --- a/x/farming/types/keys_test.go +++ b/x/farming/types/keys_test.go @@ -204,9 +204,26 @@ func (s *keysTestSuite) TestGetQueuedStakingByFarmerPrefix() { 0x53, 0x25, 0x2c, 0x9f}, types.GetQueuedStakingByFarmerPrefix(farmer3)) } -func (s *keysTestSuite) TestGetTotalStakingKey() { - s.Require().Equal([]byte{0x25}, types.GetTotalStakingsKey("")) - s.Require().Equal([]byte{0x25, 0x73, 0x74, 0x61, 0x6b, 0x65}, types.GetTotalStakingsKey(sdk.DefaultBondDenom)) +func (s *keysTestSuite) TestGetTotalStakingsKey() { + for _, tc := range []struct { + stakingCoinDenom string + expected []byte + }{ + { + "denom1", + []byte{0x25, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x31}, + }, + { + sdk.DefaultBondDenom, + []byte{0x25, 0x73, 0x74, 0x61, 0x6b, 0x65}, + }, + } { + key := types.GetTotalStakingsKey(tc.stakingCoinDenom) + s.Require().Equal(tc.expected, key) + + stakingCoinDenom := types.ParseTotalStakingsKey(key) + s.Require().Equal(tc.stakingCoinDenom, stakingCoinDenom) + } } func (s *keysTestSuite) TestGetHistoricalRewardsKey() {