Refactor community & inflation enhancements (#1428)

* refactor begin blocker to single func

* remove unused inflation keeper methods

* refactor to private keeper methods

* add testcase for failed mint due to invalid param

* add testcase for GetStakingApy()

* check for zero instead of empty

* actually test super long block time

* skip fund account for earn community proposals

* test x/community keeper GetModuleAccountBalance

* update x/kavamint begin block spec
This commit is contained in:
Robert Pirtle 2022-12-19 13:50:11 -08:00 committed by GitHub
parent dd856bb288
commit 4c1524d7bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 209 deletions

View File

@ -40,6 +40,7 @@ func (suite *KeeperTestSuite) TestCommunityPool() {
// check that community pool received balance // check that community pool received balance
suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, funds) suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, funds)
suite.Equal(funds, suite.Keeper.GetModuleAccountBalance(suite.Ctx))
// check that sender had balance deducted // check that sender had balance deducted
suite.App.CheckBalance(suite.T(), suite.Ctx, sender, sdk.NewCoins()) suite.App.CheckBalance(suite.T(), suite.Ctx, sender, sdk.NewCoins())
}) })
@ -51,12 +52,14 @@ func (suite *KeeperTestSuite) TestCommunityPool() {
// community pool has funds deducted // community pool has funds deducted
suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, sdk.NewCoins()) suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, sdk.NewCoins())
suite.Equal(sdk.NewCoins(), suite.Keeper.GetModuleAccountBalance(suite.Ctx))
// receiver receives the funds // receiver receives the funds
suite.App.CheckBalance(suite.T(), suite.Ctx, sender, funds) suite.App.CheckBalance(suite.T(), suite.Ctx, sender, funds)
}) })
// can't send more than we have! // can't send more than we have!
suite.Run("DistributeFromCommunityPool - insufficient funds", func() { suite.Run("DistributeFromCommunityPool - insufficient funds", func() {
suite.Equal(sdk.NewCoins(), suite.Keeper.GetModuleAccountBalance(suite.Ctx))
err := suite.Keeper.DistributeFromCommunityPool(suite.Ctx, sender, funds) err := suite.Keeper.DistributeFromCommunityPool(suite.Ctx, sender, funds)
suite.Require().ErrorContains(err, "insufficient funds") suite.Require().ErrorContains(err, "insufficient funds")
}) })

View File

@ -4,46 +4,15 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/earn/types" "github.com/kava-labs/kava/x/earn/types"
kavadisttypes "github.com/kava-labs/kava/x/kavadist/types"
) )
// HandleCommunityPoolDepositProposal is a handler for executing a passed community pool deposit proposal // HandleCommunityPoolDepositProposal is a handler for executing a passed community pool deposit proposal
func HandleCommunityPoolDepositProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolDepositProposal) error { func HandleCommunityPoolDepositProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolDepositProposal) error {
// move funds from community pool to the funding account return k.DepositFromModuleAccount(ctx, k.communityPoolMaccName, p.Amount, types.STRATEGY_TYPE_SAVINGS)
if err := k.bankKeeper.SendCoinsFromModuleToModule(
ctx,
k.communityPoolMaccName,
kavadisttypes.FundModuleAccount,
sdk.NewCoins(p.Amount),
); err != nil {
return err
}
err := k.DepositFromModuleAccount(ctx, kavadisttypes.FundModuleAccount, p.Amount, types.STRATEGY_TYPE_SAVINGS)
if err != nil {
return err
}
return nil
} }
// HandleCommunityPoolWithdrawProposal is a handler for executing a passed community pool withdraw proposal. // HandleCommunityPoolWithdrawProposal is a handler for executing a passed community pool withdraw proposal.
func HandleCommunityPoolWithdrawProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolWithdrawProposal) error { func HandleCommunityPoolWithdrawProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolWithdrawProposal) error {
// Withdraw to fund module account _, err := k.WithdrawFromModuleAccount(ctx, k.communityPoolMaccName, p.Amount, types.STRATEGY_TYPE_SAVINGS)
withdrawAmount, err := k.WithdrawFromModuleAccount(ctx, kavadisttypes.FundModuleAccount, p.Amount, types.STRATEGY_TYPE_SAVINGS)
if err != nil {
return err return err
} }
// Move funds to the community pool manually
if err := k.bankKeeper.SendCoinsFromModuleToModule(
ctx,
kavadisttypes.FundModuleAccount,
k.communityPoolMaccName,
sdk.NewCoins(withdrawAmount),
); err != nil {
return err
}
return nil
}

