diff --git a/PENDING.md b/PENDING.md index 47c6ab60e5ff..a20b9d6120cb 100644 --- a/PENDING.md +++ b/PENDING.md @@ -13,6 +13,7 @@ BREAKING CHANGES * [cli] [\#2874](https://github.com/cosmos/cosmos-sdk/pull/2874) `gaiacli tx sign` takes an optional `--output-document` flag to support output redirection. * Gaia + * [mint] [\#2825] minting now occurs every block, inflation parameter updates still hourly * SDK * [\#2752](https://github.com/cosmos/cosmos-sdk/pull/2752) Don't hardcode bondable denom. @@ -71,6 +72,7 @@ IMPROVEMENTS - #2821 Codespaces are now strings - #2779 Introduce `ValidateBasic` to the `Tx` interface and call it in the ante handler. + - #2825 More staking and distribution invariants * Tendermint - #2796 Update to go-amino 0.14.1 diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 70ae8e12af83..bd800b81d71b 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -59,7 +59,9 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { if numInitiallyBonded > numAccs { numInitiallyBonded = numAccs } - fmt.Printf("Selected randomly generated parameters for simulated genesis: {amount of steak per account: %v, initially bonded validators: %v}\n", amount, numInitiallyBonded) + fmt.Printf("Selected randomly generated parameters for simulated genesis:\n"+ + "\t{amount of steak per account: %v, initially bonded validators: %v}\n", + amount, numInitiallyBonded) // Randomly generate some genesis accounts for _, acc := range accs { @@ -86,7 +88,8 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { GovernancePenalty: sdk.NewDecWithPrec(1, 2), }, } - fmt.Printf("Selected randomly generated governance parameters: %+v\n", govGenesis) + fmt.Printf("Selected randomly generated governance parameters:\n\t%+v\n", govGenesis) + stakeGenesis := stake.GenesisState{ Pool: stake.InitialPool(), Params: stake.Params{ @@ -95,7 +98,8 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { BondDenom: stakeTypes.DefaultBondDenom, }, } - fmt.Printf("Selected randomly generated staking parameters: %+v\n", stakeGenesis) + fmt.Printf("Selected randomly generated staking parameters:\n\t%+v\n", stakeGenesis) + slashingGenesis := slashing.GenesisState{ Params: slashing.Params{ MaxEvidenceAge: stakeGenesis.Params.UnbondingTime, @@ -107,21 +111,21 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))), }, } - fmt.Printf("Selected randomly generated slashing parameters: %+v\n", slashingGenesis) + fmt.Printf("Selected randomly generated slashing parameters:\n\t%+v\n", slashingGenesis) + mintGenesis := mint.GenesisState{ - Minter: mint.Minter{ - InflationLastTime: time.Unix(0, 0), - Inflation: sdk.NewDecWithPrec(int64(r.Intn(99)), 2), - }, - Params: mint.Params{ - MintDenom: stakeTypes.DefaultBondDenom, - InflationRateChange: sdk.NewDecWithPrec(int64(r.Intn(99)), 2), - InflationMax: sdk.NewDecWithPrec(20, 2), - InflationMin: sdk.NewDecWithPrec(7, 2), - GoalBonded: sdk.NewDecWithPrec(67, 2), - }, + Minter: mint.InitialMinter( + sdk.NewDecWithPrec(int64(r.Intn(99)), 2)), + Params: mint.NewParams( + stakeTypes.DefaultBondDenom, + sdk.NewDecWithPrec(int64(r.Intn(99)), 2), + sdk.NewDecWithPrec(20, 2), + sdk.NewDecWithPrec(7, 2), + sdk.NewDecWithPrec(67, 2), + uint64(60*60*8766/5)), } - fmt.Printf("Selected randomly generated minting parameters: %v\n", mintGenesis) + fmt.Printf("Selected randomly generated minting parameters:\n\t%+v\n", mintGenesis) + var validators []stake.Validator var delegations []stake.Delegation diff --git a/docs/spec/mint/begin_block.md b/docs/spec/mint/begin_block.md index 7588db38b1b2..9207141472cb 100644 --- a/docs/spec/mint/begin_block.md +++ b/docs/spec/mint/begin_block.md @@ -1,18 +1,18 @@ # Begin-Block -## Inflation +Inflation occurs at the beginning of each block, however minting parameters +are only calculated once per hour. -Inflation occurs at the beginning of each block. +## NextInflationRate -### NextInflation +The target annual inflation rate is recalculated at the first block of each new +hour. The inflation is also subject to a rate change (positive or negative) +depending on the distance from the desired ratio (67%). The maximum rate change +possible is defined to be 13% per year, however the annual inflation is capped +as between 7% and 20%. -The target annual inflation rate is recalculated for each provisions cycle. The -inflation is also subject to a rate change (positive or negative) depending on -the distance from the desired ratio (67%). The maximum rate change possible is -defined to be 13% per year, however the annual inflation is capped as between -7% and 20%. - -NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { +``` +NextInflationRate(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange inflationRateChange = inflationRateChangePerYear/hrsPerYr @@ -26,3 +26,25 @@ NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { } return inflation +``` + +## NextAnnualProvisions + +Calculate the annual provisions based on current total supply and inflation +rate. This parameter is calculated once per block. + +``` +NextAnnualProvisions(params Params, totalSupply sdk.Dec) (provisions sdk.Dec) { + return Inflation * totalSupply +``` + +## BlockProvision + +Calculate the provisions generated for each block based on current +annual provisions + +``` +BlockProvision(params Params) sdk.Coin { + provisionAmt = AnnualProvisions/ params.BlocksPerYear + return sdk.NewCoin(params.MintDenom, provisionAmt.Truncate()) +``` diff --git a/docs/spec/mint/state.md b/docs/spec/mint/state.md index 98e8e63dd237..c3133296ea5c 100644 --- a/docs/spec/mint/state.md +++ b/docs/spec/mint/state.md @@ -8,8 +8,9 @@ The minter is a space for holding current inflation information. ```golang type Minter struct { - InflationLastTime time.Time // block time which the last inflation was processed - Inflation sdk.Dec // current annual inflation rate + LastUpdate time.Time // time which the last update was made to the minter + Inflation sdk.Dec // current annual inflation rate + AnnualProvisions sdk.Dec // current annual exptected provisions } ``` @@ -26,6 +27,7 @@ type Params struct { InflationMax sdk.Dec // maximum inflation rate InflationMin sdk.Dec // minimum inflation rate GoalBonded sdk.Dec // goal of percent bonded atoms + BlocksPerYear uint64 // expected blocks per year } ``` diff --git a/types/coin.go b/types/coin.go index 5f9020e84e23..9742545324be 100644 --- a/types/coin.go +++ b/types/coin.go @@ -28,7 +28,7 @@ type Coin struct { // the amount is negative. func NewCoin(denom string, amount Int) Coin { if amount.LT(ZeroInt()) { - panic("negative coin amount") + panic(fmt.Sprintf("negative coin amount: %v\n", amount)) } return Coin{ diff --git a/types/decimal.go b/types/decimal.go index 3c0e89f60ba5..0d843f6cb775 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -172,10 +172,21 @@ func NewDecFromStr(str string) (d Dec, err Error) { return Dec{combined}, nil } +// Decimal from string, panic on error +func MustNewDecFromStr(s string) Dec { + dec, err := NewDecFromStr(s) + if err != nil { + panic(err) + } + return dec +} + //______________________________________________________________________________________________ //nolint func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero +func (d Dec) IsNegative() bool { return (d.Int).Sign() == -1 } // is negative +func (d Dec) IsPositive() bool { return (d.Int).Sign() == 1 } // is positive func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal @@ -252,6 +263,14 @@ func (d Dec) IsInteger() bool { return new(big.Int).Rem(d.Int, precisionReuse).Sign() == 0 } +// format decimal state +func (d Dec) Format(s fmt.State, verb rune) { + _, err := s.Write([]byte(d.String())) + if err != nil { + panic(err) + } +} + func (d Dec) String() string { bz, err := d.Int.MarshalText() if err != nil { diff --git a/x/distribution/keeper/hooks.go b/x/distribution/keeper/hooks.go index a4f4353fa711..b531a40429a9 100644 --- a/x/distribution/keeper/hooks.go +++ b/x/distribution/keeper/hooks.go @@ -10,6 +10,11 @@ import ( // Create a new validator distribution record func (k Keeper) onValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) { + // defensive check for existence + if k.HasValidatorDistInfo(ctx, valAddr) { + panic("validator dist info already exists (not cleaned up properly)") + } + height := ctx.BlockHeight() vdi := types.ValidatorDistInfo{ OperatorAddr: valAddr, diff --git a/x/distribution/keeper/test_common.go b/x/distribution/keeper/test_common.go index 660abbd0e048..26033432fcbf 100644 --- a/x/distribution/keeper/test_common.go +++ b/x/distribution/keeper/test_common.go @@ -162,7 +162,8 @@ func (fck DummyFeeCollectionKeeper) ClearCollectedFees(_ sdk.Context) { //__________________________________________________________________________________ // used in simulation -// iterate over all the validator distribution infos (inefficient, just used to check invariants) +// iterate over all the validator distribution infos (inefficient, just used to +// check invariants) func (k Keeper) IterateValidatorDistInfos(ctx sdk.Context, fn func(index int64, distInfo types.ValidatorDistInfo) (stop bool)) { @@ -179,3 +180,22 @@ func (k Keeper) IterateValidatorDistInfos(ctx sdk.Context, index++ } } + +// iterate over all the delegation distribution infos (inefficient, just used +// to check invariants) +func (k Keeper) IterateDelegationDistInfos(ctx sdk.Context, + fn func(index int64, distInfo types.DelegationDistInfo) (stop bool)) { + + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) + defer iter.Close() + index := int64(0) + for ; iter.Valid(); iter.Next() { + var ddi types.DelegationDistInfo + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &ddi) + if fn(index, ddi) { + return + } + index++ + } +} diff --git a/x/distribution/keeper/validator.go b/x/distribution/keeper/validator.go index d2c755cfa6b5..119622edd211 100644 --- a/x/distribution/keeper/validator.go +++ b/x/distribution/keeper/validator.go @@ -36,6 +36,13 @@ func (k Keeper) SetValidatorDistInfo(ctx sdk.Context, vdi types.ValidatorDistInf // remove a validator distribution info func (k Keeper) RemoveValidatorDistInfo(ctx sdk.Context, valAddr sdk.ValAddress) { + + // defensive check + vdi := k.GetValidatorDistInfo(ctx, valAddr) + if vdi.DelAccum.Accum.IsPositive() { + panic("Should not delete validator with unwithdrawn delegator accum") + } + store := ctx.KVStore(k.storeKey) store.Delete(GetValidatorDistInfoKey(valAddr)) } diff --git a/x/distribution/simulation/invariants.go b/x/distribution/simulation/invariants.go index 75863bfdb406..128e7faa9ad2 100644 --- a/x/distribution/simulation/invariants.go +++ b/x/distribution/simulation/invariants.go @@ -18,6 +18,10 @@ func AllInvariants(d distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { if err != nil { return err } + err = DelAccumInvariants(d, sk)(app) + if err != nil { + return err + } return nil } } @@ -48,3 +52,81 @@ func ValAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invaria return nil } } + +// DelAccumInvariants checks that each validator del accum == sum all delegators' accum +func DelAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { + + return func(app *baseapp.BaseApp) error { + mockHeader := abci.Header{Height: app.LastBlockHeight() + 1} + ctx := app.NewContext(false, mockHeader) + height := ctx.BlockHeight() + + totalDelAccumFromVal := make(map[string]sdk.Dec) // key is the valOpAddr string + totalDelAccum := make(map[string]sdk.Dec) + + // iterate the validators + iterVal := func(_ int64, vdi distr.ValidatorDistInfo) bool { + key := vdi.OperatorAddr.String() + validator := sk.Validator(ctx, vdi.OperatorAddr) + totalDelAccumFromVal[key] = vdi.GetTotalDelAccum(height, + validator.GetDelegatorShares()) + + // also initialize the delegation map + totalDelAccum[key] = sdk.ZeroDec() + + return false + } + k.IterateValidatorDistInfos(ctx, iterVal) + + // iterate the delegations + iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { + key := ddi.ValOperatorAddr.String() + delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) + totalDelAccum[key] = totalDelAccum[key].Add( + ddi.GetDelAccum(height, delegation.GetShares())) + return false + } + k.IterateDelegationDistInfos(ctx, iterDel) + + // compare + for key, delAccumFromVal := range totalDelAccumFromVal { + sumDelAccum := totalDelAccum[key] + + if !sumDelAccum.Equal(delAccumFromVal) { + + logDelAccums := "" + iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { + keyLog := ddi.ValOperatorAddr.String() + if keyLog == key { + delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) + accum := ddi.GetDelAccum(height, delegation.GetShares()) + if accum.IsPositive() { + logDelAccums += fmt.Sprintf("\n\t\tdel: %v, accum: %v", + ddi.DelegatorAddr.String(), + accum.String()) + } + } + return false + } + k.IterateDelegationDistInfos(ctx, iterDel) + + operAddr, err := sdk.ValAddressFromBech32(key) + if err != nil { + panic(err) + } + validator := sk.Validator(ctx, operAddr) + + return fmt.Errorf("delegator accum invariance: \n"+ + "\tvalidator key: %v\n"+ + "\tvalidator: %+v\n"+ + "\tsum delegator accum: %v\n"+ + "\tvalidator's total delegator accum: %v\n"+ + "\tlog of delegations with accum: %v\n", + key, validator, sumDelAccum.String(), + delAccumFromVal.String(), logDelAccums) + } + } + + return nil + } +} diff --git a/x/distribution/types/dec_coin.go b/x/distribution/types/dec_coin.go index 5eedad7e36b3..c5d9f360ad65 100644 --- a/x/distribution/types/dec_coin.go +++ b/x/distribution/types/dec_coin.go @@ -20,6 +20,13 @@ func NewDecCoin(denom string, amount int64) DecCoin { } } +func NewDecCoinFromDec(denom string, amount sdk.Dec) DecCoin { + return DecCoin{ + Denom: denom, + Amount: amount, + } +} + func NewDecCoinFromCoin(coin sdk.Coin) DecCoin { return DecCoin{ Denom: coin.Denom, @@ -140,7 +147,7 @@ func (coins DecCoins) MulDec(d sdk.Dec) DecCoins { return res } -// divide all the coins by a multiple +// divide all the coins by a decimal func (coins DecCoins) QuoDec(d sdk.Dec) DecCoins { res := make([]DecCoin, len(coins)) for i, coin := range coins { @@ -176,3 +183,13 @@ func (coins DecCoins) AmountOf(denom string) sdk.Dec { } } } + +// returns the amount of a denom from deccoins +func (coins DecCoins) HasNegative() bool { + for _, coin := range coins { + if coin.Amount.IsNegative() { + return true + } + } + return false +} diff --git a/x/distribution/types/delegator_info.go b/x/distribution/types/delegator_info.go index decf321298f7..6068752f21c3 100644 --- a/x/distribution/types/delegator_info.go +++ b/x/distribution/types/delegator_info.go @@ -1,6 +1,8 @@ package types import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -24,7 +26,17 @@ func NewDelegationDistInfo(delegatorAddr sdk.AccAddress, valOperatorAddr sdk.Val // Get the calculated accum of this delegator at the provided height func (di DelegationDistInfo) GetDelAccum(height int64, delegatorShares sdk.Dec) sdk.Dec { blocks := height - di.DelPoolWithdrawalHeight - return delegatorShares.MulInt(sdk.NewInt(blocks)) + accum := delegatorShares.MulInt(sdk.NewInt(blocks)) + + // defensive check + if accum.IsNegative() { + panic(fmt.Sprintf("negative accum: %v\n"+ + "\theight: %v\n"+ + "\tdelegation_dist_info: %v\n"+ + "\tdelegator_shares: %v\n", + accum.String(), height, di, delegatorShares)) + } + return accum } // Withdraw rewards from delegator. @@ -41,7 +53,9 @@ func (di DelegationDistInfo) WithdrawRewards(wc WithdrawContext, vi ValidatorDis fp := wc.FeePool vi = vi.UpdateTotalDelAccum(wc.Height, totalDelShares) + // Break out to prevent a divide by zero. if vi.DelAccum.Accum.IsZero() { + di.DelPoolWithdrawalHeight = wc.Height return di, vi, fp, DecCoins{} } @@ -49,9 +63,43 @@ func (di DelegationDistInfo) WithdrawRewards(wc WithdrawContext, vi ValidatorDis accum := di.GetDelAccum(wc.Height, delegatorShares) di.DelPoolWithdrawalHeight = wc.Height + withdrawalTokens := vi.DelPool.MulDec(accum).QuoDec(vi.DelAccum.Accum) - vi.DelPool = vi.DelPool.Minus(withdrawalTokens) + // Clip withdrawal tokens by pool, due to possible rounding errors. + // This rounding error may be introduced upon multiplication since + // we're clipping decimal digits, and then when we divide by a number ~1 or + // < 1, the error doesn't get "buried", and if << 1 it'll get amplified. + // more: https://github.com/cosmos/cosmos-sdk/issues/2888#issuecomment-441387987 + for i, decCoin := range withdrawalTokens { + poolDenomAmount := vi.DelPool.AmountOf(decCoin.Denom) + if decCoin.Amount.GT(poolDenomAmount) { + withdrawalTokens[i] = NewDecCoinFromDec(decCoin.Denom, poolDenomAmount) + } + } + + // defensive check for impossible accum ratios + if accum.GT(vi.DelAccum.Accum) { + panic(fmt.Sprintf("accum > vi.DelAccum.Accum:\n"+ + "\taccum\t\t\t%v\n"+ + "\tvi.DelAccum.Accum\t%v\n", + accum, vi.DelAccum.Accum)) + } + + remDelPool := vi.DelPool.Minus(withdrawalTokens) + + // defensive check + if remDelPool.HasNegative() { + panic(fmt.Sprintf("negative remDelPool: %v\n"+ + "\tvi.DelPool\t\t%v\n"+ + "\taccum\t\t\t%v\n"+ + "\tvi.DelAccum.Accum\t%v\n"+ + "\twithdrawalTokens\t%v\n", + remDelPool, vi.DelPool, accum, + vi.DelAccum.Accum, withdrawalTokens)) + } + + vi.DelPool = remDelPool vi.DelAccum.Accum = vi.DelAccum.Accum.Sub(accum) return di, vi, fp, withdrawalTokens diff --git a/x/mint/abci_app.go b/x/mint/abci_app.go index 73491f8081e1..963039961a6b 100644 --- a/x/mint/abci_app.go +++ b/x/mint/abci_app.go @@ -6,21 +6,26 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Called every block, process inflation on the first block of every hour +// Inflate every block, update inflation parameters once per hour func BeginBlocker(ctx sdk.Context, k Keeper) { blockTime := ctx.BlockHeader().Time minter := k.GetMinter(ctx) - if blockTime.Sub(minter.InflationLastTime) < time.Hour { // only mint on the hour! + params := k.GetParams(ctx) + + mintedCoin := minter.BlockProvision(params) + k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin}) + k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount)) + + if blockTime.Sub(minter.LastUpdate) < time.Hour { return } - params := k.GetParams(ctx) + // adjust the inflation, hourly-provision rate every hour totalSupply := k.sk.TotalPower(ctx) bondedRatio := k.sk.BondedRatio(ctx) - minter.InflationLastTime = blockTime - minter, mintedCoin := minter.ProcessProvisions(params, totalSupply, bondedRatio) - k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin}) - k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount)) + minter.Inflation = minter.NextInflationRate(params, bondedRatio) + minter.AnnualProvisions = minter.NextAnnualProvisions(params, totalSupply) + minter.LastUpdate = blockTime k.SetMinter(ctx, minter) } diff --git a/x/mint/genesis.go b/x/mint/genesis.go index ce375d71e576..27615ed7ee47 100644 --- a/x/mint/genesis.go +++ b/x/mint/genesis.go @@ -20,7 +20,7 @@ func NewGenesisState(minter Minter, params Params) GenesisState { // get raw genesis raw message for testing func DefaultGenesisState() GenesisState { return GenesisState{ - Minter: InitialMinter(), + Minter: DefaultInitialMinter(), Params: DefaultParams(), } } diff --git a/x/mint/minter.go b/x/mint/minter.go index 135675887bf0..2edf1dabea38 100644 --- a/x/mint/minter.go +++ b/x/mint/minter.go @@ -7,45 +7,54 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// current inflation state +// Minter represents the minting state type Minter struct { - InflationLastTime time.Time `json:"inflation_last_time"` // block time which the last inflation was processed - Inflation sdk.Dec `json:"inflation"` // current annual inflation rate + LastUpdate time.Time `json:"last_update"` // time which the last update was made to the minter + Inflation sdk.Dec `json:"inflation"` // current annual inflation rate + AnnualProvisions sdk.Dec `json:"annual_provisions"` // current annual expected provisions } -// minter object for a new minter -func InitialMinter() Minter { +// Create a new minter object +func NewMinter(lastUpdate time.Time, inflation, + annualProvisions sdk.Dec) Minter { + return Minter{ - InflationLastTime: time.Unix(0, 0), - Inflation: sdk.NewDecWithPrec(13, 2), + LastUpdate: lastUpdate, + Inflation: inflation, + AnnualProvisions: annualProvisions, } } +// minter object for a new chain +func InitialMinter(inflation sdk.Dec) Minter { + return NewMinter( + time.Unix(0, 0), + inflation, + sdk.NewDec(0), + ) +} + +// default initial minter object for a new chain +// which uses an inflation rate of 13% +func DefaultInitialMinter() Minter { + return InitialMinter( + sdk.NewDecWithPrec(13, 2), + ) +} + func validateMinter(minter Minter) error { if minter.Inflation.LT(sdk.ZeroDec()) { - return fmt.Errorf("mint parameter Inflation should be positive, is %s ", minter.Inflation.String()) - } - if minter.Inflation.GT(sdk.OneDec()) { - return fmt.Errorf("mint parameter Inflation must be <= 1, is %s", minter.Inflation.String()) + return fmt.Errorf("mint parameter Inflation should be positive, is %s", + minter.Inflation.String()) } return nil } var hrsPerYr = sdk.NewDec(8766) // as defined by a julian year of 365.25 days -// process provisions for an hour period -func (m Minter) ProcessProvisions(params Params, totalSupply, bondedRatio sdk.Dec) ( - minter Minter, provisions sdk.Coin) { - - m.Inflation = m.NextInflation(params, bondedRatio) - provisionsDec := m.Inflation.Mul(totalSupply).Quo(hrsPerYr) - provisions = sdk.NewCoin(params.MintDenom, provisionsDec.TruncateInt()) - - return m, provisions -} - -// get the next inflation rate for the hour -func (m Minter) NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { +// get the new inflation rate for the next hour +func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) ( + inflation sdk.Dec) { // The target annual inflation rate is recalculated for each previsions cycle. The // inflation is also subject to a rate change (positive or negative) depending on @@ -70,3 +79,16 @@ func (m Minter) NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk return inflation } + +// calculate the annual provisions based on current total supply and inflation rate +func (m Minter) NextAnnualProvisions(params Params, totalSupply sdk.Dec) ( + provisions sdk.Dec) { + + return m.Inflation.Mul(totalSupply) +} + +// get the provisions for a block based on the annual provisions rate +func (m Minter) BlockProvision(params Params) sdk.Coin { + provisionAmt := m.AnnualProvisions.QuoInt(sdk.NewInt(int64(params.BlocksPerYear))) + return sdk.NewCoin(params.MintDenom, provisionAmt.TruncateInt()) +} diff --git a/x/mint/minter_test.go b/x/mint/minter_test.go index b022b0ec804c..63cd253d7696 100644 --- a/x/mint/minter_test.go +++ b/x/mint/minter_test.go @@ -1,6 +1,7 @@ package mint import ( + "math/rand" "testing" "github.com/stretchr/testify/require" @@ -9,7 +10,7 @@ import ( ) func TestNextInflation(t *testing.T) { - minter := InitialMinter() + minter := DefaultInitialMinter() params := DefaultParams() // Governing Mechanism: @@ -44,10 +45,57 @@ func TestNextInflation(t *testing.T) { for i, tc := range tests { minter.Inflation = tc.setInflation - inflation := minter.NextInflation(params, tc.bondedRatio) + inflation := minter.NextInflationRate(params, tc.bondedRatio) diffInflation := inflation.Sub(tc.setInflation) require.True(t, diffInflation.Equal(tc.expChange), "Test Index: %v\nDiff: %v\nExpected: %v\n", i, diffInflation, tc.expChange) } } + +func TestBlockProvision(t *testing.T) { + minter := InitialMinter(sdk.NewDecWithPrec(1, 1)) + params := DefaultParams() + + secondsPerYear := int64(60 * 60 * 8766) + + tests := []struct { + annualProvisions int64 + expProvisions int64 + }{ + {secondsPerYear / 5, 1}, + {secondsPerYear/5 + 1, 1}, + {(secondsPerYear / 5) * 2, 2}, + {(secondsPerYear / 5) / 2, 0}, + } + for i, tc := range tests { + minter.AnnualProvisions = sdk.NewDec(tc.annualProvisions) + provisions := minter.BlockProvision(params) + + expProvisions := sdk.NewCoin(params.MintDenom, + sdk.NewInt(tc.expProvisions)) + + require.True(t, expProvisions.IsEqual(provisions), + "test: %v\n\tExp: %v\n\tGot: %v\n", + i, tc.expProvisions, provisions) + } +} + +// Benchmarking :) +// previously using sdk.Int operations: +// BenchmarkBlockProvision-4 5000000 220 ns/op +// +// using sdk.Dec operations: (current implementation) +// BenchmarkBlockProvision-4 3000000 429 ns/op +func BenchmarkBlockProvision(b *testing.B) { + minter := InitialMinter(sdk.NewDecWithPrec(1, 1)) + params := DefaultParams() + + s1 := rand.NewSource(100) + r1 := rand.New(s1) + minter.AnnualProvisions = sdk.NewDec(r1.Int63n(1000000)) + + for n := 0; n < b.N; n++ { + minter.BlockProvision(params) + } +} diff --git a/x/mint/params.go b/x/mint/params.go index 47c9c85480f2..e1acd3a63014 100644 --- a/x/mint/params.go +++ b/x/mint/params.go @@ -2,6 +2,7 @@ package mint import ( "fmt" + stakeTypes "github.com/cosmos/cosmos-sdk/x/stake/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,6 +15,20 @@ type Params struct { InflationMax sdk.Dec `json:"inflation_max"` // maximum inflation rate InflationMin sdk.Dec `json:"inflation_min"` // minimum inflation rate GoalBonded sdk.Dec `json:"goal_bonded"` // goal of percent bonded atoms + BlocksPerYear uint64 `json:"blocks_per_year"` // expected blocks per year +} + +func NewParams(mintDenom string, inflationRateChange, inflationMax, + inflationMin, goalBonded sdk.Dec, blocksPerYear uint64) Params { + + return Params{ + MintDenom: mintDenom, + InflationRateChange: inflationRateChange, + InflationMax: inflationMax, + InflationMin: inflationMin, + GoalBonded: goalBonded, + BlocksPerYear: blocksPerYear, + } } // default minting module parameters @@ -24,6 +39,7 @@ func DefaultParams() Params { InflationMax: sdk.NewDecWithPrec(20, 2), InflationMin: sdk.NewDecWithPrec(7, 2), GoalBonded: sdk.NewDecWithPrec(67, 2), + BlocksPerYear: uint64(60 * 60 * 8766 / 5), // assuming 5 second block times } } diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index 32f63f5ed47a..b68b20ee8c98 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -600,6 +600,9 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, } rounded := returnAmount.TruncateInt() + if rounded.IsZero() { + return types.Redelegation{}, types.ErrVerySmallRedelegation(k.Codespace()) + } returnCoin := sdk.NewCoin(k.BondDenom(ctx), rounded) change := returnAmount.Sub(sdk.NewDecFromInt(rounded)) @@ -612,6 +615,7 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, if !found { return types.Redelegation{}, types.ErrBadRedelegationDst(k.Codespace()) } + sharesCreated, err := k.Delegate(ctx, delAddr, returnCoin, dstValidator, false) if err != nil { return types.Redelegation{}, err diff --git a/x/stake/keeper/sdk_types.go b/x/stake/keeper/sdk_types.go index 1dea473f892b..f777077d7d1f 100644 --- a/x/stake/keeper/sdk_types.go +++ b/x/stake/keeper/sdk_types.go @@ -10,7 +10,7 @@ import ( // Implements ValidatorSet var _ sdk.ValidatorSet = Keeper{} -// iterate through the active validator set and perform the provided function +// iterate through the validator set and perform the provided function func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { store := ctx.KVStore(k.storeKey) iterator := sdk.KVStorePrefixIterator(store, ValidatorsKey) @@ -27,7 +27,7 @@ func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validato iterator.Close() } -// iterate through the active validator set and perform the provided function +// iterate through the bonded validator set and perform the provided function func (k Keeper) IterateBondedValidatorsByPower(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { store := ctx.KVStore(k.storeKey) maxValidators := k.MaxValidators(ctx) diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go index 439f40de3b05..44348c673f6c 100644 --- a/x/stake/simulation/invariants.go +++ b/x/stake/simulation/invariants.go @@ -33,6 +33,16 @@ func AllInvariants(ck bank.Keeper, k stake.Keeper, return err } + err = PositiveDelegationInvariant(k)(app) + if err != nil { + return err + } + + err = DelegatorSharesInvariant(k)(app) + if err != nil { + return err + } + err = ValidatorSetInvariant(k)(app) return err } @@ -131,6 +141,53 @@ func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { } } +// PositiveDelegationInvariant checks that all stored delegations have > 0 shares. +func PositiveDelegationInvariant(k stake.Keeper) simulation.Invariant { + return func(app *baseapp.BaseApp) error { + ctx := app.NewContext(false, abci.Header{}) + + delegations := k.GetAllDelegations(ctx) + for _, delegation := range delegations { + if delegation.Shares.IsNegative() { + return fmt.Errorf("delegation with negative shares: %+v", delegation) + } + if delegation.Shares.IsZero() { + return fmt.Errorf("delegation with zero shares: %+v", delegation) + } + } + + return nil + } +} + +// DelegatorSharesInvariant checks whether all the delegator shares which persist +// in the delegator object add up to the correct total delegator shares +// amount stored in each validator +func DelegatorSharesInvariant(k stake.Keeper) simulation.Invariant { + return func(app *baseapp.BaseApp) error { + ctx := app.NewContext(false, abci.Header{}) + + validators := k.GetAllValidators(ctx) + for _, validator := range validators { + + valTotalDelShares := validator.GetDelegatorShares() + + totalDelShares := sdk.ZeroDec() + delegations := k.GetValidatorDelegations(ctx, validator.GetOperator()) + for _, delegation := range delegations { + totalDelShares = totalDelShares.Add(delegation.Shares) + } + + if !valTotalDelShares.Equal(totalDelShares) { + return fmt.Errorf("broken delegator shares invariance:\n"+ + "\tvalidator.DelegatorShares: %v\n"+ + "\tsum of Delegator.Shares: %v", valTotalDelShares, totalDelShares) + } + } + return nil + } +} + // ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant { return func(app *baseapp.BaseApp) error { diff --git a/x/stake/types/errors.go b/x/stake/types/errors.go index bbef528d5095..57dc2fb00022 100644 --- a/x/stake/types/errors.go +++ b/x/stake/types/errors.go @@ -159,6 +159,10 @@ func ErrSelfRedelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "cannot redelegate to the same validator") } +func ErrVerySmallRedelegation(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDelegation, "too few tokens to redelegate, truncates to zero tokens") +} + func ErrBadRedelegationDst(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "redelegation validator not found") }