Community Pool Staking Rewards Implementation & Improvements (#1742)

* add new field upgrade_time_set_staking_rewards_per_second with intention
of integrating into the disable inflation logic to set an initial
staking reward time

* when the disable inflation upgrade time occurs, set the staking rewards
per second to the value specified by the new
upgrade_time_set_staking_rewards_per_second.  This will allow a decoupled
implementation between the ugprade switching logic, and the core
functionality of paying staking rewards from the pool

* add staking rewards state to community keeper and community module
genesis that is required to calculate and track staking reward payouts
accross blocks

* add implementation of staking reward payouts

* remove unused error

* touch up tests and add a test case that fully tests behavior when pool
is drained

* add function comments

* refactor and pull out main calculation to private pure function with
no dependence on keeper

* zero out default parameters -- these are too chain specific to have
useful defaults

* small touch ups on comments, test cases

* use correct Int from sdkmath, not old sdk types; update protonet genesis
for new parmater

* fix copy pasta comment

* use bond denom from staking keeper instead of referncing ukava directly

* add staking reward state for valid genesis

* update kvtool genesis for new params and rewards state
This commit is contained in:
Nick DeLuca 2023-10-03 08:41:54 -07:00 committed by GitHub
parent 43be3815cc
commit 102cc0fff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1606 additions and 80 deletions

View File

@ -666,6 +666,7 @@ func NewApp(
&hardKeeper, &hardKeeper,
&app.mintKeeper, &app.mintKeeper,
&app.kavadistKeeper, &app.kavadistKeeper,
app.stakingKeeper,
) )
app.incentiveKeeper = incentivekeeper.NewKeeper( app.incentiveKeeper = incentivekeeper.NewKeeper(

View File

@ -1317,7 +1317,12 @@
"community": { "community": {
"params": { "params": {
"upgrade_time_disable_inflation": "2023-11-01T00:00:00Z", "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": { "crisis": {
@ -3627,4 +3632,4 @@
"validatorvesting": null, "validatorvesting": null,
"vesting": {} "vesting": {}
} }
} }

View File

@ -13072,6 +13072,13 @@ paths:
title: >- title: >-
staking_rewards_per_second is the amount paid out to staking_rewards_per_second is the amount paid out to
delegators each block from the community account 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: Params defines the parameters of the community module.
description: >- description: >-
QueryParamsResponse defines the response type for querying QueryParamsResponse defines the response type for querying
@ -57028,6 +57035,13 @@ definitions:
title: >- title: >-
staking_rewards_per_second is the amount paid out to delegators each staking_rewards_per_second is the amount paid out to delegators each
block from the community account 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: Params defines the parameters of the community module.
kava.community.v1beta1.QueryBalanceResponse: kava.community.v1beta1.QueryBalanceResponse:
type: object type: object
@ -57069,6 +57083,13 @@ definitions:
title: >- title: >-
staking_rewards_per_second is the amount paid out to delegators staking_rewards_per_second is the amount paid out to delegators
each block from the community account 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: Params defines the parameters of the community module.
description: >- description: >-
QueryParamsResponse defines the response type for querying x/community QueryParamsResponse defines the response type for querying x/community

View File

@ -184,6 +184,9 @@
- [kava/community/v1beta1/params.proto](#kava/community/v1beta1/params.proto) - [kava/community/v1beta1/params.proto](#kava/community/v1beta1/params.proto)
- [Params](#kava.community.v1beta1.Params) - [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) - [kava/community/v1beta1/genesis.proto](#kava/community/v1beta1/genesis.proto)
- [GenesisState](#kava.community.v1beta1.GenesisState) - [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. | | `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 | | `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 |
<!-- end messages -->
<!-- end enums -->
<!-- end HasExtensions -->
<!-- end services -->
<a name="kava/community/v1beta1/staking.proto"></a>
<p align="right"><a href="#top">Top</a></p>
## kava/community/v1beta1/staking.proto
<a name="kava.community.v1beta1.StakingRewardsState"></a>
### 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 | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| `params` | [Params](#kava.community.v1beta1.Params) | | params defines all the paramaters related to commmunity | | `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 |

View File

@ -3,6 +3,7 @@ package kava.community.v1beta1;
import "gogoproto/gogo.proto"; import "gogoproto/gogo.proto";
import "kava/community/v1beta1/params.proto"; import "kava/community/v1beta1/params.proto";
import "kava/community/v1beta1/staking.proto";
option go_package = "github.com/kava-labs/kava/x/community/types"; 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 { message GenesisState {
// params defines all the paramaters related to commmunity // params defines all the paramaters related to commmunity
Params params = 1 [(gogoproto.nullable) = false]; 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];
} }

View File

@ -22,4 +22,12 @@ message Params {
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false (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
];
} }

View File

@ -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
];
}

@ -1 +1 @@
Subproject commit 434f7d12322e4713429fe2cc73027938e11eb5a0 Subproject commit 6184dc56e9b8ee94a2fe9a1e0c165d6c1129fa42

View File

@ -10,8 +10,11 @@ import (
"github.com/kava-labs/kava/x/community/types" "github.com/kava-labs/kava/x/community/types"
) )
// BeginBlocker runs the community module begin blocker logic.
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) 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.CheckAndDisableMintAndKavaDistInflation(ctx)
k.PayoutAccumulatedStakingRewards(ctx)
} }