View File

@ -51,7 +51,6 @@ func (suite *proposalTestSuite) TestCommunityWithdrawProposal() {
depositAmount := sdk.NewCoin("ukava", sdk.NewInt(10000000)) depositAmount := sdk.NewCoin("ukava", sdk.NewInt(10000000))
suite.Require().NoError(suite.App.FundModuleAccount(ctx, macc.GetName(), fundAmount)) suite.Require().NoError(suite.App.FundModuleAccount(ctx, macc.GetName(), fundAmount))
// TODO update to STRATEGY_TYPE_SAVINGS once implemented
suite.CreateVault("ukava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil) suite.CreateVault("ukava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil)
deposit := types.NewCommunityPoolDepositProposal("test title", deposit := types.NewCommunityPoolDepositProposal("test title",
"desc", depositAmount) "desc", depositAmount)

View File

@ -1,75 +1,13 @@
package kavamint package kavamint
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/kavamint/keeper" "github.com/kava-labs/kava/x/kavamint/keeper"
"github.com/kava-labs/kava/x/kavamint/types"
) )
// BeginBlocker mints & distributes new tokens for the previous block. // BeginBlocker mints & distributes new tokens for the previous block.
func BeginBlocker(ctx sdk.Context, k keeper.KeeperI) { func BeginBlocker(ctx sdk.Context, k keeper.KeeperI) {
params := k.GetParams(ctx) if err := k.AccumulateAndMintInflation(ctx); err != nil {
// determine seconds since last mint
previousBlockTime := k.GetPreviousBlockTime(ctx)
if previousBlockTime.IsZero() {
previousBlockTime = ctx.BlockTime()
}
secondsPassed := ctx.BlockTime().Sub(previousBlockTime).Seconds()
// calculate totals before any minting is done to prevent new mints affecting the values
totalSupply := k.TotalSupply(ctx)
totalBonded := k.TotalBondedTokens(ctx)
// ------------- Staking Rewards -------------
stakingRewardCoins, err := k.AccumulateInflation(
ctx, params.StakingRewardsApy, totalBonded, secondsPassed,
)
if err != nil {
panic(err) panic(err)
} }
// mint staking rewards
if err := k.MintCoins(ctx, stakingRewardCoins); err != nil {
panic(err)
}
// send staking rewards to auth fee collector for distribution to validators
if err := k.AddCollectedFees(ctx, stakingRewardCoins); err != nil {
panic(err)
}
// ------------- Community Pool -------------
communityPoolInflation, err := k.AccumulateInflation(
ctx, params.CommunityPoolInflation, totalSupply, secondsPassed,
)
if err != nil {
panic(err)
}
// mint community pool inflation
if err := k.MintCoins(ctx, communityPoolInflation); err != nil {
panic(err)
}
// send inflation coins to the community pool (x/community module account)
if err := k.FundCommunityPool(ctx, communityPoolInflation); err != nil {
panic(err)
}
// ------------- Bookkeeping -------------
// bookkeep the previous block time
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeMint,
sdk.NewAttribute(types.AttributeKeyTotalSupply, totalSupply.String()),
sdk.NewAttribute(types.AttributeKeyTotalBonded, totalBonded.String()),
sdk.NewAttribute(types.AttributeSecondsPassed, fmt.Sprintf("%f", secondsPassed)),
sdk.NewAttribute(types.AttributeKeyCommunityPoolMint, communityPoolInflation.String()),
sdk.NewAttribute(types.AttributeKeyStakingRewardMint, stakingRewardCoins.String()),
),
)
} }

View File

