diff --git a/app/app.go b/app/app.go index c2ac222e..0e6ddf33 100644 --- a/app/app.go +++ b/app/app.go @@ -666,6 +666,7 @@ func NewApp( &hardKeeper, &app.mintKeeper, &app.kavadistKeeper, + app.stakingKeeper, ) app.incentiveKeeper = incentivekeeper.NewKeeper( diff --git a/ci/env/kava-protonet/genesis.json b/ci/env/kava-protonet/genesis.json index 19b00a48..3f55b66a 100644 --- a/ci/env/kava-protonet/genesis.json +++ b/ci/env/kava-protonet/genesis.json @@ -1317,7 +1317,12 @@ "community": { "params": { "upgrade_time_disable_inflation": "2023-11-01T00:00:00Z", - "staking_rewards_per_second": "744191" + "upgrade_time_set_staking_rewards_per_second": "744191", + "staking_rewards_per_second": "0" + }, + "staking_rewards_state": { + "last_accumulation_time": "0001-01-01T00:00:00Z", + "last_truncation_error": "0" } }, "crisis": { @@ -3627,4 +3632,4 @@ "validatorvesting": null, "vesting": {} } -} \ No newline at end of file +} diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml index c914fcc2..8d6bdf6f 100644 --- a/client/docs/swagger-ui/swagger.yaml +++ b/client/docs/swagger-ui/swagger.yaml @@ -13072,6 +13072,13 @@ paths: title: >- staking_rewards_per_second is the amount paid out to delegators each block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached description: Params defines the parameters of the community module. description: >- QueryParamsResponse defines the response type for querying @@ -57028,6 +57035,13 @@ definitions: title: >- staking_rewards_per_second is the amount paid out to delegators each block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached description: Params defines the parameters of the community module. kava.community.v1beta1.QueryBalanceResponse: type: object @@ -57069,6 +57083,13 @@ definitions: title: >- staking_rewards_per_second is the amount paid out to delegators each block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached description: Params defines the parameters of the community module. description: >- QueryParamsResponse defines the response type for querying x/community diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md index ca706e08..0391fca7 100644 --- a/docs/core/proto-docs.md +++ b/docs/core/proto-docs.md @@ -184,6 +184,9 @@ - [kava/community/v1beta1/params.proto](#kava/community/v1beta1/params.proto) - [Params](#kava.community.v1beta1.Params) +- [kava/community/v1beta1/staking.proto](#kava/community/v1beta1/staking.proto) + - [StakingRewardsState](#kava.community.v1beta1.StakingRewardsState) + - [kava/community/v1beta1/genesis.proto](#kava/community/v1beta1/genesis.proto) - [GenesisState](#kava.community.v1beta1.GenesisState) @@ -2907,6 +2910,39 @@ Params defines the parameters of the community module. | ----- | ---- | ----- | ----------- | | `upgrade_time_disable_inflation` | [google.protobuf.Timestamp](#google.protobuf.Timestamp) | | upgrade_time_disable_inflation is the time at which to disable mint and kavadist module inflation. If set to 0, inflation will be disabled from block 1. | | `staking_rewards_per_second` | [string](#string) | | staking_rewards_per_second is the amount paid out to delegators each block from the community account | +| `upgrade_time_set_staking_rewards_per_second` | [string](#string) | | upgrade_time_set_staking_rewards_per_second is the initial staking_rewards_per_second to set and use when the disable inflation time is reached | + + + + + + + + + + + + + + + + +

Top

+ +## kava/community/v1beta1/staking.proto + + + + + +### StakingRewardsState +StakingRewardsState represents the state of staking reward accumulation between blocks. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `last_accumulation_time` | [google.protobuf.Timestamp](#google.protobuf.Timestamp) | | last_accumulation_time represents the last block time which rewards where calculated and distributed. This may be zero to signal accumulation should start on the next interval. | +| `last_truncation_error` | [string](#string) | | accumulated_truncation_error represents the sum of previous errors due to truncation on payout This value will always be on the interval [0, 1). | @@ -2938,6 +2974,7 @@ GenesisState defines the community module's genesis state. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | `params` | [Params](#kava.community.v1beta1.Params) | | params defines all the paramaters related to commmunity | +| `staking_rewards_state` | [StakingRewardsState](#kava.community.v1beta1.StakingRewardsState) | | StakingRewardsState stores the internal staking reward data required to track staking rewards across blocks | diff --git a/proto/kava/community/v1beta1/genesis.proto b/proto/kava/community/v1beta1/genesis.proto index 2d75e611..08b5a4e5 100644 --- a/proto/kava/community/v1beta1/genesis.proto +++ b/proto/kava/community/v1beta1/genesis.proto @@ -3,6 +3,7 @@ package kava.community.v1beta1; import "gogoproto/gogo.proto"; import "kava/community/v1beta1/params.proto"; +import "kava/community/v1beta1/staking.proto"; option go_package = "github.com/kava-labs/kava/x/community/types"; @@ -10,4 +11,8 @@ option go_package = "github.com/kava-labs/kava/x/community/types"; message GenesisState { // params defines all the paramaters related to commmunity Params params = 1 [(gogoproto.nullable) = false]; + + // StakingRewardsState stores the internal staking reward data required to + // track staking rewards across blocks + StakingRewardsState staking_rewards_state = 2 [(gogoproto.nullable) = false]; } diff --git a/proto/kava/community/v1beta1/params.proto b/proto/kava/community/v1beta1/params.proto index ebb19b94..c09db13f 100644 --- a/proto/kava/community/v1beta1/params.proto +++ b/proto/kava/community/v1beta1/params.proto @@ -22,4 +22,12 @@ message Params { (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", (gogoproto.nullable) = false ]; + + // upgrade_time_set_staking_rewards_per_second is the initial staking_rewards_per_second to set + // and use when the disable inflation time is reached + string upgrade_time_set_staking_rewards_per_second = 3 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", + (gogoproto.nullable) = false + ]; } diff --git a/proto/kava/community/v1beta1/staking.proto b/proto/kava/community/v1beta1/staking.proto new file mode 100644 index 00000000..e49fe58f --- /dev/null +++ b/proto/kava/community/v1beta1/staking.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; +package kava.community.v1beta1; + +import "cosmos_proto/cosmos.proto"; +import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/kava-labs/kava/x/community/types"; + +// StakingRewardsState represents the state of staking reward accumulation between blocks. +message StakingRewardsState { + // last_accumulation_time represents the last block time which rewards where calculated and distributed. + // This may be zero to signal accumulation should start on the next interval. + google.protobuf.Timestamp last_accumulation_time = 1 [ + (gogoproto.stdtime) = true, + (gogoproto.nullable) = false + ]; + + // accumulated_truncation_error represents the sum of previous errors due to truncation on payout + // This value will always be on the interval [0, 1). + string last_truncation_error = 2 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", + (gogoproto.nullable) = false + ]; +} diff --git a/tests/e2e/kvtool b/tests/e2e/kvtool index 434f7d12..6184dc56 160000 --- a/tests/e2e/kvtool +++ b/tests/e2e/kvtool @@ -1 +1 @@ -Subproject commit 434f7d12322e4713429fe2cc73027938e11eb5a0 +Subproject commit 6184dc56e9b8ee94a2fe9a1e0c165d6c1129fa42 diff --git a/x/community/abci.go b/x/community/abci.go index 729a2547..b20aeafd 100644 --- a/x/community/abci.go +++ b/x/community/abci.go @@ -10,8 +10,11 @@ import ( "github.com/kava-labs/kava/x/community/types" ) +// BeginBlocker runs the community module begin blocker logic. func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) + // This exact call order is required to allow payout on the upgrade block k.CheckAndDisableMintAndKavaDistInflation(ctx) + k.PayoutAccumulatedStakingRewards(ctx) } diff --git a/x/community/abci_test.go b/x/community/abci_test.go new file mode 100644 index 00000000..1b654da7 --- /dev/null +++ b/x/community/abci_test.go @@ -0,0 +1,74 @@ +package community_test + +import ( + "testing" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/community" + "github.com/kava-labs/kava/x/community/types" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +) + +func TestABCIStakingRewardsArePaidOutOnDisableInflationBlock(t *testing.T) { + app.SetSDKConfig() + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + keeper := tApp.GetCommunityKeeper() + accountKeeper := tApp.GetAccountKeeper() + bankKeeper := tApp.GetBankKeeper() + + // a block that runs after addition of the disable inflation code on chain + // but before the disable inflation time + initialBlockTime := time.Now() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: initialBlockTime}) + + poolAcc := accountKeeper.GetModuleAccount(ctx, types.ModuleName) + feeCollectorAcc := accountKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName) + + disableTime := initialBlockTime.Add(9 * time.Second) + + // set state + params, _ := keeper.GetParams(ctx) + params.UpgradeTimeDisableInflation = disableTime + params.UpgradeTimeSetStakingRewardsPerSecond = sdkmath.LegacyNewDec(1000000) // 1 KAVA + params.StakingRewardsPerSecond = sdkmath.LegacyZeroDec() + keeper.SetParams(ctx, params) + + // fund community pool account + tApp.FundAccount(ctx, poolAcc.GetAddress(), sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(10000000)))) // 10 KAVA + initialFeeCollectorBalance := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount + + // run one block + community.BeginBlocker(ctx, keeper) + + // assert that staking rewards in parameters are still set to zero + params, found := keeper.GetParams(ctx) + require.True(t, found) + require.Equal(t, sdkmath.LegacyZeroDec(), params.StakingRewardsPerSecond) + + // assert no rewards are given yet + rewards := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount.Sub(initialFeeCollectorBalance) + require.Equal(t, sdkmath.ZeroInt(), rewards) + + // new block when disable inflation runs, 10 seconds from initial block for easy math + blockTime := disableTime.Add(1 * time.Second) + ctx = tApp.NewContext(true, tmproto.Header{Height: ctx.BlockHeight() + 1, Time: blockTime}) + + // run the next block + community.BeginBlocker(ctx, keeper) + + // assert that staking rewards have been set and disable inflation time is zero + params, found = keeper.GetParams(ctx) + require.True(t, found) + require.True(t, params.UpgradeTimeDisableInflation.IsZero()) + require.Equal(t, sdkmath.LegacyNewDec(1000000), params.StakingRewardsPerSecond) + + // assert that 10 KAVA has been distributed in rewards + rewards = bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount.Sub(initialFeeCollectorBalance) + require.Equal(t, sdkmath.NewInt(10000000).String(), rewards.String()) +} diff --git a/x/community/genesis.go b/x/community/genesis.go index c68a9720..e359fc49 100644 --- a/x/community/genesis.go +++ b/x/community/genesis.go @@ -18,6 +18,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, ak types.AccountKeeper, gs ty } k.SetParams(ctx, gs.Params) + k.SetStakingRewardsState(ctx, gs.StakingRewardsState) } // ExportGenesis exports the store to a genesis state @@ -27,5 +28,7 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState { params = types.Params{} } - return types.NewGenesisState(params) + stakingRewardsState := k.GetStakingRewardsState(ctx) + + return types.NewGenesisState(params, stakingRewardsState) } diff --git a/x/community/genesis_test.go b/x/community/genesis_test.go index 82aa0a71..1a8a2589 100644 --- a/x/community/genesis_test.go +++ b/x/community/genesis_test.go @@ -34,6 +34,11 @@ func (suite *genesisTestSuite) TestInitGenesis() { types.NewParams( time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), sdkmath.LegacyNewDec(1000), + sdkmath.LegacyNewDec(1000), + ), + types.NewStakingRewardsState( + time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC), + sdkmath.LegacyMustNewDecFromStr("0.100000000000000000"), ), ) @@ -47,21 +52,33 @@ func (suite *genesisTestSuite) TestInitGenesis() { _, ok := acc.(authtypes.ModuleAccountI) suite.True(ok) - storedParams, found := suite.App.GetCommunityKeeper().GetParams(suite.Ctx) + keeper := suite.App.GetCommunityKeeper() + storedParams, found := keeper.GetParams(suite.Ctx) suite.True(found) suite.Equal(genesisState.Params, storedParams) + + stakingRewardsState := keeper.GetStakingRewardsState(suite.Ctx) + suite.Equal(genesisState.StakingRewardsState, stakingRewardsState) } func (suite *genesisTestSuite) TestExportGenesis() { params := types.NewParams( time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), sdkmath.LegacyNewDec(1000), + sdkmath.LegacyNewDec(1000), ) suite.Keeper.SetParams(suite.Ctx, params) + stakingRewardsState := types.NewStakingRewardsState( + time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC), + sdkmath.LegacyMustNewDecFromStr("0.100000000000000000"), + ) + suite.Keeper.SetStakingRewardsState(suite.Ctx, stakingRewardsState) + genesisState := community.ExportGenesis(suite.Ctx, suite.Keeper) suite.Equal(params, genesisState.Params) + suite.Equal(stakingRewardsState, genesisState.StakingRewardsState) } func (suite *genesisTestSuite) TestInitExportIsLossless() { @@ -69,6 +86,11 @@ func (suite *genesisTestSuite) TestInitExportIsLossless() { types.NewParams( time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), sdkmath.LegacyNewDec(1000), + sdkmath.LegacyNewDec(1000), + ), + types.NewStakingRewardsState( + time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC), + sdkmath.LegacyMustNewDecFromStr("0.100000000000000000"), ), ) diff --git a/x/community/keeper/disable_inflation.go b/x/community/keeper/disable_inflation.go index f921d376..85e77d3f 100644 --- a/x/community/keeper/disable_inflation.go +++ b/x/community/keeper/disable_inflation.go @@ -10,13 +10,10 @@ import ( // and disables inflation if time is set and before block time. Inflation time is reset, // so this method is safe to call more than once. func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) { - params, found := k.GetParams(ctx) - if !found { - // panic since this can only be reached if chain state is corrupted or method is ran at an invalid height - panic("invalid state: module parameters not found") - } + // panic if params are not found since this can only be reached if chain state is corrupted or method is ran at an invalid height + params := k.mustGetParams(ctx) - // if upgrade time is in the future or zero there is nothing to do, so return + // if disable inflation time is in the future or zero there is nothing to do, so return if params.UpgradeTimeDisableInflation.IsZero() || params.UpgradeTimeDisableInflation.After(ctx.BlockTime()) { return } @@ -26,6 +23,8 @@ func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) { // reset disable inflation time to ensure next call is a no-op params.UpgradeTimeDisableInflation = time.Time{} + // set staking rewards to provided intial value + params.StakingRewardsPerSecond = params.UpgradeTimeSetStakingRewardsPerSecond k.SetParams(ctx, params) } diff --git a/x/community/keeper/grpc_query_test.go b/x/community/keeper/grpc_query_test.go index eefa7ea0..c8ba5544 100644 --- a/x/community/keeper/grpc_query_test.go +++ b/x/community/keeper/grpc_query_test.go @@ -37,6 +37,7 @@ func (suite *grpcQueryTestSuite) TestGrpcQueryParams() { p := types.NewParams( time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), sdkmath.LegacyNewDec(1000), + sdkmath.LegacyNewDec(1000), ) suite.Keeper.SetParams(suite.Ctx, p) diff --git a/x/community/keeper/keeper.go b/x/community/keeper/keeper.go index a9db95fe..f44dc266 100644 --- a/x/community/keeper/keeper.go +++ b/x/community/keeper/keeper.go @@ -23,6 +23,7 @@ type Keeper struct { moduleAddress sdk.AccAddress mintKeeper types.MintKeeper kavadistKeeper types.KavadistKeeper + stakingKeeper types.StakingKeeper legacyCommunityPoolAddress sdk.AccAddress } @@ -38,6 +39,7 @@ func NewKeeper( hk types.HardKeeper, mk types.MintKeeper, kk types.KavadistKeeper, + sk types.StakingKeeper, ) Keeper { // ensure community module account is set addr := ak.GetModuleAddress(types.ModuleAccountName) @@ -59,6 +61,7 @@ func NewKeeper( hardKeeper: hk, mintKeeper: mk, kavadistKeeper: kk, + stakingKeeper: sk, moduleAddress: addr, legacyCommunityPoolAddress: legacyAddr, @@ -84,3 +87,30 @@ func (k Keeper) FundCommunityPool(ctx sdk.Context, sender sdk.AccAddress, amount func (k Keeper) DistributeFromCommunityPool(ctx sdk.Context, recipient sdk.AccAddress, amount sdk.Coins) error { return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, recipient, amount) } + +// GetStakingRewardsState returns the staking reward state or the default state if not set +func (k Keeper) GetStakingRewardsState(ctx sdk.Context) types.StakingRewardsState { + store := ctx.KVStore(k.key) + + b := store.Get(types.StakingRewardsStateKey) + if b == nil { + return types.DefaultStakingRewardsState() + } + + state := types.StakingRewardsState{} + k.cdc.MustUnmarshal(b, &state) + + return state +} + +// SetStakingRewardsState validates and sets the staking rewards state in the store +func (k Keeper) SetStakingRewardsState(ctx sdk.Context, state types.StakingRewardsState) { + if err := state.Validate(); err != nil { + panic(fmt.Sprintf("invalid state: %s", err)) + } + + store := ctx.KVStore(k.key) + b := k.cdc.MustMarshal(&state) + + store.Set(types.StakingRewardsStateKey, b) +} diff --git a/x/community/keeper/keeper_test.go b/x/community/keeper/keeper_test.go index 25c61dc8..20916e21 100644 --- a/x/community/keeper/keeper_test.go +++ b/x/community/keeper/keeper_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -65,3 +66,26 @@ func (suite *KeeperTestSuite) TestCommunityPool() { suite.Require().ErrorContains(err, "insufficient funds") }) } + +func (suite *KeeperTestSuite) TestGetAndSetStakingRewardsState() { + keeper := suite.Keeper + + defaultParams := keeper.GetStakingRewardsState(suite.Ctx) + suite.Equal(time.Time{}, defaultParams.LastAccumulationTime, "expected default returned accumulation time to be zero") + suite.Equal(sdkmath.LegacyZeroDec(), defaultParams.LastTruncationError, "expected default truncation error to be zero") + + suite.NotPanics(func() { keeper.SetStakingRewardsState(suite.Ctx, defaultParams) }, "expected setting default state to not panic") + + invalidParams := defaultParams + invalidParams.LastTruncationError = sdkmath.LegacyDec{} + + suite.Panics(func() { keeper.SetStakingRewardsState(suite.Ctx, invalidParams) }, "expected setting invalid state to panic") + + validParams := defaultParams + validParams.LastAccumulationTime = time.Date(2023, 9, 29, 11, 42, 53, 123456789, time.UTC) + validParams.LastTruncationError = sdkmath.LegacyMustNewDecFromStr("0.50000000000000000") + + suite.NotPanics(func() { keeper.SetStakingRewardsState(suite.Ctx, validParams) }, "expected setting valid state to not panic") + + suite.Equal(validParams, keeper.GetStakingRewardsState(suite.Ctx), "expected fetched state to equal set state") +} diff --git a/x/community/keeper/params.go b/x/community/keeper/params.go index f5607bb2..9819af33 100644 --- a/x/community/keeper/params.go +++ b/x/community/keeper/params.go @@ -34,3 +34,12 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { store.Set(types.ParamsKey, bz) } + +func (k Keeper) mustGetParams(ctx sdk.Context) types.Params { + params, found := k.GetParams(ctx) + if !found { + panic("invalid state: module parameters not found") + } + + return params +} diff --git a/x/community/keeper/params_test.go b/x/community/keeper/params_test.go index f42bf1b2..ad1a7b50 100644 --- a/x/community/keeper/params_test.go +++ b/x/community/keeper/params_test.go @@ -63,6 +63,7 @@ func (suite *StoreTestSuite) TestGetSetParams() { params := types.NewParams( time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), sdkmath.LegacyNewDec(1000), + sdkmath.LegacyNewDec(1000), ) suite.Keeper.SetParams(suite.Ctx, params) diff --git a/x/community/keeper/staking.go b/x/community/keeper/staking.go new file mode 100644 index 00000000..716ee492 --- /dev/null +++ b/x/community/keeper/staking.go @@ -0,0 +1,91 @@ +package keeper + +import ( + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/kava-labs/kava/x/community/types" +) + +const nanosecondsInOneSecond = int64(1000000000) + +// PayoutAccumulatedStakingRewards calculates and transfers taking rewards to the fee collector address +func (k Keeper) PayoutAccumulatedStakingRewards(ctx sdk.Context) { + // get module parameters which define the amount of rewards to payout per second + params := k.mustGetParams(ctx) + currentBlockTime := ctx.BlockTime() + state := k.GetStakingRewardsState(ctx) + + // we have un-initialized state -- set accumulation time and exit since there is nothing to do + if state.LastAccumulationTime.IsZero() { + state.LastAccumulationTime = currentBlockTime + + k.SetStakingRewardsState(ctx, state) + + return + } + + // get the denom for staking + stakingRewardDenom := k.stakingKeeper.BondDenom(ctx) + + // we fetch the community pool balance to ensure only accumulate rewards up to the current balance + communityPoolBalance := sdkmath.LegacyNewDecFromInt(k.bankKeeper.GetBalance(ctx, k.moduleAddress, stakingRewardDenom).Amount) + + // calculate staking reward payout capped to community pool balance + truncatedRewards, truncationError := calculateStakingRewards( + currentBlockTime, + state.LastAccumulationTime, + state.LastTruncationError, + params.StakingRewardsPerSecond, + communityPoolBalance, + ) + + // only payout if the truncated rewards are non-zero + if !truncatedRewards.IsZero() { + transferAmount := sdk.NewCoins(sdk.NewCoin(stakingRewardDenom, truncatedRewards)) + + if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, authtypes.FeeCollectorName, transferAmount); err != nil { + // we check for a valid balance and rewards can never be negative so panic since this will only + // occur in cases where the chain is running in an invalid state + panic(err) + } + } + + // update accumulation state + state.LastAccumulationTime = currentBlockTime + // if the community pool balance is zero, this also resets the truncation error + state.LastTruncationError = truncationError + + // save state + k.SetStakingRewardsState(ctx, state) + + return +} + +// calculateStakingRewards takees the currentBlockTime, state of last accumulation, rewards per second, and the community pool balance +// in order to calculate the total payout since the last accumulation time. It returns the truncated payout amount and the truncation error. +func calculateStakingRewards(currentBlockTime, lastAccumulationTime time.Time, lastTruncationError, stakingRewardsPerSecond, communityPoolBalance sdkmath.LegacyDec) (sdkmath.Int, sdkmath.LegacyDec) { + // we get the duration since we last accumulated, then use nanoseconds for full precision available + durationSinceLastPayout := currentBlockTime.Sub(lastAccumulationTime) + nanosecondsSinceLastPayout := sdkmath.LegacyNewDec(durationSinceLastPayout.Nanoseconds()) + + // We multiply by nanoseconds first, then divide by conversion to avoid loss of precision. + // This multiplicaiton is also tested against very large values so we are safe from overflow + // in normal operations. + accumulatedRewards := nanosecondsSinceLastPayout.Mul(stakingRewardsPerSecond).QuoInt64(nanosecondsInOneSecond) + // Ensure we add any error from previous truncations + accumulatedRewards = accumulatedRewards.Add(lastTruncationError) + + if communityPoolBalance.LT(accumulatedRewards) { + accumulatedRewards = communityPoolBalance + } + + // we truncate since we can only transfer whole units + truncatedRewards := accumulatedRewards.TruncateDec() + // the truncation error to carry over to the next accumulation + truncationError := accumulatedRewards.Sub(truncatedRewards) + + return truncatedRewards.TruncateInt(), truncationError +} diff --git a/x/community/keeper/staking_test.go b/x/community/keeper/staking_test.go new file mode 100644 index 00000000..5b02f044 --- /dev/null +++ b/x/community/keeper/staking_test.go @@ -0,0 +1,18 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/x/community/keeper" + "github.com/kava-labs/kava/x/community/testutil" +) + +func TestKeeperPayoutAccumulatedStakingRewards(t *testing.T) { + testFunc := func(ctx sdk.Context, k keeper.Keeper) { + k.PayoutAccumulatedStakingRewards(ctx) + } + suite.Run(t, testutil.NewStakingRewardsTestSuite(testFunc)) +} diff --git a/x/community/migrations/v2/store.go b/x/community/migrations/v2/store.go index f4548781..7827b3d8 100644 --- a/x/community/migrations/v2/store.go +++ b/x/community/migrations/v2/store.go @@ -25,6 +25,7 @@ func Migrate( params := types.NewParams( time.Time{}, sdkmath.LegacyNewDec(0), + sdkmath.LegacyNewDec(0), ) if err := params.Validate(); err != nil { diff --git a/x/community/migrations/v2/store_test.go b/x/community/migrations/v2/store_test.go index facb581b..6c8deb2c 100644 --- a/x/community/migrations/v2/store_test.go +++ b/x/community/migrations/v2/store_test.go @@ -42,6 +42,7 @@ func TestMigrateStore(t *testing.T) { types.NewParams( time.Time{}, sdkmath.LegacyNewDec(0), + sdkmath.LegacyNewDec(0), ), params, "params should be correct after migration", diff --git a/x/community/staking_rewards_abci_test.go b/x/community/staking_rewards_abci_test.go new file mode 100644 index 00000000..d943170e --- /dev/null +++ b/x/community/staking_rewards_abci_test.go @@ -0,0 +1,18 @@ +package community_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/community" + "github.com/kava-labs/kava/x/community/keeper" + "github.com/kava-labs/kava/x/community/testutil" + "github.com/stretchr/testify/suite" +) + +func TestABCIPayoutAccumulatedStakingRewards(t *testing.T) { + testFunc := func(ctx sdk.Context, k keeper.Keeper) { + community.BeginBlocker(ctx, k) + } + suite.Run(t, testutil.NewStakingRewardsTestSuite(testFunc)) +} diff --git a/x/community/testutil/disable_inflation.go b/x/community/testutil/disable_inflation.go index 21bb6c0f..50646290 100644 --- a/x/community/testutil/disable_inflation.go +++ b/x/community/testutil/disable_inflation.go @@ -9,6 +9,7 @@ import ( tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + sdkmath "cosmossdk.io/math" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/community" "github.com/kava-labs/kava/x/community/keeper" @@ -64,7 +65,7 @@ func (suite *disableInflationTestSuite) SetupTest() { } func (suite *disableInflationTestSuite) TestDisableInflation() { - validateState := func(upgraded bool, expectedDisableTime time.Time, msg string) { + validateState := func(upgraded bool, expectedDisableTime time.Time, originalStakingRewards sdkmath.LegacyDec, setStakingRewards sdkmath.LegacyDec, msg string) { params, found := suite.Keeper.GetParams(suite.Ctx) suite.Require().True(found) mintParams := suite.App.GetMintKeeper().GetParams(suite.Ctx) @@ -73,6 +74,7 @@ func (suite *disableInflationTestSuite) TestDisableInflation() { disableTimeMsg := "expected inflation disable time to match" expectedMintState := suite.genesisMintState expectedKavadistState := suite.genesisKavadistState + expectedStakingRewards := originalStakingRewards msgSuffix := "before upgrade" // The state expected after upgrade time is reached @@ -84,6 +86,7 @@ func (suite *disableInflationTestSuite) TestDisableInflation() { // without extra logic or state. expectedDisableTime = time.Time{} disableTimeMsg = "expected inflation disable time to be reset" + expectedStakingRewards = setStakingRewards expectedMintState.Params.InflationMin = sdk.ZeroDec() expectedMintState.Params.InflationMax = sdk.ZeroDec() @@ -96,37 +99,49 @@ func (suite *disableInflationTestSuite) TestDisableInflation() { suite.Require().Equal(expectedMintState.Params.InflationMax, mintParams.InflationMax, msg+": expected mint inflation max to match state "+msgSuffix) suite.Require().Equal(expectedKavadistState.Params.Active, kavadistParams.Active, msg+":expected kavadist active flag match state "+msgSuffix) suite.Require().Equal(expectedDisableTime, params.UpgradeTimeDisableInflation, msg+": "+disableTimeMsg) + + // we always check staking rewards per second matches the passed in expectation + suite.Require().Equal(expectedStakingRewards, params.StakingRewardsPerSecond, msg+": "+"staking rewards per second to match "+msgSuffix) + // we don't modify or zero out the initial rewards per second for upgrade time + suite.Require().Equal(setStakingRewards, params.UpgradeTimeSetStakingRewardsPerSecond, msg+": "+"set staking rewards per second to match "+msgSuffix) } blockTime := suite.Ctx.BlockTime() testCases := []struct { - name string - upgradeTime time.Time - shouldUpgrade bool + name string + upgradeTime time.Time + setStakingRewards sdkmath.LegacyDec + shouldUpgrade bool }{ - {"zero upgrade time -- should not upgrade", time.Time{}, false}, - {"upgrade time in future -- should not upgrade", blockTime.Add(1 * time.Second), false}, - {"upgrade time in past -- should upgrade", blockTime.Add(-1 * time.Second), true}, - {"upgrade time equal to block time -- should upgrade", blockTime, true}, + {"zero upgrade time -- should not upgrade", time.Time{}, sdkmath.LegacyNewDec(1001), false}, + {"upgrade time in future -- should not upgrade", blockTime.Add(1 * time.Second), sdkmath.LegacyNewDec(1002), false}, + {"upgrade time in past -- should upgrade", blockTime.Add(-1 * time.Second), sdkmath.LegacyNewDec(1003), true}, + {"upgrade time equal to block time -- should upgrade", blockTime, sdkmath.LegacyNewDec(1004), true}, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() - // ensure state is as we expect before running upgrade or updating time - validateState(false, time.Time{}, "initial state") - - // set inflation disable time params, found := suite.Keeper.GetParams(suite.Ctx) suite.Require().True(found) + + // these should not match in order to assure assertions test correct behavior + suite.Require().NotEqual(params.StakingRewardsPerSecond, tc.setStakingRewards, "set staking rewards can not match initial staking rewards") + + // ensure state is as we expect before running upgrade or updating time + validateState(false, time.Time{}, params.StakingRewardsPerSecond, params.UpgradeTimeSetStakingRewardsPerSecond, "initial state") + + // set inflation disable time params.UpgradeTimeDisableInflation = tc.upgradeTime + // set upgrade time set staking rewards per second + params.UpgradeTimeSetStakingRewardsPerSecond = tc.setStakingRewards suite.Keeper.SetParams(suite.Ctx, params) // run test function suite.testFunc(suite.Ctx, suite.Keeper) // run assertions to ensure upgrade did or did not run - validateState(tc.shouldUpgrade, tc.upgradeTime, "first begin blocker run") + validateState(tc.shouldUpgrade, tc.upgradeTime, params.StakingRewardsPerSecond, tc.setStakingRewards, "first begin blocker run") // test idempotence only if upgrade should have been ran if tc.shouldUpgrade { @@ -134,11 +149,17 @@ func (suite *disableInflationTestSuite) TestDisableInflation() { suite.App.GetMintKeeper().SetParams(suite.Ctx, suite.genesisMintState.Params) suite.App.GetKavadistKeeper().SetParams(suite.Ctx, suite.genesisKavadistState.Params) + // modify staking rewards per second to ensure they are not overridden again + params, found := suite.Keeper.GetParams(suite.Ctx) + suite.Require().True(found) + params.StakingRewardsPerSecond = params.StakingRewardsPerSecond.Add(sdkmath.LegacyOneDec()) + suite.Keeper.SetParams(suite.Ctx, params) + // run begin blocker again community.BeginBlocker(suite.Ctx, suite.Keeper) - // ensure begin blocker is impodent and never runs twice - validateState(false, time.Time{}, "second begin blocker run") + // ensure begin blocker is idempotent and never runs twice + validateState(false, time.Time{}, params.StakingRewardsPerSecond, tc.setStakingRewards, "second begin blocker run") } }) } diff --git a/x/community/testutil/staking_rewards.go b/x/community/testutil/staking_rewards.go new file mode 100644 index 00000000..4e1efc27 --- /dev/null +++ b/x/community/testutil/staking_rewards.go @@ -0,0 +1,375 @@ +package testutil + +import ( + "fmt" + "math" + "math/rand" + "time" + + "github.com/stretchr/testify/suite" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/community" + "github.com/kava-labs/kava/x/community/keeper" + "github.com/kava-labs/kava/x/community/types" +) + +// StakingRewardsTestSuite tests staking rewards per second logic +type stakingRewardsTestSuite struct { + suite.Suite + + App app.TestApp + Keeper keeper.Keeper + + testFunc testFunc +} + +func NewStakingRewardsTestSuite(tf testFunc) *stakingRewardsTestSuite { + suite := &stakingRewardsTestSuite{} + suite.testFunc = tf + return suite +} + +// The default state used by each test +func (suite *stakingRewardsTestSuite) SetupTest() { + app.SetSDKConfig() + + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + + suite.App = tApp + suite.Keeper = suite.App.GetCommunityKeeper() +} + +func (suite *stakingRewardsTestSuite) TestStakingRewards() { + testCases := []struct { + // name of subtest + name string + + // block time of first block + periodStart time.Time + // block time of last block + periodEnd time.Time + + // block time n will be periodStart + rand(range_min...range_max)*(n-1) up to periodEnd + blockTimeRangeMin float64 + blockTimeRangeMax float64 + + // rewards per second to set in state + rewardsPerSecond sdkmath.LegacyDec + + // the amount of ukava to mint and transfer to the community pool + // to use to pay for rewards + communityPoolFunds sdkmath.Int + + // how many total rewards are expected to be accumulated in ukava + expectedRewardsTotal sdkmath.Int + }{ + // ** These take a long time to run ** + //{ + // name: "one year with 0.5 to 1 second block times", + // periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + // periodEnd: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + // blockTimeRangeMin: 0.5, + // blockTimeRangeMax: 1, + // rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("1585489.599188229325215626"), + // expectedRewardsTotal: sdkmath.NewInt(49999999999999), // 50 million KAVA per year + //}, + //{ + // name: "one year with 5.5 to 6.5 second blocktimes", + // periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + // periodEnd: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + // blockTimeRangeMin: 5.5, + // blockTimeRangeMax: 6.5, + // rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("1585489.599188229325215626"), // 50 million kava per year + // communityPoolFunds: sdkmath.NewInt(50000000000000), + // expectedRewardsTotal: sdkmath.NewInt(49999999999999), // truncation results in 1 ukava error + //}, + // + // + // One Day of blocks with different block time variations + // + // + { + name: "one day with sub-second block times and 50 million KAVA per year", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 0.1, + blockTimeRangeMax: 1, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("1585489.599188229325215626"), // 50 million kava per year + communityPoolFunds: sdkmath.NewInt(200000000000), + expectedRewardsTotal: sdkmath.NewInt(136986301369), // 50 million / 365 days - 1 ukava + + }, + { + name: "one day with 5.5 to 6.5 second block times and 50 million KAVA per year", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.5, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("1585489.599188229325215626"), // 50 million kava per year + communityPoolFunds: sdkmath.NewInt(200000000000), + expectedRewardsTotal: sdkmath.NewInt(136986301369), // 50 million / 365 days - 1 ukava + }, + // + // + // Total time span under 1 second + // + // + { + name: "single 6.9 second time span and 25 million KAVA per year", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 1, 0, 0, 6, 900000000, time.UTC), + blockTimeRangeMin: 10, // forces only two blocks -- one time span + blockTimeRangeMax: 10, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("792744.799594114662607813"), // 25 million kava per year + communityPoolFunds: sdkmath.NewInt(10000000), + expectedRewardsTotal: sdkmath.NewInt(5469939), // per second rate * 6.9 + }, + { + name: "multiple blocks across sub-second time span nd 10 million KAVA per year", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 1, 0, 0, 0, 800000000, time.UTC), + blockTimeRangeMin: 0.1, // multiple blocks in a sub-second time span + blockTimeRangeMax: 0.2, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("317097.919837645865043125"), // 10 million kava per year + communityPoolFunds: sdkmath.NewInt(300000), + expectedRewardsTotal: sdkmath.NewInt(253678), // per second rate * 0.8 + }, + // + // + // Variations of community pool balance + // + // + { + name: "community pool exact funds -- should spend community to zero and not panic", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.2, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("317097.919837645865043125"), // 10 million kava per year + communityPoolFunds: sdkmath.NewInt(27397260273), + expectedRewardsTotal: sdkmath.NewInt(27397260273), + }, + { + name: "community pool under funded -- should spend community pool to down to zero and not panic", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.5, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("1585489.599188229325215626"), // 25 million kava per year + communityPoolFunds: sdkmath.NewInt(100000000000), // under funded + expectedRewardsTotal: sdkmath.NewInt(100000000000), // rewards max is the community pool balance + }, + { + name: "community pool no funds -- should pay zero rewards and not panic", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.5, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("792744.799594114662607813"), // 25 million kava per year + communityPoolFunds: sdkmath.NewInt(0), + expectedRewardsTotal: sdkmath.NewInt(0), + }, + // + // + // Disabled + // + // + { + name: "zero rewards per second results in zero rewards paid", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.5, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("0.000000000000000000"), // 25 million kava per year + communityPoolFunds: sdkmath.NewInt(100000000000000), + expectedRewardsTotal: sdkmath.NewInt(0), + }, + // + // + // Test underlying calculations are safe and overflow/underflow bounds are reasonable + // + // + { + name: "does not overflow with extremely large per second value and extremely large single block durations", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2033, 1, 1, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 315619200, // a single 10 year long block in seconds (w/ 3 leap years) + blockTimeRangeMax: 315619200, // a single 10 year long block in seconds (w/ 3 leap years) + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("100000000000000000000000000.000000000000000000"), // 100 million kava per second in 18 decimal form + communityPoolFunds: newIntFromString("40000000000000000000000000000000000"), + expectedRewardsTotal: newIntFromString("31561920000000000000000000000000000"), // 10 years worth of rewards (with three leap years) + }, + { + name: "able to accumulate decimal ukava units across blocks", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 5.5, + blockTimeRangeMax: 6.5, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("0.100000000000000000"), // blocks are not long enough to accumulate a single ukava with this rate + communityPoolFunds: sdkmath.NewInt(10000), + expectedRewardsTotal: sdkmath.NewInt(8640), + }, + { + name: "down to 1 ukava per year can be accumulated -- we are safe from underflow at reasonably small values", + periodStart: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + periodEnd: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + blockTimeRangeMin: 60, // large block times speed up this test case + blockTimeRangeMax: 120, + rewardsPerSecond: sdkmath.LegacyMustNewDecFromStr("0.000000031709791984"), + communityPoolFunds: sdkmath.NewInt(1), + expectedRewardsTotal: sdkmath.NewInt(1), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + // keepers + keeper := suite.Keeper + accountKeeper := suite.App.GetAccountKeeper() + bankKeeper := suite.App.GetBankKeeper() + + // initial context at height 1 + height := int64(1) + blockTime := tc.periodStart + ctx := suite.App.NewContext(true, tmproto.Header{Height: height, Time: blockTime}) + + // ensure community pool balance matches the test expectations + poolAcc := accountKeeper.GetModuleAccount(ctx, types.ModuleName) + // community pool balance should start at zero + suite.Require().True(bankKeeper.GetBalance(ctx, poolAcc.GetAddress(), "ukava").Amount.IsZero(), "expected community pool to start with zero coins in test genesis") + // fund withexact amount from test case + suite.App.FundAccount(ctx, poolAcc.GetAddress(), sdk.NewCoins(sdk.NewCoin("ukava", tc.communityPoolFunds))) + + // get starting balance of fee collector to substract later in case this is non-zero in genesis + feeCollectorAcc := accountKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName) + initialFeeCollectorBalance := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount + + // set rewards per second in state + params, found := keeper.GetParams(ctx) + suite.Require().True(found) + params.StakingRewardsPerSecond = tc.rewardsPerSecond + keeper.SetParams(ctx, params) + + for { + // run community begin blocker logic + suite.testFunc(ctx, keeper) + + // exit loop if we are at last block + if blockTime.Equal(tc.periodEnd) { + break + } + + // create random block duration in nanoseconds + randomBlockDurationInSeconds := tc.blockTimeRangeMin + rand.Float64()*(tc.blockTimeRangeMax-tc.blockTimeRangeMin) + nextBlockDuration := time.Duration(randomBlockDurationInSeconds * math.Pow10(9)) + + // move to next block by incrementing height, adding random duration, and settings new context + height++ + blockTime = blockTime.Add(nextBlockDuration) + // set last block to exact end of period if we go past + if blockTime.After(tc.periodEnd) { + blockTime = tc.periodEnd + } + ctx = suite.App.NewContext(true, tmproto.Header{Height: height, Time: blockTime}) + } + + endingFeeCollectorBalance := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount + feeCollectorBalanceAdded := endingFeeCollectorBalance.Sub(initialFeeCollectorBalance) + + // assert fee pool was payed the correct rewards + suite.Equal(tc.expectedRewardsTotal.String(), feeCollectorBalanceAdded.String(), "expected fee collector balance to match") + + // assert the community pool deducted the same amount + expectedCommunityPoolBalance := tc.communityPoolFunds.Sub(tc.expectedRewardsTotal) + actualCommunityPoolBalance := bankKeeper.GetBalance(ctx, poolAcc.GetAddress(), "ukava").Amount + suite.Equal(expectedCommunityPoolBalance.String(), actualCommunityPoolBalance.String(), "expected community pool balance to match") + }) + } + +} + +func (suite *stakingRewardsTestSuite) TestStakingRewardsDoNotAccumulateWhenPoolIsDrained() { + app := suite.App + keeper := suite.Keeper + accountKeeper := suite.App.GetAccountKeeper() + bankKeeper := suite.App.GetBankKeeper() + + // first block + blockTime := time.Now() + ctx := app.NewContext(true, tmproto.Header{Height: 1, Time: blockTime}) + + poolAcc := accountKeeper.GetModuleAccount(ctx, types.ModuleName) + feeCollectorAcc := accountKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName) + + // set state to pay staking rewards + params, _ := keeper.GetParams(ctx) + // we set a decimal amount that ensures after 10 seconds we overspend the community pool + // with enough truncation error that we would have an ending balance of 20.000001 if it was + // carried over after the pool run out of funds + params.StakingRewardsPerSecond = sdkmath.LegacyMustNewDecFromStr("1000000.099999999999999999") // > 1 KAVA per second + keeper.SetParams(ctx, params) + + // fund community pool account + app.FundAccount(ctx, poolAcc.GetAddress(), sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(10000000)))) // 10 KAVA + initialFeeCollectorBalance := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount + + // run first block (no rewards hapeen on first block) + community.BeginBlocker(ctx, keeper) + + // run second block 10 seconds in future and spend all community pool rewards + blockTime = blockTime.Add(10 * time.Second) + ctx = app.NewContext(true, tmproto.Header{Height: 2, Time: blockTime}) + community.BeginBlocker(ctx, keeper) + + // run third block 10 seconds in future which no rewards will be paid + blockTime = blockTime.Add(10 * time.Second) + ctx = app.NewContext(true, tmproto.Header{Height: 3, Time: blockTime}) + community.BeginBlocker(ctx, keeper) + + // run fourth block 10 seconds in future which no rewards will be paid + blockTime = blockTime.Add(10 * time.Second) + ctx = app.NewContext(true, tmproto.Header{Height: 4, Time: blockTime}) + community.BeginBlocker(ctx, keeper) + + // refund the community pool with 100 KAVA -- plenty of funds + app.FundAccount(ctx, poolAcc.GetAddress(), sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(100000000)))) // 100 KAVA + + // run fifth block 10 seconds in future which no rewards will be paid + blockTime = blockTime.Add(10 * time.Second) + ctx = app.NewContext(true, tmproto.Header{Height: 5, Time: blockTime}) + community.BeginBlocker(ctx, keeper) + + // assert that only 20 total KAVA has been distributed in rewards + // and blocks where community pool had d + rewards := bankKeeper.GetBalance(ctx, feeCollectorAcc.GetAddress(), "ukava").Amount.Sub(initialFeeCollectorBalance) + suite.Require().Equal(sdkmath.NewInt(20000000).String(), rewards.String()) +} + +func (suite *stakingRewardsTestSuite) TestPanicsOnMissingParameters() { + suite.SetupTest() + + ctx := suite.App.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()}) + store := ctx.KVStore(suite.App.GetKVStoreKey(types.StoreKey)) + store.Delete(types.ParamsKey) + + suite.PanicsWithValue("invalid state: module parameters not found", func() { + suite.testFunc(ctx, suite.Keeper) + }) +} + +// newIntFromString returns a new sdkmath.Int from a string +func newIntFromString(str string) sdkmath.Int { + num, ok := sdkmath.NewIntFromString(str) + if !ok { + panic(fmt.Sprintf("overflow creating Int from %s", str)) + } + return num +} diff --git a/x/community/types/expected_keepers.go b/x/community/types/expected_keepers.go index a07788ff..4b88ab3a 100644 --- a/x/community/types/expected_keepers.go +++ b/x/community/types/expected_keepers.go @@ -15,10 +15,12 @@ type AccountKeeper interface { // BankKeeper defines the contract needed to be fulfilled for banking dependencies. type BankKeeper interface { + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error } // CdpKeeper defines the contract needed to be fulfilled for cdp dependencies. @@ -49,3 +51,8 @@ type KavadistKeeper interface { GetParams(ctx sdk.Context) (params kavadisttypes.Params) SetParams(ctx sdk.Context, params kavadisttypes.Params) } + +// StakingKeeper expected interface for the staking keeper +type StakingKeeper interface { + BondDenom(ctx sdk.Context) string +} diff --git a/x/community/types/genesis.go b/x/community/types/genesis.go index ec327f21..2d75fdec 100644 --- a/x/community/types/genesis.go +++ b/x/community/types/genesis.go @@ -1,9 +1,10 @@ package types // NewGenesisState returns a new genesis state object -func NewGenesisState(params Params) GenesisState { +func NewGenesisState(params Params, stakingRewardsState StakingRewardsState) GenesisState { return GenesisState{ - Params: params, + Params: params, + StakingRewardsState: stakingRewardsState, } } @@ -11,10 +12,15 @@ func NewGenesisState(params Params) GenesisState { func DefaultGenesisState() GenesisState { return NewGenesisState( DefaultParams(), + DefaultStakingRewardsState(), ) } // Validate checks the params are valid func (gs GenesisState) Validate() error { - return gs.Params.Validate() + if err := gs.Params.Validate(); err != nil { + return err + } + + return gs.StakingRewardsState.Validate() } diff --git a/x/community/types/genesis.pb.go b/x/community/types/genesis.pb.go index 7aa5d198..6917d6dc 100644 --- a/x/community/types/genesis.pb.go +++ b/x/community/types/genesis.pb.go @@ -27,6 +27,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type GenesisState struct { // params defines all the paramaters related to commmunity Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + // StakingRewardsState stores the internal staking reward data required to + // track staking rewards across blocks + StakingRewardsState StakingRewardsState `protobuf:"bytes,2,opt,name=staking_rewards_state,json=stakingRewardsState,proto3" json:"staking_rewards_state"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -69,6 +72,13 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetStakingRewardsState() StakingRewardsState { + if m != nil { + return m.StakingRewardsState + } + return StakingRewardsState{} +} + func init() { proto.RegisterType((*GenesisState)(nil), "kava.community.v1beta1.GenesisState") } @@ -78,20 +88,23 @@ func init() { } var fileDescriptor_ccf84d82ea3861e0 = []byte{ - // 201 bytes of a gzipped FileDescriptorProto + // 255 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xc9, 0x4e, 0x2c, 0x4b, 0xd4, 0x4f, 0xce, 0xcf, 0xcd, 0x2d, 0xcd, 0xcb, 0x2c, 0xa9, 0xd4, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x03, 0xa9, 0xd2, 0x83, 0xab, 0xd2, 0x83, 0xaa, 0x92, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0x2b, 0xd1, 0x07, 0xb1, 0x20, 0xaa, 0xa5, 0x94, 0x71, 0x98, 0x59, 0x90, 0x58, 0x94, 0x98, - 0x0b, 0x35, 0x52, 0xc9, 0x87, 0x8b, 0xc7, 0x1d, 0x62, 0x47, 0x70, 0x49, 0x62, 0x49, 0xaa, 0x90, - 0x0d, 0x17, 0x1b, 0x44, 0x5e, 0x82, 0x51, 0x81, 0x51, 0x83, 0xdb, 0x48, 0x4e, 0x0f, 0xbb, 0x9d, - 0x7a, 0x01, 0x60, 0x55, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, 0xf5, 0x38, 0xb9, 0x9e, - 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, - 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x76, 0x7a, 0x66, 0x49, 0x46, 0x69, - 0x12, 0xc8, 0x20, 0x7d, 0x90, 0x89, 0xba, 0x39, 0x89, 0x49, 0xc5, 0x60, 0x96, 0x7e, 0x05, 0x92, - 0x1b, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0x6e, 0x33, 0x06, 0x04, 0x00, 0x00, 0xff, - 0xff, 0xb3, 0x0f, 0x84, 0xbd, 0x16, 0x01, 0x00, 0x00, + 0x0b, 0x35, 0x52, 0x0a, 0x97, 0xc5, 0xc5, 0x25, 0x89, 0xd9, 0x99, 0x79, 0xe9, 0x10, 0x55, 0x4a, + 0x9b, 0x19, 0xb9, 0x78, 0xdc, 0x21, 0x4e, 0x09, 0x2e, 0x49, 0x2c, 0x49, 0x15, 0xb2, 0xe1, 0x62, + 0x83, 0x18, 0x23, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa7, 0x87, 0xdd, 0x69, 0x7a, 0x01, + 0x60, 0x55, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, 0xf5, 0x08, 0xa5, 0x72, 0x89, 0x42, + 0xcd, 0x8f, 0x2f, 0x4a, 0x2d, 0x4f, 0x2c, 0x4a, 0x29, 0x8e, 0x2f, 0x06, 0x19, 0x2b, 0xc1, 0x04, + 0x36, 0x4c, 0x1b, 0x97, 0x61, 0xc1, 0x10, 0x4d, 0x41, 0x10, 0x3d, 0x60, 0x97, 0x40, 0x4d, 0x16, + 0x2e, 0xc6, 0x22, 0xe5, 0x7a, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, + 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c, 0x51, 0xda, + 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x20, 0x2b, 0xf4, 0x41, 0x76, 0xe9, 0xe6, 0x24, 0x26, 0x15, + 0x83, 0x59, 0xfa, 0x15, 0x48, 0x81, 0x51, 0x52, 0x59, 0x90, 0x5a, 0x9c, 0xc4, 0x06, 0x0e, 0x03, + 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x84, 0x31, 0x9f, 0xa4, 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -114,6 +127,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.StakingRewardsState.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -146,6 +169,8 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + l = m.StakingRewardsState.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -217,6 +242,39 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StakingRewardsState", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.StakingRewardsState.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/community/types/genesis_test.go b/x/community/types/genesis_test.go index 95bbd942..124364a2 100644 --- a/x/community/types/genesis_test.go +++ b/x/community/types/genesis_test.go @@ -13,12 +13,32 @@ func TestDefaultGenesisState(t *testing.T) { require.NoError(t, defaultGen.Validate()) require.Equal(t, types.DefaultParams(), defaultGen.Params) + require.Equal(t, types.DefaultStakingRewardsState(), defaultGen.StakingRewardsState) } func TestGenesisState_ValidateParams(t *testing.T) { for _, tc := range paramTestCases { t.Run(tc.name, func(t *testing.T) { - genState := types.NewGenesisState(tc.params) + genState := types.DefaultGenesisState() + genState.Params = tc.params + + err := genState.Validate() + + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + } + }) + } +} + +func TestGenesisState_ValidateStakingRewardsState(t *testing.T) { + for _, tc := range stakingRewardsStateTestCases { + t.Run(tc.name, func(t *testing.T) { + genState := types.DefaultGenesisState() + genState.StakingRewardsState = tc.stakingRewardsState err := genState.Validate() diff --git a/x/community/types/keys.go b/x/community/types/keys.go index bcc66868..dc6f0a35 100644 --- a/x/community/types/keys.go +++ b/x/community/types/keys.go @@ -23,5 +23,6 @@ const ( // key prefixes for store var ( - ParamsKey = []byte{0x01} + ParamsKey = []byte{0x01} + StakingRewardsStateKey = []byte{0x02} ) diff --git a/x/community/types/params.go b/x/community/types/params.go index 5d4981bd..f6fd1aef 100644 --- a/x/community/types/params.go +++ b/x/community/types/params.go @@ -1,7 +1,6 @@ package types import ( - "errors" fmt "fmt" "time" @@ -10,18 +9,22 @@ import ( var ( DefaultUpgradeTimeDisableInflation = time.Time{} - // DefaultStakingRewardsPerSecond is ~4.6 KAVA per block, 6.3s block time - DefaultStakingRewardsPerSecond = sdkmath.LegacyNewDec(744191) + // DefaultStakingRewardsPerSecond is zero and should be set by genesis or upgrade + DefaultStakingRewardsPerSecond = sdkmath.LegacyNewDec(0) + // DefaultStakingRewardsPerSecond is zero and should be set by genesis or upgrade + DefaultUpgradeTimeSetStakingRewardsPerSecond = sdkmath.LegacyNewDec(0) ) // NewParams returns a new params object func NewParams( upgradeTime time.Time, stakingRewardsPerSecond sdkmath.LegacyDec, + upgradeTimeSetstakingRewardsPerSecond sdkmath.LegacyDec, ) Params { return Params{ - UpgradeTimeDisableInflation: upgradeTime, - StakingRewardsPerSecond: stakingRewardsPerSecond, + UpgradeTimeDisableInflation: upgradeTime, + StakingRewardsPerSecond: stakingRewardsPerSecond, + UpgradeTimeSetStakingRewardsPerSecond: upgradeTimeSetstakingRewardsPerSecond, } } @@ -30,6 +33,7 @@ func DefaultParams() Params { return NewParams( DefaultUpgradeTimeDisableInflation, DefaultStakingRewardsPerSecond, + DefaultUpgradeTimeSetStakingRewardsPerSecond, ) } @@ -37,12 +41,24 @@ func DefaultParams() Params { func (p Params) Validate() error { // p.UpgradeTimeDisableInflation.IsZero() is a valid state. It's taken to mean inflation will be disabled on the block 1. - if p.StakingRewardsPerSecond.IsNil() { - return errors.New("StakingRewardsPerSecond should not be nil") + if err := validateDecNotNilNonNegative(p.StakingRewardsPerSecond, "StakingRewardsPerSecond"); err != nil { + return err } - if p.StakingRewardsPerSecond.IsNegative() { - return fmt.Errorf("StakingRewardsPerSecond should not be negative: %s", p.StakingRewardsPerSecond) + if err := validateDecNotNilNonNegative(p.UpgradeTimeSetStakingRewardsPerSecond, "UpgradeTimeSetStakingRewardsPerSecond"); err != nil { + return err + } + + return nil +} + +func validateDecNotNilNonNegative(value sdkmath.LegacyDec, name string) error { + if value.IsNil() { + return fmt.Errorf("%s should not be nil", name) + } + + if value.IsNegative() { + return fmt.Errorf("%s should not be negative: %s", name, value) } return nil diff --git a/x/community/types/params.pb.go b/x/community/types/params.pb.go index 0047a3ae..8c3052c7 100644 --- a/x/community/types/params.pb.go +++ b/x/community/types/params.pb.go @@ -36,6 +36,9 @@ type Params struct { UpgradeTimeDisableInflation time.Time `protobuf:"bytes,1,opt,name=upgrade_time_disable_inflation,json=upgradeTimeDisableInflation,proto3,stdtime" json:"upgrade_time_disable_inflation"` // staking_rewards_per_second is the amount paid out to delegators each block from the community account StakingRewardsPerSecond cosmossdk_io_math.LegacyDec `protobuf:"bytes,2,opt,name=staking_rewards_per_second,json=stakingRewardsPerSecond,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"staking_rewards_per_second"` + // upgrade_time_set_staking_rewards_per_second is the initial staking_rewards_per_second to set + // and use when the disable inflation time is reached + UpgradeTimeSetStakingRewardsPerSecond cosmossdk_io_math.LegacyDec `protobuf:"bytes,3,opt,name=upgrade_time_set_staking_rewards_per_second,json=upgradeTimeSetStakingRewardsPerSecond,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"upgrade_time_set_staking_rewards_per_second"` } func (m *Params) Reset() { *m = Params{} } @@ -87,29 +90,31 @@ func init() { } var fileDescriptor_0a48475520900507 = []byte{ - // 339 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x91, 0x31, 0x4e, 0xc3, 0x30, - 0x14, 0x86, 0x63, 0x86, 0x0a, 0xc2, 0x56, 0x21, 0x28, 0xa9, 0x94, 0x54, 0xb0, 0x54, 0x42, 0xb5, - 0x55, 0xb8, 0x41, 0x55, 0x06, 0x24, 0x86, 0xaa, 0x30, 0xb1, 0x44, 0x2f, 0x89, 0xeb, 0x5a, 0x4d, - 0xe2, 0xc8, 0x76, 0x0a, 0xbd, 0x45, 0x0f, 0xc3, 0x21, 0x3a, 0x56, 0x4c, 0x88, 0xa1, 0xa0, 0xe6, - 0x22, 0xc8, 0x71, 0x8a, 0xd8, 0xde, 0x7b, 0xfe, 0xfc, 0x59, 0xef, 0xb7, 0x7b, 0xbd, 0x80, 0x25, - 0x90, 0x58, 0x64, 0x59, 0x99, 0x73, 0xbd, 0x22, 0xcb, 0x61, 0x44, 0x35, 0x0c, 0x49, 0x01, 0x12, - 0x32, 0x85, 0x0b, 0x29, 0xb4, 0x68, 0x9f, 0x1b, 0x08, 0xff, 0x41, 0xb8, 0x81, 0xbc, 0xcb, 0x58, - 0xa8, 0x4c, 0xa8, 0xb0, 0xa6, 0x88, 0x6d, 0xec, 0x15, 0xef, 0x8c, 0x09, 0x26, 0xec, 0xdc, 0x54, - 0xcd, 0x34, 0x60, 0x42, 0xb0, 0x94, 0x92, 0xba, 0x8b, 0xca, 0x19, 0xd1, 0x3c, 0xa3, 0x4a, 0x43, - 0x56, 0x58, 0xe0, 0xaa, 0x42, 0x6e, 0x6b, 0x52, 0x3f, 0xdd, 0xe6, 0xae, 0x5f, 0x16, 0x4c, 0x42, - 0x42, 0x43, 0x43, 0x85, 0x09, 0x57, 0x10, 0xa5, 0x34, 0xe4, 0xf9, 0x2c, 0x05, 0xcd, 0x45, 0xde, - 0x41, 0x3d, 0xd4, 0x3f, 0xbd, 0xf5, 0xb0, 0x95, 0xe2, 0x83, 0x14, 0x3f, 0x1f, 0xa4, 0xa3, 0xe3, - 0xcd, 0x2e, 0x70, 0xd6, 0xdf, 0x01, 0x9a, 0x76, 0x1b, 0x97, 0x39, 0x1b, 0x5b, 0xd3, 0xc3, 0x41, - 0xd4, 0xce, 0x5d, 0x4f, 0x69, 0x58, 0xf0, 0x9c, 0x85, 0x92, 0xbe, 0x82, 0x4c, 0x54, 0x58, 0x50, - 0x19, 0x2a, 0x1a, 0x8b, 0x3c, 0xe9, 0x1c, 0xf5, 0x50, 0xff, 0x64, 0x34, 0x34, 0xaa, 0xaf, 0x5d, - 0xd0, 0xb5, 0x6b, 0xaa, 0x64, 0x81, 0xb9, 0x20, 0x19, 0xe8, 0x39, 0x7e, 0xa4, 0x0c, 0xe2, 0xd5, - 0x98, 0xc6, 0x1f, 0xef, 0x03, 0xb7, 0x49, 0x61, 0x4c, 0xe3, 0xe9, 0x45, 0x23, 0x9d, 0x5a, 0xe7, - 0x84, 0xca, 0xa7, 0xda, 0x38, 0xba, 0xdf, 0xec, 0x7d, 0xb4, 0xdd, 0xfb, 0xe8, 0x67, 0xef, 0xa3, - 0x75, 0xe5, 0x3b, 0xdb, 0xca, 0x77, 0x3e, 0x2b, 0xdf, 0x79, 0xb9, 0x61, 0x5c, 0xcf, 0xcb, 0xc8, - 0x64, 0x4d, 0x4c, 0xe8, 0x83, 0x14, 0x22, 0x55, 0x57, 0xe4, 0xed, 0xdf, 0x2f, 0xe9, 0x55, 0x41, - 0x55, 0xd4, 0xaa, 0x37, 0xbe, 0xfb, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x87, 0x19, 0x27, 0xc4, - 0x01, 0x00, 0x00, + // 373 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xc1, 0x4a, 0xc3, 0x30, + 0x18, 0xc7, 0x9b, 0x09, 0x43, 0xeb, 0xad, 0x88, 0xce, 0x0e, 0xda, 0xa1, 0x08, 0x83, 0xb1, 0x84, + 0xe9, 0x1b, 0x8c, 0x79, 0x10, 0x3c, 0x8c, 0xcd, 0x93, 0x97, 0x90, 0xb6, 0x59, 0x16, 0xd6, 0x36, + 0xa5, 0x49, 0xa7, 0x7b, 0x08, 0x61, 0x0f, 0xe3, 0x43, 0xec, 0x38, 0x3c, 0x89, 0x87, 0x29, 0xdb, + 0x23, 0xf8, 0x02, 0xd2, 0xa6, 0x93, 0x09, 0xe2, 0xc1, 0xdb, 0xf7, 0x7d, 0xf9, 0xe7, 0x97, 0xef, + 0x9f, 0xef, 0x33, 0xcf, 0x27, 0x64, 0x4a, 0x90, 0x2f, 0xa2, 0x28, 0x8b, 0xb9, 0x9a, 0xa1, 0x69, + 0xc7, 0xa3, 0x8a, 0x74, 0x50, 0x42, 0x52, 0x12, 0x49, 0x98, 0xa4, 0x42, 0x09, 0xeb, 0x38, 0x17, + 0xc1, 0x6f, 0x11, 0x2c, 0x45, 0xf6, 0xa9, 0x2f, 0x64, 0x24, 0x24, 0x2e, 0x54, 0x48, 0x27, 0xfa, + 0x8a, 0x7d, 0xc4, 0x04, 0x13, 0xba, 0x9e, 0x47, 0x65, 0xd5, 0x65, 0x42, 0xb0, 0x90, 0xa2, 0x22, + 0xf3, 0xb2, 0x11, 0x52, 0x3c, 0xa2, 0x52, 0x91, 0x28, 0xd1, 0x82, 0xb3, 0xcf, 0x8a, 0x59, 0xed, + 0x17, 0x4f, 0x5b, 0xdc, 0x74, 0xb2, 0x84, 0xa5, 0x24, 0xa0, 0x38, 0x57, 0xe1, 0x80, 0x4b, 0xe2, + 0x85, 0x14, 0xf3, 0x78, 0x14, 0x12, 0xc5, 0x45, 0x5c, 0x03, 0x0d, 0xd0, 0x3c, 0xbc, 0xb4, 0xa1, + 0x86, 0xc2, 0x2d, 0x14, 0xde, 0x6d, 0xa1, 0xdd, 0xfd, 0xc5, 0xca, 0x35, 0xe6, 0xef, 0x2e, 0x18, + 0xd4, 0x4b, 0x56, 0x7e, 0xd6, 0xd3, 0xa4, 0x9b, 0x2d, 0xc8, 0x8a, 0x4d, 0x5b, 0x2a, 0x32, 0xe1, + 0x31, 0xc3, 0x29, 0x7d, 0x20, 0x69, 0x20, 0x71, 0x42, 0x53, 0x2c, 0xa9, 0x2f, 0xe2, 0xa0, 0x56, + 0x69, 0x80, 0xe6, 0x41, 0xb7, 0x93, 0xa3, 0xde, 0x56, 0x6e, 0x5d, 0xdb, 0x94, 0xc1, 0x04, 0x72, + 0x81, 0x22, 0xa2, 0xc6, 0xf0, 0x96, 0x32, 0xe2, 0xcf, 0x7a, 0xd4, 0x7f, 0x79, 0x6e, 0x9b, 0xe5, + 0x2f, 0xf4, 0xa8, 0x3f, 0x38, 0x29, 0xa1, 0x03, 0xcd, 0xec, 0xd3, 0x74, 0x58, 0x10, 0xad, 0x27, + 0x60, 0xb6, 0x7e, 0x78, 0x93, 0x54, 0xe1, 0x3f, 0x3a, 0xd8, 0xfb, 0x6f, 0x07, 0x17, 0x3b, 0xae, + 0x87, 0x54, 0x0d, 0x7f, 0xef, 0xa7, 0x7b, 0xbd, 0x58, 0x3b, 0x60, 0xb9, 0x76, 0xc0, 0xc7, 0xda, + 0x01, 0xf3, 0x8d, 0x63, 0x2c, 0x37, 0x8e, 0xf1, 0xba, 0x71, 0x8c, 0xfb, 0x16, 0xe3, 0x6a, 0x9c, + 0x79, 0xf9, 0xec, 0x51, 0xbe, 0x04, 0xed, 0x90, 0x78, 0xb2, 0x88, 0xd0, 0xe3, 0xce, 0xd6, 0xa8, + 0x59, 0x42, 0xa5, 0x57, 0x2d, 0x26, 0x70, 0xf5, 0x15, 0x00, 0x00, 0xff, 0xff, 0x8a, 0x51, 0x6e, + 0xa4, 0x54, 0x02, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -132,6 +137,16 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.UpgradeTimeSetStakingRewardsPerSecond.Size() + i -= size + if _, err := m.UpgradeTimeSetStakingRewardsPerSecond.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintParams(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a { size := m.StakingRewardsPerSecond.Size() i -= size @@ -174,6 +189,8 @@ func (m *Params) Size() (n int) { n += 1 + l + sovParams(uint64(l)) l = m.StakingRewardsPerSecond.Size() n += 1 + l + sovParams(uint64(l)) + l = m.UpgradeTimeSetStakingRewardsPerSecond.Size() + n += 1 + l + sovParams(uint64(l)) return n } @@ -279,6 +296,40 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field UpgradeTimeSetStakingRewardsPerSecond", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.UpgradeTimeSetStakingRewardsPerSecond.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:]) diff --git a/x/community/types/params_test.go b/x/community/types/params_test.go index 3398eb18..c9017c13 100644 --- a/x/community/types/params_test.go +++ b/x/community/types/params_test.go @@ -25,35 +25,66 @@ var paramTestCases = []paramTestCase{ { name: "valid params", params: types.Params{ - UpgradeTimeDisableInflation: time.Time{}, - StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), }, expectedErr: "", }, { name: "rewards per second are allowed to be zero", params: types.Params{ - UpgradeTimeDisableInflation: time.Time{}, - StakingRewardsPerSecond: sdkmath.LegacyNewDec(0), + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(0), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), }, expectedErr: "", }, { name: "nil rewards per second", params: types.Params{ - UpgradeTimeDisableInflation: time.Time{}, - StakingRewardsPerSecond: sdkmath.LegacyDec{}, + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyDec{}, + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), }, expectedErr: "StakingRewardsPerSecond should not be nil", }, { name: "negative rewards per second", params: types.Params{ - UpgradeTimeDisableInflation: time.Time{}, - StakingRewardsPerSecond: sdkmath.LegacyNewDec(-5), + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(-5), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), }, expectedErr: "StakingRewardsPerSecond should not be negative", }, + { + name: "upgrade time set rewards per second are allowed to be zero", + params: types.Params{ + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(0), + }, + expectedErr: "", + }, + { + name: "nil upgrade time set rewards per second", + params: types.Params{ + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyDec{}, + }, + expectedErr: "UpgradeTimeSetStakingRewardsPerSecond should not be nil", + }, + { + name: "upgrade time set negative rewards per second", + params: types.Params{ + UpgradeTimeDisableInflation: time.Time{}, + StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), + UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(-5), + }, + expectedErr: "UpgradeTimeSetStakingRewardsPerSecond should not be negative", + }, } func TestParamsValidate(t *testing.T) { diff --git a/x/community/types/staking.go b/x/community/types/staking.go new file mode 100644 index 00000000..4f37222d --- /dev/null +++ b/x/community/types/staking.go @@ -0,0 +1,51 @@ +package types + +import ( + "errors" + "time" + + sdkmath "cosmossdk.io/math" +) + +var ( + // DefaultLastAccumulationTime is zero + DefaultLastAccumulationTime = time.Time{} + // DefaultLastTruncationError is zero + DefaultLastTruncationError = sdkmath.LegacyZeroDec() +) + +// NewStakingRewardsState returns a new staking rewards state object +func NewStakingRewardsState( + lastAccumulationTime time.Time, + lastTruncationError sdkmath.LegacyDec, +) StakingRewardsState { + return StakingRewardsState{ + LastAccumulationTime: lastAccumulationTime, + LastTruncationError: lastTruncationError, + } +} + +// DefaultStakingRewardsState returns default params +func DefaultStakingRewardsState() StakingRewardsState { + return NewStakingRewardsState( + DefaultLastAccumulationTime, + DefaultLastTruncationError, + ) +} + +// Validate checks the params are valid +func (p StakingRewardsState) Validate() error { + if err := validateDecNotNilNonNegative(p.LastTruncationError, "LastTruncationError"); err != nil { + return err + } + + if p.LastTruncationError.GTE(sdkmath.LegacyOneDec()) { + return errors.New("LastTruncationError should not be greater or equal to 1") + } + + if p.LastAccumulationTime.IsZero() && !p.LastTruncationError.IsZero() { + return errors.New("LastTruncationError should be zero if last accumulation time is zero") + } + + return nil +} diff --git a/x/community/types/staking.pb.go b/x/community/types/staking.pb.go new file mode 100644 index 00000000..440bfd3e --- /dev/null +++ b/x/community/types/staking.pb.go @@ -0,0 +1,386 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: kava/community/v1beta1/staking.proto + +package types + +import ( + cosmossdk_io_math "cosmossdk.io/math" + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" + _ "google.golang.org/protobuf/types/known/timestamppb" + io "io" + math "math" + math_bits "math/bits" + time "time" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf +var _ = time.Kitchen + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// StakingRewardsState represents the state of staking reward accumulation between blocks. +type StakingRewardsState struct { + // last_accumulation_time represents the last block time which rewards where calculated and distributed. + // This may be zero to signal accumulation should start on the next interval. + LastAccumulationTime time.Time `protobuf:"bytes,1,opt,name=last_accumulation_time,json=lastAccumulationTime,proto3,stdtime" json:"last_accumulation_time"` + // accumulated_truncation_error represents the sum of previous errors due to truncation on payout + // This value will always be on the interval [0, 1). + LastTruncationError cosmossdk_io_math.LegacyDec `protobuf:"bytes,2,opt,name=last_truncation_error,json=lastTruncationError,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"last_truncation_error"` +} + +func (m *StakingRewardsState) Reset() { *m = StakingRewardsState{} } +func (m *StakingRewardsState) String() string { return proto.CompactTextString(m) } +func (*StakingRewardsState) ProtoMessage() {} +func (*StakingRewardsState) Descriptor() ([]byte, []int) { + return fileDescriptor_fce59dad9b680fa3, []int{0} +} +func (m *StakingRewardsState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *StakingRewardsState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_StakingRewardsState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *StakingRewardsState) XXX_Merge(src proto.Message) { + xxx_messageInfo_StakingRewardsState.Merge(m, src) +} +func (m *StakingRewardsState) XXX_Size() int { + return m.Size() +} +func (m *StakingRewardsState) XXX_DiscardUnknown() { + xxx_messageInfo_StakingRewardsState.DiscardUnknown(m) +} + +var xxx_messageInfo_StakingRewardsState proto.InternalMessageInfo + +func (m *StakingRewardsState) GetLastAccumulationTime() time.Time { + if m != nil { + return m.LastAccumulationTime + } + return time.Time{} +} + +func init() { + proto.RegisterType((*StakingRewardsState)(nil), "kava.community.v1beta1.StakingRewardsState") +} + +func init() { + proto.RegisterFile("kava/community/v1beta1/staking.proto", fileDescriptor_fce59dad9b680fa3) +} + +var fileDescriptor_fce59dad9b680fa3 = []byte{ + // 331 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x91, 0x31, 0x6e, 0x2a, 0x31, + 0x10, 0x86, 0xd7, 0xaf, 0x78, 0x7a, 0x6f, 0xd3, 0x01, 0x41, 0x84, 0x48, 0xbb, 0x28, 0x4a, 0x81, + 0x14, 0x61, 0x8b, 0xe4, 0x04, 0x41, 0xd0, 0xa5, 0x02, 0x2a, 0x1a, 0x34, 0x6b, 0x1c, 0xb3, 0x62, + 0xbd, 0x46, 0xeb, 0x59, 0x12, 0x6e, 0xc1, 0x61, 0x72, 0x08, 0x4a, 0x94, 0x0a, 0xa5, 0x20, 0x11, + 0x5c, 0x24, 0xf2, 0x1a, 0x10, 0xdd, 0xcc, 0xe8, 0x9b, 0xcf, 0xfa, 0xc7, 0xfe, 0xfd, 0x0c, 0x16, + 0xc0, 0xb8, 0x56, 0x2a, 0x4f, 0x63, 0x5c, 0xb2, 0x45, 0x3b, 0x12, 0x08, 0x6d, 0x66, 0x10, 0x66, + 0x71, 0x2a, 0xe9, 0x3c, 0xd3, 0xa8, 0x4b, 0x55, 0x4b, 0xd1, 0x33, 0x45, 0x8f, 0x54, 0xfd, 0x86, + 0x6b, 0xa3, 0xb4, 0x19, 0x17, 0x14, 0x73, 0x8d, 0x5b, 0xa9, 0x57, 0xa4, 0x96, 0xda, 0xcd, 0x6d, + 0x75, 0x9c, 0x86, 0x52, 0x6b, 0x99, 0x08, 0x56, 0x74, 0x51, 0xfe, 0xca, 0x30, 0x56, 0xc2, 0x20, + 0xa8, 0xb9, 0x03, 0xee, 0xb6, 0xc4, 0x2f, 0x0f, 0xdc, 0xdb, 0x7d, 0xf1, 0x06, 0xd9, 0xc4, 0x0c, + 0x10, 0x50, 0x94, 0x46, 0x7e, 0x35, 0x01, 0x83, 0x63, 0xe0, 0x3c, 0x57, 0x79, 0x02, 0x18, 0xeb, + 0x74, 0x6c, 0x97, 0x6b, 0xa4, 0x41, 0x9a, 0x57, 0x8f, 0x75, 0xea, 0xcc, 0xf4, 0x64, 0xa6, 0xc3, + 0x93, 0xb9, 0xf3, 0x6f, 0xbd, 0x0b, 0xbd, 0xd5, 0x77, 0x48, 0xfa, 0x15, 0xeb, 0x78, 0xbe, 0x50, + 0x58, 0xa8, 0x24, 0xfc, 0xeb, 0xc2, 0x8d, 0x59, 0x9e, 0x72, 0x67, 0x16, 0x59, 0xa6, 0xb3, 0xda, + 0x9f, 0x06, 0x69, 0xfe, 0xef, 0xb4, 0xed, 0xfa, 0xd7, 0x2e, 0xbc, 0x75, 0xf9, 0xcc, 0x64, 0x46, + 0x63, 0xcd, 0x14, 0xe0, 0x94, 0xbe, 0x08, 0x09, 0x7c, 0xd9, 0x15, 0xfc, 0xf3, 0xa3, 0xe5, 0x1f, + 0xe3, 0x77, 0x05, 0xef, 0x97, 0xad, 0x6f, 0x78, 0xd6, 0xf5, 0xac, 0xad, 0xd3, 0x5b, 0xef, 0x03, + 0xb2, 0xd9, 0x07, 0xe4, 0x67, 0x1f, 0x90, 0xd5, 0x21, 0xf0, 0x36, 0x87, 0xc0, 0xdb, 0x1e, 0x02, + 0x6f, 0xf4, 0x20, 0x63, 0x9c, 0xe6, 0x91, 0x3d, 0x30, 0xb3, 0x97, 0x6e, 0x25, 0x10, 0x99, 0xa2, + 0x62, 0xef, 0x17, 0x7f, 0x83, 0xcb, 0xb9, 0x30, 0xd1, 0xdf, 0x22, 0xe1, 0xd3, 0x6f, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x98, 0xd8, 0x77, 0x55, 0xba, 0x01, 0x00, 0x00, +} + +func (m *StakingRewardsState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *StakingRewardsState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *StakingRewardsState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.LastTruncationError.Size() + i -= size + if _, err := m.LastTruncationError.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintStaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.LastAccumulationTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.LastAccumulationTime):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintStaking(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintStaking(dAtA []byte, offset int, v uint64) int { + offset -= sovStaking(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *StakingRewardsState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.LastAccumulationTime) + n += 1 + l + sovStaking(uint64(l)) + l = m.LastTruncationError.Size() + n += 1 + l + sovStaking(uint64(l)) + return n +} + +func sovStaking(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozStaking(x uint64) (n int) { + return sovStaking(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *StakingRewardsState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: StakingRewardsState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: StakingRewardsState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastAccumulationTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthStaking + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthStaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.LastAccumulationTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastTruncationError", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthStaking + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthStaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.LastTruncationError.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipStaking(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthStaking + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipStaking(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowStaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowStaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowStaking + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthStaking + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupStaking + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthStaking + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthStaking = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowStaking = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupStaking = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/community/types/staking_test.go b/x/community/types/staking_test.go new file mode 100644 index 00000000..20e83128 --- /dev/null +++ b/x/community/types/staking_test.go @@ -0,0 +1,105 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + "github.com/kava-labs/kava/x/community/types" +) + +type stakingRewardsStateTestCase struct { + name string + stakingRewardsState types.StakingRewardsState + expectedErr string +} + +var stakingRewardsStateTestCases = []stakingRewardsStateTestCase{ + { + name: "default stakingRewardsState are valid", + stakingRewardsState: types.DefaultStakingRewardsState(), + expectedErr: "", + }, + { + name: "valid example state", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Now(), + LastTruncationError: newDecFromString("0.10000000000000000"), + }, + expectedErr: "", + }, + { + name: "last accumulation time can be zero", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Time{}, + LastTruncationError: sdkmath.LegacyZeroDec(), + }, + expectedErr: "", + }, + { + name: "nil last truncation error is invalid", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Now(), + LastTruncationError: sdkmath.LegacyDec{}, + }, + expectedErr: "LastTruncationError should not be nil", + }, + { + name: "negative last truncation error is invalid", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Now(), + LastTruncationError: newDecFromString("-0.10000000000000000"), + }, + expectedErr: "LastTruncationError should not be negative", + }, + { + name: "last truncation error equal to 1 is invalid", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Now(), + LastTruncationError: newDecFromString("1.00000000000000000"), + }, + expectedErr: "LastTruncationError should not be greater or equal to 1", + }, + { + name: "last truncation error greater than 1 is invalid", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Now(), + LastTruncationError: newDecFromString("1.00000000000000000"), + }, + expectedErr: "LastTruncationError should not be greater or equal to 1", + }, + { + name: "last truncation error can not be set if last accumulation time is zero", + stakingRewardsState: types.StakingRewardsState{ + LastAccumulationTime: time.Time{}, + LastTruncationError: newDecFromString("0.10000000000000000"), + }, + expectedErr: "LastTruncationError should be zero if last accumulation time is zero", + }, +} + +func TestStakingRewardsStateValidate(t *testing.T) { + for _, tc := range stakingRewardsStateTestCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.stakingRewardsState.Validate() + + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + } + }) + } +} + +// newDecFromString returns a new sdkmath.Int from a string +func newDecFromString(str string) sdkmath.LegacyDec { + num, err := sdkmath.LegacyNewDecFromStr(str) + if err != nil { + panic(err) + } + return num +}