74
x/community/abci_test.go Normal file
View File

@ -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())
}

View File

@ -18,6 +18,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, ak types.AccountKeeper, gs ty
} }
k.SetParams(ctx, gs.Params) k.SetParams(ctx, gs.Params)
k.SetStakingRewardsState(ctx, gs.StakingRewardsState)
} }
// ExportGenesis exports the store to a genesis state // 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{} params = types.Params{}
} }
return types.NewGenesisState(params) stakingRewardsState := k.GetStakingRewardsState(ctx)
return types.NewGenesisState(params, stakingRewardsState)
} }

View File

@ -34,6 +34,11 @@ func (suite *genesisTestSuite) TestInitGenesis() {
types.NewParams( types.NewParams(
time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyNewDec(1000), 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) _, ok := acc.(authtypes.ModuleAccountI)
suite.True(ok) 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.True(found)
suite.Equal(genesisState.Params, storedParams) suite.Equal(genesisState.Params, storedParams)
stakingRewardsState := keeper.GetStakingRewardsState(suite.Ctx)
suite.Equal(genesisState.StakingRewardsState, stakingRewardsState)
} }
func (suite *genesisTestSuite) TestExportGenesis() { func (suite *genesisTestSuite) TestExportGenesis() {
params := types.NewParams( params := types.NewParams(
time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyNewDec(1000), sdkmath.LegacyNewDec(1000),
sdkmath.LegacyNewDec(1000),
) )
suite.Keeper.SetParams(suite.Ctx, params) 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) genesisState := community.ExportGenesis(suite.Ctx, suite.Keeper)
suite.Equal(params, genesisState.Params) suite.Equal(params, genesisState.Params)
suite.Equal(stakingRewardsState, genesisState.StakingRewardsState)
} }
func (suite *genesisTestSuite) TestInitExportIsLossless() { func (suite *genesisTestSuite) TestInitExportIsLossless() {
@ -69,6 +86,11 @@ func (suite *genesisTestSuite) TestInitExportIsLossless() {
types.NewParams( types.NewParams(
time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyNewDec(1000), sdkmath.LegacyNewDec(1000),
sdkmath.LegacyNewDec(1000),
),
types.NewStakingRewardsState(
time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyMustNewDecFromStr("0.100000000000000000"),
), ),
) )

View File

@ -10,13 +10,10 @@ import (
// and disables inflation if time is set and before block time. Inflation time is reset, // 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. // so this method is safe to call more than once.
func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) { func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) {
params, found := k.GetParams(ctx) // 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
if !found { params := k.mustGetParams(ctx)
// 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")
}
// 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()) { if params.UpgradeTimeDisableInflation.IsZero() || params.UpgradeTimeDisableInflation.After(ctx.BlockTime()) {
return return
} }
@ -26,6 +23,8 @@ func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) {
// reset disable inflation time to ensure next call is a no-op // reset disable inflation time to ensure next call is a no-op
params.UpgradeTimeDisableInflation = time.Time{} params.UpgradeTimeDisableInflation = time.Time{}
// set staking rewards to provided intial value
params.StakingRewardsPerSecond = params.UpgradeTimeSetStakingRewardsPerSecond
k.SetParams(ctx, params) k.SetParams(ctx, params)
} }

View File

