blockly minting (#2825)
* update mechanism to use average block time * correctly sets accum height for zero-delegations * update Decimal Format() * clip withdrawal tokens * PositiveDelegationInvariant * DelegatorSharesInvariant * DelAccumInvariants
This commit is contained in:
parent
4c36b0fe05
commit
7cb1ba625e
|
@ -14,6 +14,7 @@ BREAKING CHANGES
|
|||
* [cli] [\#2875](https://github.com/cosmos/cosmos-sdk/pull/2875) Refactor `gaiad gentx` and avoid redirection to `gaiacli tx sign` for tx signing.
|
||||
|
||||
* 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.
|
||||
|
@ -74,6 +75,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
```
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue