From 4c1524d7bc52f633f04e6ada198db909093eadc2 Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Mon, 19 Dec 2022 13:50:11 -0800 Subject: [PATCH] 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 --- x/community/keeper/keeper_test.go | 3 + x/earn/keeper/proposal_handler.go | 37 +------ x/earn/keeper/proposal_handler_test.go | 1 - x/kavamint/abci.go | 64 +----------- x/kavamint/abci_test.go | 24 ++++- x/kavamint/keeper/grpc_query.go | 4 +- x/kavamint/keeper/inflation.go | 130 +++++++++++++++++++++---- x/kavamint/keeper/inflation_test.go | 37 +++++-- x/kavamint/keeper/keeper.go | 71 +++++--------- x/kavamint/keeper/keeper_test.go | 18 ++++ x/kavamint/spec/06_begin_block.md | 40 ++------ x/kavamint/testutil/suite.go | 2 +- 12 files changed, 222 insertions(+), 209 deletions(-) diff --git a/x/community/keeper/keeper_test.go b/x/community/keeper/keeper_test.go index b998e021..9a782234 100644 --- a/x/community/keeper/keeper_test.go +++ b/x/community/keeper/keeper_test.go @@ -40,6 +40,7 @@ func (suite *KeeperTestSuite) TestCommunityPool() { // check that community pool received balance suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, funds) + suite.Equal(funds, suite.Keeper.GetModuleAccountBalance(suite.Ctx)) // check that sender had balance deducted suite.App.CheckBalance(suite.T(), suite.Ctx, sender, sdk.NewCoins()) }) @@ -51,12 +52,14 @@ func (suite *KeeperTestSuite) TestCommunityPool() { // community pool has funds deducted suite.App.CheckBalance(suite.T(), suite.Ctx, maccAddr, sdk.NewCoins()) + suite.Equal(sdk.NewCoins(), suite.Keeper.GetModuleAccountBalance(suite.Ctx)) // receiver receives the funds suite.App.CheckBalance(suite.T(), suite.Ctx, sender, funds) }) // can't send more than we have! suite.Run("DistributeFromCommunityPool - insufficient funds", func() { + suite.Equal(sdk.NewCoins(), suite.Keeper.GetModuleAccountBalance(suite.Ctx)) err := suite.Keeper.DistributeFromCommunityPool(suite.Ctx, sender, funds) suite.Require().ErrorContains(err, "insufficient funds") }) diff --git a/x/earn/keeper/proposal_handler.go b/x/earn/keeper/proposal_handler.go index f003b435..50265ed7 100644 --- a/x/earn/keeper/proposal_handler.go +++ b/x/earn/keeper/proposal_handler.go @@ -4,46 +4,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/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 func HandleCommunityPoolDepositProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolDepositProposal) error { - // move funds from community pool to the funding account - 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 - + return k.DepositFromModuleAccount(ctx, k.communityPoolMaccName, p.Amount, types.STRATEGY_TYPE_SAVINGS) } // HandleCommunityPoolWithdrawProposal is a handler for executing a passed community pool withdraw proposal. func HandleCommunityPoolWithdrawProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolWithdrawProposal) error { - // Withdraw to fund module account - withdrawAmount, err := k.WithdrawFromModuleAccount(ctx, kavadisttypes.FundModuleAccount, p.Amount, types.STRATEGY_TYPE_SAVINGS) - if err != nil { - 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 + _, err := k.WithdrawFromModuleAccount(ctx, k.communityPoolMaccName, p.Amount, types.STRATEGY_TYPE_SAVINGS) + return err } diff --git a/x/earn/keeper/proposal_handler_test.go b/x/earn/keeper/proposal_handler_test.go index ce82969c..cab6adb0 100644 --- a/x/earn/keeper/proposal_handler_test.go +++ b/x/earn/keeper/proposal_handler_test.go @@ -51,7 +51,6 @@ func (suite *proposalTestSuite) TestCommunityWithdrawProposal() { depositAmount := sdk.NewCoin("ukava", sdk.NewInt(10000000)) 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) deposit := types.NewCommunityPoolDepositProposal("test title", "desc", depositAmount) diff --git a/x/kavamint/abci.go b/x/kavamint/abci.go index 27b09592..5e25fa17 100644 --- a/x/kavamint/abci.go +++ b/x/kavamint/abci.go @@ -1,75 +1,13 @@ package kavamint import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" "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. func BeginBlocker(ctx sdk.Context, k keeper.KeeperI) { - 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) - - // ------------- Staking Rewards ------------- - stakingRewardCoins, err := k.AccumulateInflation( - ctx, params.StakingRewardsApy, totalBonded, secondsPassed, - ) - if err != nil { + if err := k.AccumulateAndMintInflation(ctx); err != nil { 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()), - ), - ) } diff --git a/x/kavamint/abci_test.go b/x/kavamint/abci_test.go index 6dd4cfa2..3911670b 100644 --- a/x/kavamint/abci_test.go +++ b/x/kavamint/abci_test.go @@ -103,6 +103,15 @@ func (suite *abciTestSuite) Test_BeginBlocker_MintsExpectedTokens() { expCommunityPoolBalance: 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", blockTime: 6, @@ -121,8 +130,8 @@ func (suite *abciTestSuite) Test_BeginBlocker_MintsExpectedTokens() { expFeeCollectorBalance: sdk.NewInt(121), }, { - name: "handles extra long block time", - blockTime: 60, // like if we're upgrading the network & it takes an hour to get back up + name: "handles long block time", + blockTime: 60, // a minute communityPoolInflation: sdk.NewDecWithPrec(50, 2), stakingRewardsApy: sdk.NewDecWithPrec(20, 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 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 { diff --git a/x/kavamint/keeper/grpc_query.go b/x/kavamint/keeper/grpc_query.go index 84df3e50..75230688 100644 --- a/x/kavamint/keeper/grpc_query.go +++ b/x/kavamint/keeper/grpc_query.go @@ -66,8 +66,8 @@ func (mq MintQueryServer) Inflation( ctx := sdk.UnwrapSDKContext(c) stakingApy := mq.keeper.GetParams(ctx).StakingRewardsApy - totalBonded := mq.keeper.TotalBondedTokens(ctx) - totalSupply := mq.keeper.TotalSupply(ctx) + totalBonded := mq.keeper.totalBondedTokens(ctx) + totalSupply := mq.keeper.totalSupply(ctx) // inflation = staking_apy * total_bonded / total_supply inflation := stakingApy.MulInt(totalBonded).QuoInt(totalSupply) diff --git a/x/kavamint/keeper/inflation.go b/x/kavamint/keeper/inflation.go index d0c260ed..6540c098 100644 --- a/x/kavamint/keeper/inflation.go +++ b/x/kavamint/keeper/inflation.go @@ -1,7 +1,11 @@ package keeper import ( + "fmt" + 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 @@ -9,32 +13,46 @@ const ( SecondsPerYear = uint64(31536000) ) -// AccumulateInflation calculates the number of coins that should be minted to match a yearly `rate` -// for interest compounded each second of the year over `secondsSinceLastMint` seconds. -// `basis` is the base amount of coins that is inflated. -func (k Keeper) AccumulateInflation( - ctx sdk.Context, - rate sdk.Dec, - basis sdk.Int, - secondsSinceLastMint float64, -) (sdk.Coins, error) { - bondDenom := k.BondDenom(ctx) +// Minter wraps the logic of a single source of inflation. It calculates the amount of coins to be +// minted to match a yearly `rate` of inflating the `basis`. +type Minter struct { + name string // name of the minting. used as event attribute to report how much is minted + rate sdk.Dec // the yearly apy of the mint. ex. 20% => 0.2 + basis sdk.Int // the base amount of coins that is inflated + destMaccName string // the destination module account name to mint coins to + mintDenom string // denom of coin this minter is responsible for minting +} - // 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 +// 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, } +} - amount := inflationRate.MulInt(basis).TruncateInt() - - 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 // over a period of secondsPassed seconds. -func CalculateInflationRate(apy sdk.Dec, secondsPassed uint64) (sdk.Dec, error) { - perSecondInterestRate, err := apyToSpy(apy.Add(sdk.OneDec())) +func (m Minter) CalculateInflationRate(secondsPassed uint64) (sdk.Dec, error) { + perSecondInterestRate, err := apyToSpy(m.rate.Add(sdk.OneDec())) if err != nil { return sdk.ZeroDec(), err } @@ -42,6 +60,80 @@ func CalculateInflationRate(apy sdk.Dec, secondsPassed uint64) (sdk.Dec, error) 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. // SPY = Per second compounded interest rate is how cosmos mathematically represents APY. func apyToSpy(apy sdk.Dec) (sdk.Dec, error) { diff --git a/x/kavamint/keeper/inflation_test.go b/x/kavamint/keeper/inflation_test.go index 21029460..d19ef867 100644 --- a/x/kavamint/keeper/inflation_test.go +++ b/x/kavamint/keeper/inflation_test.go @@ -116,7 +116,8 @@ func (suite *InflationTestSuite) TestCalculateInflationFactor() { for _, tc := range testCases { 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) marginOfError := sdk.ZeroDec() @@ -127,11 +128,35 @@ func (suite *InflationTestSuite) TestCalculateInflationFactor() { }) } - // TODO: nick bring back - //suite.Run("errors when rate is too high", func() { - // _, err := keeper.CalculateInflationRate(types.MaxMintingRate.Add(sdk.OneDec()), 100) - // suite.Error(err) - //}) + suite.Run("errors with out-of-bounds when rate is too high", func() { + // Dec.ApproxRoot will error w/ out-of-bounds when rate is >176.5 + oob := sdk.NewDec(177) + 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) { diff --git a/x/kavamint/keeper/keeper.go b/x/kavamint/keeper/keeper.go index c0d6ab56..086ab087 100644 --- a/x/kavamint/keeper/keeper.go +++ b/x/kavamint/keeper/keeper.go @@ -12,28 +12,9 @@ import ( "github.com/kava-labs/kava/x/kavamint/types" ) +// KeeperI is the required keeper interface for x/kavamint's begin blocker type KeeperI interface { - GetParams(ctx sdk.Context) (params types.Params) - 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) + AccumulateAndMintInflation(ctx sdk.Context) error } // Keeper of the kavamint store @@ -93,44 +74,40 @@ func (k Keeper) GetStakingApy(ctx sdk.Context) sdk.Dec { return params.StakingRewardsApy } -// BondDenom implements an alias call to the underlying staking keeper's BondDenom. -func (k Keeper) BondDenom(ctx sdk.Context) string { +// bondDenom implements an alias call to the underlying staking keeper's BondDenom. +func (k Keeper) bondDenom(ctx sdk.Context) string { 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. -func (k Keeper) TotalBondedTokens(ctx sdk.Context) sdk.Int { +func (k Keeper) totalBondedTokens(ctx sdk.Context) sdk.Int { return k.stakingKeeper.TotalBondedTokens(ctx) } -// MintCoins implements an alias call to the underlying supply keeper's -// MintCoins to be used in BeginBlocker. -func (k Keeper) MintCoins(ctx sdk.Context, newCoins sdk.Coins) error { - if newCoins.Empty() { +// mintCoinsToModule mints teh desired coins to the x/kavamint module account and then +// transfers them to the designated module account. +// if `newCoins` is empty or zero, this method is a noop. +func (k Keeper) mintCoinsToModule(ctx sdk.Context, newCoins sdk.Coins, destMaccName string) error { + if newCoins.IsZero() { // skip as no coins need to be minted 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 + } + + // transfer them to the desired destination module account + return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, destMaccName, newCoins) } -// AddCollectedFees implements an alias call to the underlying supply keeper's -// AddCollectedFees to be used in BeginBlocker. -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 -// 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 +// totalSupply implements an alias call to the underlying supply keeper's // GetSupply for the mint denom to be used in calculating cumulative inflation. -func (k Keeper) TotalSupply(ctx sdk.Context) sdk.Int { - return k.bankKeeper.GetSupply(ctx, k.BondDenom(ctx)).Amount +func (k Keeper) totalSupply(ctx sdk.Context) sdk.Int { + return k.bankKeeper.GetSupply(ctx, k.bondDenom(ctx)).Amount } 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) // staking rewards contribution is the apy * bonded_ratio - bondedSupply := k.TotalBondedTokens(ctx) - totalSupply := k.TotalSupply(ctx) + bondedSupply := k.totalBondedTokens(ctx) + totalSupply := k.totalSupply(ctx) bondedRatio := sdk.NewDecFromInt(bondedSupply).QuoInt(totalSupply) inflationFromStakingRewards := params.StakingRewardsApy.Mul(bondedRatio) diff --git a/x/kavamint/keeper/keeper_test.go b/x/kavamint/keeper/keeper_test.go index c3474b86..d4780395 100644 --- a/x/kavamint/keeper/keeper_test.go +++ b/x/kavamint/keeper/keeper_test.go @@ -51,3 +51,21 @@ func (suite keeperTestSuite) TestPreviousBlockTime_Persistance() { keeper.SetPreviousBlockTime(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)) + } +} diff --git a/x/kavamint/spec/06_begin_block.md b/x/kavamint/spec/06_begin_block.md index 6b104fa1..0542c795 100644 --- a/x/kavamint/spec/06_begin_block.md +++ b/x/kavamint/spec/06_begin_block.md @@ -9,41 +9,13 @@ At the start of each block, new KAVA tokens are minted and distributed ```go // BeginBlocker mints & distributes new tokens for the previous block. func BeginBlocker(ctx sdk.Context, k Keeper) { - params := k.GetParams(ctx) - // fetch the last block time from state - 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()) + if err := k.AccumulateAndMintInflation(ctx); err != nil { + panic(err) + } } ``` -`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 -more details. +more details on calculations & the defined sources of inflation. diff --git a/x/kavamint/testutil/suite.go b/x/kavamint/testutil/suite.go index 87d6f8a6..66c04ea7 100644 --- a/x/kavamint/testutil/suite.go +++ b/x/kavamint/testutil/suite.go @@ -31,7 +31,7 @@ func (suite *KavamintTestSuite) SetupTest() { suite.Keeper = suite.App.GetKavamintKeeper() 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