@ -37,6 +37,7 @@ func (suite *grpcQueryTestSuite) TestGrpcQueryParams() {
p := types.NewParams( p := types.NewParams(
time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyNewDec(1000), sdkmath.LegacyNewDec(1000),
sdkmath.LegacyNewDec(1000),
) )
suite.Keeper.SetParams(suite.Ctx, p) suite.Keeper.SetParams(suite.Ctx, p)

View File

@ -23,6 +23,7 @@ type Keeper struct {
moduleAddress sdk.AccAddress moduleAddress sdk.AccAddress
mintKeeper types.MintKeeper mintKeeper types.MintKeeper
kavadistKeeper types.KavadistKeeper kavadistKeeper types.KavadistKeeper
stakingKeeper types.StakingKeeper
legacyCommunityPoolAddress sdk.AccAddress legacyCommunityPoolAddress sdk.AccAddress
} }
@ -38,6 +39,7 @@ func NewKeeper(
hk types.HardKeeper, hk types.HardKeeper,
mk types.MintKeeper, mk types.MintKeeper,
kk types.KavadistKeeper, kk types.KavadistKeeper,
sk types.StakingKeeper,
) Keeper { ) Keeper {
// ensure community module account is set // ensure community module account is set
addr := ak.GetModuleAddress(types.ModuleAccountName) addr := ak.GetModuleAddress(types.ModuleAccountName)
@ -59,6 +61,7 @@ func NewKeeper(
hardKeeper: hk, hardKeeper: hk,
mintKeeper: mk, mintKeeper: mk,
kavadistKeeper: kk, kavadistKeeper: kk,
stakingKeeper: sk,
moduleAddress: addr, moduleAddress: addr,
legacyCommunityPoolAddress: legacyAddr, 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 { func (k Keeper) DistributeFromCommunityPool(ctx sdk.Context, recipient sdk.AccAddress, amount sdk.Coins) error {
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, recipient, amount) 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)
}

View File

@ -2,6 +2,7 @@ package keeper_test
import ( import (
"testing" "testing"
"time"
sdkmath "cosmossdk.io/math" sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
@ -65,3 +66,26 @@ func (suite *KeeperTestSuite) TestCommunityPool() {
suite.Require().ErrorContains(err, "insufficient funds") 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")
}

View File

@ -34,3 +34,12 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
store.Set(types.ParamsKey, bz) 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
}

View File

@ -63,6 +63,7 @@ func (suite *StoreTestSuite) TestGetSetParams() {
params := types.NewParams( params := types.NewParams(
time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
sdkmath.LegacyNewDec(1000), sdkmath.LegacyNewDec(1000),
sdkmath.LegacyNewDec(1000),
) )
suite.Keeper.SetParams(suite.Ctx, params) suite.Keeper.SetParams(suite.Ctx, params)

View File

@ -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
}

View File

@ -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))
}

View File

@ -25,6 +25,7 @@ func Migrate(
params := types.NewParams( params := types.NewParams(
time.Time{}, time.Time{},
sdkmath.LegacyNewDec(0), sdkmath.LegacyNewDec(0),
sdkmath.LegacyNewDec(0),
) )
if err := params.Validate(); err != nil { if err := params.Validate(); err != nil {

View File

@ -42,6 +42,7 @@ func TestMigrateStore(t *testing.T) {
types.NewParams( types.NewParams(
time.Time{}, time.Time{},
sdkmath.LegacyNewDec(0), sdkmath.LegacyNewDec(0),
sdkmath.LegacyNewDec(0),
), ),
params, params,
"params should be correct after migration", "params should be correct after migration",

View File

@ -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))
}

View File

@ -9,6 +9,7 @@ import (
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
sdkmath "cosmossdk.io/math"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/community" "github.com/kava-labs/kava/x/community"
"github.com/kava-labs/kava/x/community/keeper" "github.com/kava-labs/kava/x/community/keeper"
@ -64,7 +65,7 @@ func (suite *disableInflationTestSuite) SetupTest() {
} }
func (suite *disableInflationTestSuite) TestDisableInflation() { 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) params, found := suite.Keeper.GetParams(suite.Ctx)
suite.Require().True(found) suite.Require().True(found)
mintParams := suite.App.GetMintKeeper().GetParams(suite.Ctx) mintParams := suite.App.GetMintKeeper().GetParams(suite.Ctx)
@ -73,6 +74,7 @@ func (suite *disableInflationTestSuite) TestDisableInflation() {
disableTimeMsg := "expected inflation disable time to match" disableTimeMsg := "expected inflation disable time to match"
expectedMintState := suite.genesisMintState expectedMintState := suite.genesisMintState
expectedKavadistState := suite.genesisKavadistState expectedKavadistState := suite.genesisKavadistState
expectedStakingRewards := originalStakingRewards
msgSuffix := "before upgrade" msgSuffix := "before upgrade"
// The state expected after upgrade time is reached // The state expected after upgrade time is reached
@ -84,6 +86,7 @@ func (suite *disableInflationTestSuite) TestDisableInflation() {
// without extra logic or state. // without extra logic or state.
expectedDisableTime = time.Time{} expectedDisableTime = time.Time{}
disableTimeMsg = "expected inflation disable time to be reset" disableTimeMsg = "expected inflation disable time to be reset"
expectedStakingRewards = setStakingRewards
expectedMintState.Params.InflationMin = sdk.ZeroDec() expectedMintState.Params.InflationMin = sdk.ZeroDec()
expectedMintState.Params.InflationMax = 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(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(expectedKavadistState.Params.Active, kavadistParams.Active, msg+":expected kavadist active flag match state "+msgSuffix)
suite.Require().Equal(expectedDisableTime, params.UpgradeTimeDisableInflation, msg+": "+disableTimeMsg) 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() blockTime := suite.Ctx.BlockTime()
testCases := []struct { testCases := []struct {
name string name string
upgradeTime time.Time upgradeTime time.Time
shouldUpgrade bool setStakingRewards sdkmath.LegacyDec
shouldUpgrade bool
}{ }{
{"zero upgrade time -- should not upgrade", time.Time{}, false}, {"zero upgrade time -- should not upgrade", time.Time{}, sdkmath.LegacyNewDec(1001), false},
{"upgrade time in future -- should not upgrade", blockTime.Add(1 * time.Second), 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), true}, {"upgrade time in past -- should upgrade", blockTime.Add(-1 * time.Second), sdkmath.LegacyNewDec(1003), true},
{"upgrade time equal to block time -- should upgrade", blockTime, true}, {"upgrade time equal to block time -- should upgrade", blockTime, sdkmath.LegacyNewDec(1004), true},
} }
for _, tc := range testCases { for _, tc := range testCases {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {
suite.SetupTest() 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) params, found := suite.Keeper.GetParams(suite.Ctx)
suite.Require().True(found) 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 params.UpgradeTimeDisableInflation = tc.upgradeTime
// set upgrade time set staking rewards per second
params.UpgradeTimeSetStakingRewardsPerSecond = tc.setStakingRewards
suite.Keeper.SetParams(suite.Ctx, params) suite.Keeper.SetParams(suite.Ctx, params)
// run test function // run test function
suite.testFunc(suite.Ctx, suite.Keeper) suite.testFunc(suite.Ctx, suite.Keeper)
// run assertions to ensure upgrade did or did not run // 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 // test idempotence only if upgrade should have been ran
if tc.shouldUpgrade { if tc.shouldUpgrade {
@ -134,11 +149,17 @@ func (suite *disableInflationTestSuite) TestDisableInflation() {
suite.App.GetMintKeeper().SetParams(suite.Ctx, suite.genesisMintState.Params) suite.App.GetMintKeeper().SetParams(suite.Ctx, suite.genesisMintState.Params)
suite.App.GetKavadistKeeper().SetParams(suite.Ctx, suite.genesisKavadistState.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 // run begin blocker again
community.BeginBlocker(suite.Ctx, suite.Keeper) community.BeginBlocker(suite.Ctx, suite.Keeper)
// ensure begin blocker is impodent and never runs twice // ensure begin blocker is idempotent and never runs twice
validateState(false, time.Time{}, "second begin blocker run") validateState(false, time.Time{}, params.StakingRewardsPerSecond, tc.setStakingRewards, "second begin blocker run")
} }
}) })
} }

View File

@ -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
}

