0g-chain/x/community/keeper/staking.go
Robert Pirtle 0efe7f2281
feat(community): add AnnualizedRewards grpc query (#1751)
* add annualized_reward query proto

* use sdkmath.LegacyDec to match RPS param...

* add AnnualizedRewards grpc query

* add changelog entry

* simplify calculation & expand test cases
2023-10-24 12:24:21 -07:00

99 lines
3.9 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)
}
// emit event with amount transferred
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeStakingRewardsPaid,
sdk.NewAttribute(types.AttributeKeyStakingRewardAmount, transferAmount.String()),
),
)
}
// 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)
}
// calculateStakingRewards takes 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 multiplication 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
}