@ -103,6 +103,15 @@ func (suite *abciTestSuite) Test_BeginBlocker_MintsExpectedTokens() {
expCommunityPoolBalance: sdk.ZeroInt(), expCommunityPoolBalance: sdk.ZeroInt(),
expFeeCollectorBalance: sdk.ZeroInt(), expFeeCollectorBalance: sdk.ZeroInt(),
}, },
{
name: "mints no tokens if zero seconds passed",
blockTime: 0,
communityPoolInflation: sdk.NewDecWithPrec(50, 2),
stakingRewardsApy: sdk.NewDecWithPrec(20, 2),
bondedRatio: sdk.NewDecWithPrec(35, 2),
expCommunityPoolBalance: sdk.ZeroInt(),
expFeeCollectorBalance: sdk.ZeroInt(),
},
{ {
name: "mints community pool inflation and staking rewards", name: "mints community pool inflation and staking rewards",
blockTime: 6, blockTime: 6,
@ -121,8 +130,8 @@ func (suite *abciTestSuite) Test_BeginBlocker_MintsExpectedTokens() {
expFeeCollectorBalance: sdk.NewInt(121), expFeeCollectorBalance: sdk.NewInt(121),
}, },
{ {
name: "handles extra long block time", name: "handles long block time",
blockTime: 60, // like if we're upgrading the network & it takes an hour to get back up blockTime: 60, // a minute
communityPoolInflation: sdk.NewDecWithPrec(50, 2), communityPoolInflation: sdk.NewDecWithPrec(50, 2),
stakingRewardsApy: sdk.NewDecWithPrec(20, 2), stakingRewardsApy: sdk.NewDecWithPrec(20, 2),
bondedRatio: sdk.NewDecWithPrec(35, 2), bondedRatio: sdk.NewDecWithPrec(35, 2),
@ -131,6 +140,17 @@ func (suite *abciTestSuite) Test_BeginBlocker_MintsExpectedTokens() {
// https://www.wolframalpha.com/input?i2d=true&i=%5C%2840%29Power%5B%5C%2840%29Surd%5B1.20%2C31536000%5D%5C%2841%29%2C60%5D-1%5C%2841%29*1e10*.35 // https://www.wolframalpha.com/input?i2d=true&i=%5C%2840%29Power%5B%5C%2840%29Surd%5B1.20%2C31536000%5D%5C%2841%29%2C60%5D-1%5C%2841%29*1e10*.35
expFeeCollectorBalance: sdk.NewInt(1214), expFeeCollectorBalance: sdk.NewInt(1214),
}, },
{
name: "handles extra long block time",
blockTime: 3 * 3600, // three hours
communityPoolInflation: sdk.NewDecWithPrec(50, 2),
stakingRewardsApy: sdk.NewDecWithPrec(20, 2),
bondedRatio: sdk.NewDecWithPrec(35, 2),
// https://www.wolframalpha.com/input?i2d=true&i=%5C%2840%29Power%5B%5C%2840%29Surd%5B1.5%2C31536000%5D%5C%2841%29%2C3*3600%5D-1%5C%2841%29*1e10
expCommunityPoolBalance: sdk.NewInt(1388675),
// https://www.wolframalpha.com/input?i2d=true&i=%5C%2840%29Power%5B%5C%2840%29Surd%5B1.20%2C31536000%5D%5C%2841%29%2C3*3600%5D-1%5C%2841%29*1e10*.35
expFeeCollectorBalance: sdk.NewInt(218542),
},
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -66,8 +66,8 @@ func (mq MintQueryServer) Inflation(
ctx := sdk.UnwrapSDKContext(c) ctx := sdk.UnwrapSDKContext(c)
stakingApy := mq.keeper.GetParams(ctx).StakingRewardsApy stakingApy := mq.keeper.GetParams(ctx).StakingRewardsApy
totalBonded := mq.keeper.TotalBondedTokens(ctx) totalBonded := mq.keeper.totalBondedTokens(ctx)
totalSupply := mq.keeper.TotalSupply(ctx) totalSupply := mq.keeper.totalSupply(ctx)
// inflation = staking_apy * total_bonded / total_supply // inflation = staking_apy * total_bonded / total_supply
inflation := stakingApy.MulInt(totalBonded).QuoInt(totalSupply) inflation := stakingApy.MulInt(totalBonded).QuoInt(totalSupply)

View File

@ -1,7 +1,11 @@
package keeper package keeper
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/kavamint/types"
) )
// this is the same value used in the x/hard // this is the same value used in the x/hard
@ -9,32 +13,46 @@ const (
SecondsPerYear = uint64(31536000) SecondsPerYear = uint64(31536000)
) )
// AccumulateInflation calculates the number of coins that should be minted to match a yearly `rate` // Minter wraps the logic of a single source of inflation. It calculates the amount of coins to be
// for interest compounded each second of the year over `secondsSinceLastMint` seconds. // minted to match a yearly `rate` of inflating the `basis`.
// `basis` is the base amount of coins that is inflated. type Minter struct {
func (k Keeper) AccumulateInflation( name string // name of the minting. used as event attribute to report how much is minted
ctx sdk.Context, rate sdk.Dec // the yearly apy of the mint. ex. 20% => 0.2
rate sdk.Dec, basis sdk.Int // the base amount of coins that is inflated
basis sdk.Int, destMaccName string // the destination module account name to mint coins to
secondsSinceLastMint float64, mintDenom string // denom of coin this minter is responsible for minting
) (sdk.Coins, error) {
bondDenom := k.BondDenom(ctx)
// calculate the rate factor based on apy & seconds passed since last block
inflationRate, err := CalculateInflationRate(rate, uint64(secondsSinceLastMint))
if err != nil {
return sdk.NewCoins(), err
} }
amount := inflationRate.MulInt(basis).TruncateInt() // NewMinter returns creates a new source of minting inflation
func NewMinter(
name string, rate sdk.Dec, basis sdk.Int, mintDenom string, destMaccName string,
) Minter {
return Minter{
name: name,
rate: rate,
basis: basis,
destMaccName: destMaccName,
mintDenom: mintDenom,
}
}
return sdk.NewCoins(sdk.NewCoin(bondDenom, amount)), nil // AccumulateInflation calculates the number of coins that should be minted to match a yearly `rate`
// for interest compounded each second of the year over `secondsPassed` seconds.
// `basis` is the base amount of coins that is inflated.
func (m Minter) AccumulateInflation(secondsPassed uint64) (sdk.Coin, error) {
// calculate the rate factor based on apy & seconds passed since last block
inflationRate, err := m.CalculateInflationRate(secondsPassed)
if err != nil {
return sdk.Coin{}, err
}
amount := inflationRate.MulInt(m.basis).TruncateInt()
return sdk.NewCoin(m.mintDenom, amount), nil
} }
// CalculateInflationRate converts an APY into the factor corresponding with that APY's accumulation // CalculateInflationRate converts an APY into the factor corresponding with that APY's accumulation
// over a period of secondsPassed seconds. // over a period of secondsPassed seconds.
func CalculateInflationRate(apy sdk.Dec, secondsPassed uint64) (sdk.Dec, error) { func (m Minter) CalculateInflationRate(secondsPassed uint64) (sdk.Dec, error) {
perSecondInterestRate, err := apyToSpy(apy.Add(sdk.OneDec())) perSecondInterestRate, err := apyToSpy(m.rate.Add(sdk.OneDec()))
if err != nil { if err != nil {
return sdk.ZeroDec(), err return sdk.ZeroDec(), err
} }
@ -42,6 +60,80 @@ func CalculateInflationRate(apy sdk.Dec, secondsPassed uint64) (sdk.Dec, error)
return rate.Sub(sdk.OneDec()), nil return rate.Sub(sdk.OneDec()), nil
} }
// AccumulateAndMintInflation defines the sources of inflation, determines the seconds passed since
// the last mint, and then mints each source of inflation to the defined destination.
func (k Keeper) AccumulateAndMintInflation(ctx sdk.Context) error {
params := k.GetParams(ctx)
// determine seconds since last mint
previousBlockTime := k.GetPreviousBlockTime(ctx)
if previousBlockTime.IsZero() {
previousBlockTime = ctx.BlockTime()
}
secondsPassed := ctx.BlockTime().Sub(previousBlockTime).Seconds()
// calculate totals before any minting is done to prevent new mints affecting the values
totalSupply := k.totalSupply(ctx)
totalBonded := k.totalBondedTokens(ctx)
bondDenom := k.bondDenom(ctx)
// define minters for each contributing piece of inflation
minters := []Minter{
// staking rewards
NewMinter(
types.AttributeKeyStakingRewardMint,
params.StakingRewardsApy,
totalBonded,
bondDenom,
k.stakingRewardsFeeCollectorName,
),
// community pool inflation
NewMinter(
types.AttributeKeyCommunityPoolMint,
params.CommunityPoolInflation,
totalSupply,
bondDenom,
k.communityPoolModuleAccountName,
),
}
mintEventAttrs := make([]sdk.Attribute, 0, len(minters))
for _, minter := range minters {
// calculate amount for time passed
amount, err := minter.AccumulateInflation(uint64(secondsPassed))
if err != nil {
return err
}
// mint & transfer coins
inflation := sdk.NewCoins(amount)
if err := k.mintCoinsToModule(ctx, inflation, minter.destMaccName); err != nil {
return err
}
// add attribute to event
mintEventAttrs = append(mintEventAttrs, sdk.NewAttribute(
minter.name,
inflation.String(),
))
}
// ------------- Bookkeeping -------------
// bookkeep the previous block time
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeMint,
sdk.NewAttribute(types.AttributeKeyTotalSupply, totalSupply.String()),
sdk.NewAttribute(types.AttributeKeyTotalBonded, totalBonded.String()),
sdk.NewAttribute(types.AttributeSecondsPassed, fmt.Sprintf("%f", secondsPassed)),
).AppendAttributes(mintEventAttrs...),
)
return nil
}
// apyToSpy converts the input annual interest rate. For example, 10% apy would be passed as 1.10. // apyToSpy converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY. // SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
func apyToSpy(apy sdk.Dec) (sdk.Dec, error) { func apyToSpy(apy sdk.Dec) (sdk.Dec, error) {

View File

@ -116,7 +116,8 @@ func (suite *InflationTestSuite) TestCalculateInflationFactor() {
for _, tc := range testCases { for _, tc := range testCases {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {
actualRate, err := keeper.CalculateInflationRate(tc.apy, tc.secondsPassed) minter := keeper.NewMinter(tc.name, tc.apy, sdk.ZeroInt(), "ukava", "ignored")
actualRate, err := minter.CalculateInflationRate(tc.secondsPassed)
suite.Require().NoError(err) suite.Require().NoError(err)
marginOfError := sdk.ZeroDec() marginOfError := sdk.ZeroDec()
@ -127,11 +128,35 @@ func (suite *InflationTestSuite) TestCalculateInflationFactor() {
}) })
} }
// TODO: nick bring back suite.Run("errors with out-of-bounds when rate is too high", func() {
//suite.Run("errors when rate is too high", func() { // Dec.ApproxRoot will error w/ out-of-bounds when rate is >176.5
// _, err := keeper.CalculateInflationRate(types.MaxMintingRate.Add(sdk.OneDec()), 100) oob := sdk.NewDec(177)
// suite.Error(err) minter := keeper.NewMinter(
//}) "out-of-bounds-minter",
oob,
sdk.OneInt(),
"uakva",
"ignored",
)
_, err := minter.CalculateInflationRate(100)
suite.Error(err)
// ensure max mint rate is less than out-of-bounds value
suite.True(types.MaxMintingRate.LT(oob))
})
}
func (suite *InflationTestSuite) Test_AccumulateInflation_FailsWithInvalidRate() {
oob := sdk.NewDec(177)
minter := keeper.NewMinter(
"out-of-bounds-minter",
oob,
sdk.OneInt(),
"uakva",
"ignored",
)
_, err := minter.AccumulateInflation(1)
suite.ErrorContains(err, "out of bounds")
} }
func (suite *InflationTestSuite) requireWithinError(expected, actual, margin sdk.Dec) { func (suite *InflationTestSuite) requireWithinError(expected, actual, margin sdk.Dec) {

View File

@ -12,28 +12,9 @@ import (
"github.com/kava-labs/kava/x/kavamint/types" "github.com/kava-labs/kava/x/kavamint/types"
) )
// KeeperI is the required keeper interface for x/kavamint's begin blocker
type KeeperI interface { type KeeperI interface {
GetParams(ctx sdk.Context) (params types.Params) AccumulateAndMintInflation(ctx sdk.Context) error
SetParams(ctx sdk.Context, params types.Params)
BondDenom(ctx sdk.Context) string
TotalSupply(ctx sdk.Context) sdk.Int
TotalBondedTokens(ctx sdk.Context) sdk.Int
MintCoins(ctx sdk.Context, newCoins sdk.Coins) error
AddCollectedFees(ctx sdk.Context, fees sdk.Coins) error
FundCommunityPool(ctx sdk.Context, funds sdk.Coins) error
GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time)
SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time)
CumulativeInflation(ctx sdk.Context) sdk.Dec
AccumulateInflation(
ctx sdk.Context,
rate sdk.Dec,
basis sdk.Int,
secondsSinceLastMint float64,
) (sdk.Coins, error)
} }
// Keeper of the kavamint store // Keeper of the kavamint store
@ -93,44 +74,40 @@ func (k Keeper) GetStakingApy(ctx sdk.Context) sdk.Dec {
return params.StakingRewardsApy return params.StakingRewardsApy
} }
// BondDenom implements an alias call to the underlying staking keeper's BondDenom. // bondDenom implements an alias call to the underlying staking keeper's BondDenom.
func (k Keeper) BondDenom(ctx sdk.Context) string { func (k Keeper) bondDenom(ctx sdk.Context) string {
return k.stakingKeeper.BondDenom(ctx) return k.stakingKeeper.BondDenom(ctx)
} }
// TotalBondedTokens implements an alias call to the underlying staking keeper's // totalBondedTokens implements an alias call to the underlying staking keeper's
// TotalBondedTokens to be used in BeginBlocker. // TotalBondedTokens to be used in BeginBlocker.
func (k Keeper) TotalBondedTokens(ctx sdk.Context) sdk.Int { func (k Keeper) totalBondedTokens(ctx sdk.Context) sdk.Int {
return k.stakingKeeper.TotalBondedTokens(ctx) return k.stakingKeeper.TotalBondedTokens(ctx)
} }
// MintCoins implements an alias call to the underlying supply keeper's // mintCoinsToModule mints teh desired coins to the x/kavamint module account and then
// MintCoins to be used in BeginBlocker. // transfers them to the designated module account.
func (k Keeper) MintCoins(ctx sdk.Context, newCoins sdk.Coins) error { // if `newCoins` is empty or zero, this method is a noop.
if newCoins.Empty() { func (k Keeper) mintCoinsToModule(ctx sdk.Context, newCoins sdk.Coins, destMaccName string) error {
if newCoins.IsZero() {
// skip as no coins need to be minted // skip as no coins need to be minted
return nil return nil
} }
return k.bankKeeper.MintCoins(ctx, types.ModuleName, newCoins) // mint the coins
err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, newCoins)
if err != nil {
return nil
} }
// AddCollectedFees implements an alias call to the underlying supply keeper's // transfer them to the desired destination module account
// AddCollectedFees to be used in BeginBlocker. return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, destMaccName, newCoins)
func (k Keeper) AddCollectedFees(ctx sdk.Context, fees sdk.Coins) error {
return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, k.stakingRewardsFeeCollectorName, fees)
} }
// FundCommunityPool implements an alias call to the underlying supply keeper's // totalSupply implements an alias call to the underlying supply keeper's
// FundCommunityPool to be used in BeginBlocker.
func (k Keeper) FundCommunityPool(ctx sdk.Context, funds sdk.Coins) error {
return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, k.communityPoolModuleAccountName, funds)
}
// TotalSupply implements an alias call to the underlying supply keeper's
// GetSupply for the mint denom to be used in calculating cumulative inflation. // GetSupply for the mint denom to be used in calculating cumulative inflation.
func (k Keeper) TotalSupply(ctx sdk.Context) sdk.Int { func (k Keeper) totalSupply(ctx sdk.Context) sdk.Int {
return k.bankKeeper.GetSupply(ctx, k.BondDenom(ctx)).Amount return k.bankKeeper.GetSupply(ctx, k.bondDenom(ctx)).Amount
} }
func (k Keeper) CumulativeInflation(ctx sdk.Context) sdk.Dec { func (k Keeper) CumulativeInflation(ctx sdk.Context) sdk.Dec {
@ -141,8 +118,8 @@ func (k Keeper) CumulativeInflation(ctx sdk.Context) sdk.Dec {
totalInflation = totalInflation.Add(params.CommunityPoolInflation) totalInflation = totalInflation.Add(params.CommunityPoolInflation)
// staking rewards contribution is the apy * bonded_ratio // staking rewards contribution is the apy * bonded_ratio
bondedSupply := k.TotalBondedTokens(ctx) bondedSupply := k.totalBondedTokens(ctx)
totalSupply := k.TotalSupply(ctx) totalSupply := k.totalSupply(ctx)
bondedRatio := sdk.NewDecFromInt(bondedSupply).QuoInt(totalSupply) bondedRatio := sdk.NewDecFromInt(bondedSupply).QuoInt(totalSupply)
inflationFromStakingRewards := params.StakingRewardsApy.Mul(bondedRatio) inflationFromStakingRewards := params.StakingRewardsApy.Mul(bondedRatio)

View File

@ -51,3 +51,21 @@ func (suite keeperTestSuite) TestPreviousBlockTime_Persistance() {
keeper.SetPreviousBlockTime(suite.Ctx, newTime) keeper.SetPreviousBlockTime(suite.Ctx, newTime)
suite.Equal(keeper.GetPreviousBlockTime(suite.Ctx), newTime) suite.Equal(keeper.GetPreviousBlockTime(suite.Ctx), newTime)
} }
func (suite keeperTestSuite) Test_GetStakingApy() {
suite.SetupTest()
testCases := []sdk.Dec{
sdk.ZeroDec(),
sdk.MustNewDecFromStr("0.000005"),
sdk.MustNewDecFromStr("0.15"),
sdk.OneDec(),
types.MaxMintingRate,
}
for _, tc := range testCases {
params := types.NewParams(sdk.ZeroDec(), tc)
suite.Keeper.SetParams(suite.Ctx, params)
suite.Equal(tc, suite.Keeper.GetStakingApy(suite.Ctx))
}
}

View File

@ -9,41 +9,13 @@ At the start of each block, new KAVA tokens are minted and distributed
```go ```go
// BeginBlocker mints & distributes new tokens for the previous block. // BeginBlocker mints & distributes new tokens for the previous block.
func BeginBlocker(ctx sdk.Context, k Keeper) { func BeginBlocker(ctx sdk.Context, k Keeper) {
params := k.GetParams(ctx) if err := k.AccumulateAndMintInflation(ctx); err != nil {
// fetch the last block time from state panic(err)
previousBlockTime := k.GetPreviousBlockTime(ctx) }
secondsPassed := ctx.BlockTime().Sub(previousBlockTime).Seconds()
// determine totals before any new mints
totalSupply := k.TotalSupply(ctx)
totalBonded := k.TotalBondedTokens(ctx)
// ------------- Staking Rewards -------------
// determine amount of the bond denom to mint for staking rewards
stakingRewardCoins, err := k.AccumulateInflation(
ctx, params.StakingRewardsApy, totalBonded, secondsPassed,
)
// mint the staking rewards
k.MintCoins(ctx, stakingRewardCoins)
// distribute them to the fee pool for distribution by x/distribution
k.AddCollectedFees(ctx, stakingRewardCoins)
// ------------- Community Pool -------------
// determine amount of the bond denom to mint for community pool inflation
communityPoolInflation, err := k.AccumulateInflation(
ctx, params.CommunityPoolInflation, totalSupply, secondsPassed,
)
// mint the community pool tokens
k.MintCoins(ctx, communityPoolCoins)
// send them to the community module account (the community pool)
k.AddCommunityPoolFunds(ctx, communityPoolCoins)
// ------------- Bookkeeping -------------
// set block time for next iteration's minting
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
} }
``` ```
`AccumulateInflation` determines the effective rate of the yearly interest rate assuming it is `AccumulateAndMintInflation` defines all sources of inflation from yearly APYs set via the parameters.
Those rates are converted to the effective rate of the yearly interest rate assuming it is
compounded once per second, for the number of seconds since the previous mint. See concepts for compounded once per second, for the number of seconds since the previous mint. See concepts for
more details. more details on calculations & the defined sources of inflation.

View File

@ -31,7 +31,7 @@ func (suite *KavamintTestSuite) SetupTest() {
suite.Keeper = suite.App.GetKavamintKeeper() suite.Keeper = suite.App.GetKavamintKeeper()
suite.StakingKeeper = suite.App.GetStakingKeeper() suite.StakingKeeper = suite.App.GetStakingKeeper()
suite.BondDenom = suite.Keeper.BondDenom(suite.Ctx) suite.BondDenom = suite.StakingKeeper.BondDenom(suite.Ctx)
} }
// SetBondedTokenRatio mints the total supply to an account and creates a validator with a self // SetBondedTokenRatio mints the total supply to an account and creates a validator with a self