View File

@ -15,10 +15,12 @@ type AccountKeeper interface {
// BankKeeper defines the contract needed to be fulfilled for banking dependencies. // BankKeeper defines the contract needed to be fulfilled for banking dependencies.
type BankKeeper interface { type BankKeeper interface {
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error 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 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. // 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) GetParams(ctx sdk.Context) (params kavadisttypes.Params)
SetParams(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
}

View File

@ -1,9 +1,10 @@
package types package types
// NewGenesisState returns a new genesis state object // NewGenesisState returns a new genesis state object
func NewGenesisState(params Params) GenesisState { func NewGenesisState(params Params, stakingRewardsState StakingRewardsState) GenesisState {
return GenesisState{ return GenesisState{
Params: params, Params: params,
StakingRewardsState: stakingRewardsState,
} }
} }
@ -11,10 +12,15 @@ func NewGenesisState(params Params) GenesisState {
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return NewGenesisState( return NewGenesisState(
DefaultParams(), DefaultParams(),
DefaultStakingRewardsState(),
) )
} }
// Validate checks the params are valid // Validate checks the params are valid
func (gs GenesisState) Validate() error { func (gs GenesisState) Validate() error {
return gs.Params.Validate() if err := gs.Params.Validate(); err != nil {
return err
}
return gs.StakingRewardsState.Validate()
} }

View File

@ -27,6 +27,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type GenesisState struct { type GenesisState struct {
// params defines all the paramaters related to commmunity // params defines all the paramaters related to commmunity
Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` 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{} } func (m *GenesisState) Reset() { *m = GenesisState{} }
@ -69,6 +72,13 @@ func (m *GenesisState) GetParams() Params {
return Params{} return Params{}
} }
func (m *GenesisState) GetStakingRewardsState() StakingRewardsState {
if m != nil {
return m.StakingRewardsState
}
return StakingRewardsState{}
}
func init() { func init() {
proto.RegisterType((*GenesisState)(nil), "kava.community.v1beta1.GenesisState") proto.RegisterType((*GenesisState)(nil), "kava.community.v1beta1.GenesisState")
} }
@ -78,20 +88,23 @@ func init() {
} }
var fileDescriptor_ccf84d82ea3861e0 = []byte{ 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, 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, 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, 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, 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, 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, 0x0b, 0x35, 0x52, 0x0a, 0x97, 0xc5, 0xc5, 0x25, 0x89, 0xd9, 0x99, 0x79, 0xe9, 0x10, 0x55, 0x4a,
0x0d, 0x17, 0x1b, 0x44, 0x5e, 0x82, 0x51, 0x81, 0x51, 0x83, 0xdb, 0x48, 0x4e, 0x0f, 0xbb, 0x9d, 0x9b, 0x19, 0xb9, 0x78, 0xdc, 0x21, 0x4e, 0x09, 0x2e, 0x49, 0x2c, 0x49, 0x15, 0xb2, 0xe1, 0x62,
0x7a, 0x01, 0x60, 0x55, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, 0xf5, 0x38, 0xb9, 0x9e, 0x83, 0x18, 0x23, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa7, 0x87, 0xdd, 0x69, 0x7a, 0x01,
0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x60, 0x55, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, 0xf5, 0x08, 0xa5, 0x72, 0x89, 0x42,
0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x76, 0x7a, 0x66, 0x49, 0x46, 0x69, 0xcd, 0x8f, 0x2f, 0x4a, 0x2d, 0x4f, 0x2c, 0x4a, 0x29, 0x8e, 0x2f, 0x06, 0x19, 0x2b, 0xc1, 0x04,
0x12, 0xc8, 0x20, 0x7d, 0x90, 0x89, 0xba, 0x39, 0x89, 0x49, 0xc5, 0x60, 0x96, 0x7e, 0x05, 0x92, 0x36, 0x4c, 0x1b, 0x97, 0x61, 0xc1, 0x10, 0x4d, 0x41, 0x10, 0x3d, 0x60, 0x97, 0x40, 0x4d, 0x16,
0x1b, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0x6e, 0x33, 0x06, 0x04, 0x00, 0x00, 0xff, 0x2e, 0xc6, 0x22, 0xe5, 0x7a, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9,
0xff, 0xb3, 0x0f, 0x84, 0xbd, 0x16, 0x01, 0x00, 0x00, 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) { func (m *GenesisState) Marshal() (dAtA []byte, err error) {
@ -114,6 +127,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = 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]) size, err := m.Params.MarshalToSizedBuffer(dAtA[:i])
if err != nil { if err != nil {
@ -146,6 +169,8 @@ func (m *GenesisState) Size() (n int) {
_ = l _ = l
l = m.Params.Size() l = m.Params.Size()
n += 1 + l + sovGenesis(uint64(l)) n += 1 + l + sovGenesis(uint64(l))
l = m.StakingRewardsState.Size()
n += 1 + l + sovGenesis(uint64(l))
return n return n
} }
@ -217,6 +242,39 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex 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: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:]) skippy, err := skipGenesis(dAtA[iNdEx:])

View File

@ -13,12 +13,32 @@ func TestDefaultGenesisState(t *testing.T) {
require.NoError(t, defaultGen.Validate()) require.NoError(t, defaultGen.Validate())
require.Equal(t, types.DefaultParams(), defaultGen.Params) require.Equal(t, types.DefaultParams(), defaultGen.Params)
require.Equal(t, types.DefaultStakingRewardsState(), defaultGen.StakingRewardsState)
} }
func TestGenesisState_ValidateParams(t *testing.T) { func TestGenesisState_ValidateParams(t *testing.T) {
for _, tc := range paramTestCases { for _, tc := range paramTestCases {
t.Run(tc.name, func(t *testing.T) { 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() err := genState.Validate()

View File

@ -23,5 +23,6 @@ const (
// key prefixes for store // key prefixes for store
var ( var (
ParamsKey = []byte{0x01} ParamsKey = []byte{0x01}
StakingRewardsStateKey = []byte{0x02}
) )

View File

@ -1,7 +1,6 @@
package types package types
import ( import (
"errors"
fmt "fmt" fmt "fmt"
"time" "time"
@ -10,18 +9,22 @@ import (
var ( var (
DefaultUpgradeTimeDisableInflation = time.Time{} DefaultUpgradeTimeDisableInflation = time.Time{}
// DefaultStakingRewardsPerSecond is ~4.6 KAVA per block, 6.3s block time // DefaultStakingRewardsPerSecond is zero and should be set by genesis or upgrade
DefaultStakingRewardsPerSecond = sdkmath.LegacyNewDec(744191) 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 // NewParams returns a new params object
func NewParams( func NewParams(
upgradeTime time.Time, upgradeTime time.Time,
stakingRewardsPerSecond sdkmath.LegacyDec, stakingRewardsPerSecond sdkmath.LegacyDec,
upgradeTimeSetstakingRewardsPerSecond sdkmath.LegacyDec,
) Params { ) Params {
return Params{ return Params{
UpgradeTimeDisableInflation: upgradeTime, UpgradeTimeDisableInflation: upgradeTime,
StakingRewardsPerSecond: stakingRewardsPerSecond, StakingRewardsPerSecond: stakingRewardsPerSecond,
UpgradeTimeSetStakingRewardsPerSecond: upgradeTimeSetstakingRewardsPerSecond,
} }
} }
@ -30,6 +33,7 @@ func DefaultParams() Params {
return NewParams( return NewParams(
DefaultUpgradeTimeDisableInflation, DefaultUpgradeTimeDisableInflation,
DefaultStakingRewardsPerSecond, DefaultStakingRewardsPerSecond,
DefaultUpgradeTimeSetStakingRewardsPerSecond,
) )
} }
@ -37,12 +41,24 @@ func DefaultParams() Params {
func (p Params) Validate() error { 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. // p.UpgradeTimeDisableInflation.IsZero() is a valid state. It's taken to mean inflation will be disabled on the block 1.
if p.StakingRewardsPerSecond.IsNil() { if err := validateDecNotNilNonNegative(p.StakingRewardsPerSecond, "StakingRewardsPerSecond"); err != nil {
return errors.New("StakingRewardsPerSecond should not be nil") return err
} }
if p.StakingRewardsPerSecond.IsNegative() { if err := validateDecNotNilNonNegative(p.UpgradeTimeSetStakingRewardsPerSecond, "UpgradeTimeSetStakingRewardsPerSecond"); err != nil {
return fmt.Errorf("StakingRewardsPerSecond should not be negative: %s", p.StakingRewardsPerSecond) 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 return nil

View File

@ -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"` 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 // 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"` 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{} } func (m *Params) Reset() { *m = Params{} }
@ -87,29 +90,31 @@ func init() {
} }
var fileDescriptor_0a48475520900507 = []byte{ var fileDescriptor_0a48475520900507 = []byte{
// 339 bytes of a gzipped FileDescriptorProto // 373 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x91, 0x31, 0x4e, 0xc3, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xc1, 0x4a, 0xc3, 0x30,
0x14, 0x86, 0x63, 0x86, 0x0a, 0xc2, 0x56, 0x21, 0x28, 0xa9, 0x94, 0x54, 0xb0, 0x54, 0x42, 0xb5, 0x18, 0xc7, 0x9b, 0x09, 0x43, 0xeb, 0xad, 0x88, 0xce, 0x0e, 0xda, 0xa1, 0x08, 0x83, 0xb1, 0x84,
0x55, 0xb8, 0x41, 0x55, 0x06, 0x24, 0x86, 0xaa, 0x30, 0xb1, 0x44, 0x2f, 0x89, 0xeb, 0x5a, 0x4d, 0xe9, 0x1b, 0x8c, 0x79, 0x10, 0x3c, 0x8c, 0xcd, 0x93, 0x97, 0x90, 0xb6, 0x59, 0x16, 0xd6, 0x36,
0xe2, 0xc8, 0x76, 0x0a, 0xbd, 0x45, 0x0f, 0xc3, 0x21, 0x3a, 0x56, 0x4c, 0x88, 0xa1, 0xa0, 0xe6, 0xa5, 0x49, 0xa7, 0x7b, 0x08, 0x61, 0x0f, 0xe3, 0x43, 0xec, 0x38, 0x3c, 0x89, 0x87, 0x29, 0xdb,
0x22, 0xc8, 0x71, 0x8a, 0xd8, 0xde, 0x7b, 0xfe, 0xfc, 0x59, 0xef, 0xb7, 0x7b, 0xbd, 0x80, 0x25, 0x23, 0xf8, 0x02, 0xd2, 0xa6, 0x93, 0x09, 0xe2, 0xc1, 0xdb, 0xf7, 0x7d, 0xf9, 0xe7, 0x97, 0xef,
0x90, 0x58, 0x64, 0x59, 0x99, 0x73, 0xbd, 0x22, 0xcb, 0x61, 0x44, 0x35, 0x0c, 0x49, 0x01, 0x12, 0x9f, 0xef, 0x33, 0xcf, 0x27, 0x64, 0x4a, 0x90, 0x2f, 0xa2, 0x28, 0x8b, 0xb9, 0x9a, 0xa1, 0x69,
0x32, 0x85, 0x0b, 0x29, 0xb4, 0x68, 0x9f, 0x1b, 0x08, 0xff, 0x41, 0xb8, 0x81, 0xbc, 0xcb, 0x58, 0xc7, 0xa3, 0x8a, 0x74, 0x50, 0x42, 0x52, 0x12, 0x49, 0x98, 0xa4, 0x42, 0x09, 0xeb, 0x38, 0x17,
0xa8, 0x4c, 0xa8, 0xb0, 0xa6, 0x88, 0x6d, 0xec, 0x15, 0xef, 0x8c, 0x09, 0x26, 0xec, 0xdc, 0x54, 0xc1, 0x6f, 0x11, 0x2c, 0x45, 0xf6, 0xa9, 0x2f, 0x64, 0x24, 0x24, 0x2e, 0x54, 0x48, 0x27, 0xfa,
0xcd, 0x34, 0x60, 0x42, 0xb0, 0x94, 0x92, 0xba, 0x8b, 0xca, 0x19, 0xd1, 0x3c, 0xa3, 0x4a, 0x43, 0x8a, 0x7d, 0xc4, 0x04, 0x13, 0xba, 0x9e, 0x47, 0x65, 0xd5, 0x65, 0x42, 0xb0, 0x90, 0xa2, 0x22,
0x56, 0x58, 0xe0, 0xaa, 0x42, 0x6e, 0x6b, 0x52, 0x3f, 0xdd, 0xe6, 0xae, 0x5f, 0x16, 0x4c, 0x42, 0xf3, 0xb2, 0x11, 0x52, 0x3c, 0xa2, 0x52, 0x91, 0x28, 0xd1, 0x82, 0xb3, 0xcf, 0x8a, 0x59, 0xed,
0x42, 0x43, 0x43, 0x85, 0x09, 0x57, 0x10, 0xa5, 0x34, 0xe4, 0xf9, 0x2c, 0x05, 0xcd, 0x45, 0xde, 0x17, 0x4f, 0x5b, 0xdc, 0x74, 0xb2, 0x84, 0xa5, 0x24, 0xa0, 0x38, 0x57, 0xe1, 0x80, 0x4b, 0xe2,
0x41, 0x3d, 0xd4, 0x3f, 0xbd, 0xf5, 0xb0, 0x95, 0xe2, 0x83, 0x14, 0x3f, 0x1f, 0xa4, 0xa3, 0xe3, 0x85, 0x14, 0xf3, 0x78, 0x14, 0x12, 0xc5, 0x45, 0x5c, 0x03, 0x0d, 0xd0, 0x3c, 0xbc, 0xb4, 0xa1,
0xcd, 0x2e, 0x70, 0xd6, 0xdf, 0x01, 0x9a, 0x76, 0x1b, 0x97, 0x39, 0x1b, 0x5b, 0xd3, 0xc3, 0x41, 0x86, 0xc2, 0x2d, 0x14, 0xde, 0x6d, 0xa1, 0xdd, 0xfd, 0xc5, 0xca, 0x35, 0xe6, 0xef, 0x2e, 0x18,
0xd4, 0xce, 0x5d, 0x4f, 0x69, 0x58, 0xf0, 0x9c, 0x85, 0x92, 0xbe, 0x82, 0x4c, 0x54, 0x58, 0x50, 0xd4, 0x4b, 0x56, 0x7e, 0xd6, 0xd3, 0xa4, 0x9b, 0x2d, 0xc8, 0x8a, 0x4d, 0x5b, 0x2a, 0x32, 0xe1,
0x19, 0x2a, 0x1a, 0x8b, 0x3c, 0xe9, 0x1c, 0xf5, 0x50, 0xff, 0x64, 0x34, 0x34, 0xaa, 0xaf, 0x5d, 0x31, 0xc3, 0x29, 0x7d, 0x20, 0x69, 0x20, 0x71, 0x42, 0x53, 0x2c, 0xa9, 0x2f, 0xe2, 0xa0, 0x56,
0xd0, 0xb5, 0x6b, 0xaa, 0x64, 0x81, 0xb9, 0x20, 0x19, 0xe8, 0x39, 0x7e, 0xa4, 0x0c, 0xe2, 0xd5, 0x69, 0x80, 0xe6, 0x41, 0xb7, 0x93, 0xa3, 0xde, 0x56, 0x6e, 0x5d, 0xdb, 0x94, 0xc1, 0x04, 0x72,
0x98, 0xc6, 0x1f, 0xef, 0x03, 0xb7, 0x49, 0x61, 0x4c, 0xe3, 0xe9, 0x45, 0x23, 0x9d, 0x5a, 0xe7, 0x81, 0x22, 0xa2, 0xc6, 0xf0, 0x96, 0x32, 0xe2, 0xcf, 0x7a, 0xd4, 0x7f, 0x79, 0x6e, 0x9b, 0xe5,
0x84, 0xca, 0xa7, 0xda, 0x38, 0xba, 0xdf, 0xec, 0x7d, 0xb4, 0xdd, 0xfb, 0xe8, 0x67, 0xef, 0xa3, 0x2f, 0xf4, 0xa8, 0x3f, 0x38, 0x29, 0xa1, 0x03, 0xcd, 0xec, 0xd3, 0x74, 0x58, 0x10, 0xad, 0x27,
0x75, 0xe5, 0x3b, 0xdb, 0xca, 0x77, 0x3e, 0x2b, 0xdf, 0x79, 0xb9, 0x61, 0x5c, 0xcf, 0xcb, 0xc8, 0x60, 0xb6, 0x7e, 0x78, 0x93, 0x54, 0xe1, 0x3f, 0x3a, 0xd8, 0xfb, 0x6f, 0x07, 0x17, 0x3b, 0xae,
0x64, 0x4d, 0x4c, 0xe8, 0x83, 0x14, 0x22, 0x55, 0x57, 0xe4, 0xed, 0xdf, 0x2f, 0xe9, 0x55, 0x41, 0x87, 0x54, 0x0d, 0x7f, 0xef, 0xa7, 0x7b, 0xbd, 0x58, 0x3b, 0x60, 0xb9, 0x76, 0xc0, 0xc7, 0xda,
0x55, 0xd4, 0xaa, 0x37, 0xbe, 0xfb, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x87, 0x19, 0x27, 0xc4, 0x01, 0xf3, 0x8d, 0x63, 0x2c, 0x37, 0x8e, 0xf1, 0xba, 0x71, 0x8c, 0xfb, 0x16, 0xe3, 0x6a, 0x9c,
0x01, 0x00, 0x00, 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) { func (m *Params) Marshal() (dAtA []byte, err error) {
@ -132,6 +137,16 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = 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() size := m.StakingRewardsPerSecond.Size()
i -= size i -= size
@ -174,6 +189,8 @@ func (m *Params) Size() (n int) {
n += 1 + l + sovParams(uint64(l)) n += 1 + l + sovParams(uint64(l))
l = m.StakingRewardsPerSecond.Size() l = m.StakingRewardsPerSecond.Size()
n += 1 + l + sovParams(uint64(l)) n += 1 + l + sovParams(uint64(l))
l = m.UpgradeTimeSetStakingRewardsPerSecond.Size()
n += 1 + l + sovParams(uint64(l))
return n return n
} }
@ -279,6 +296,40 @@ func (m *Params) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex 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: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipParams(dAtA[iNdEx:]) skippy, err := skipParams(dAtA[iNdEx:])

View File

@ -25,35 +25,66 @@ var paramTestCases = []paramTestCase{
{ {
name: "valid params", name: "valid params",
params: types.Params{ params: types.Params{
UpgradeTimeDisableInflation: time.Time{}, UpgradeTimeDisableInflation: time.Time{},
StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000), StakingRewardsPerSecond: sdkmath.LegacyNewDec(1000),
UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000),
}, },
expectedErr: "", expectedErr: "",
}, },
{ {
name: "rewards per second are allowed to be zero", name: "rewards per second are allowed to be zero",
params: types.Params{ params: types.Params{
UpgradeTimeDisableInflation: time.Time{}, UpgradeTimeDisableInflation: time.Time{},
StakingRewardsPerSecond: sdkmath.LegacyNewDec(0), StakingRewardsPerSecond: sdkmath.LegacyNewDec(0),
UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000),
}, },
expectedErr: "", expectedErr: "",
}, },
{ {
name: "nil rewards per second", name: "nil rewards per second",
params: types.Params{ params: types.Params{
UpgradeTimeDisableInflation: time.Time{}, UpgradeTimeDisableInflation: time.Time{},
StakingRewardsPerSecond: sdkmath.LegacyDec{}, StakingRewardsPerSecond: sdkmath.LegacyDec{},
UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000),
}, },
expectedErr: "StakingRewardsPerSecond should not be nil", expectedErr: "StakingRewardsPerSecond should not be nil",
}, },
{ {
name: "negative rewards per second", name: "negative rewards per second",
params: types.Params{ params: types.Params{
UpgradeTimeDisableInflation: time.Time{}, UpgradeTimeDisableInflation: time.Time{},
StakingRewardsPerSecond: sdkmath.LegacyNewDec(-5), StakingRewardsPerSecond: sdkmath.LegacyNewDec(-5),
UpgradeTimeSetStakingRewardsPerSecond: sdkmath.LegacyNewDec(1000),
}, },
expectedErr: "StakingRewardsPerSecond should not be negative", 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) { func TestParamsValidate(t *testing.T) {

View File

@ -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
}

View File

@ -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")
)

View File

@ -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
}