0g-chain/x/community/keeper/staking.go
Nick DeLuca 102cc0fff3
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
2023-10-03 08:41:54 -07:00

92 lines
3.7 KiB
Go

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
}