diff --git a/app/app.go b/app/app.go index 01491cb1..3cd5d3d0 100644 --- a/app/app.go +++ b/app/app.go @@ -3,9 +3,10 @@ package app import ( "encoding/json" "github.com/cosmos/cosmos-sdk/simapp" - "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/unification-com/mainchain-cosmos/app/ante" "github.com/unification-com/mainchain-cosmos/x/enterprise" + "github.com/unification-com/mainchain-cosmos/x/mint" "io" "os" @@ -224,7 +225,7 @@ func NewMainchainApp( bank.NewAppModule(app.bankKeeper, app.accountKeeper), supply.NewAppModule(app.supplyKeeper, app.accountKeeper), distr.NewAppModule(app.distrKeeper, app.supplyKeeper), - mint.NewAppModule(app.mintKeeper), + mint.NewAppModule(app.mintKeeper, app.enterpriseKeeper), slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), wrkchain.NewAppModule(app.wrkChainKeeper), diff --git a/x/mint/abci.go b/x/mint/abci.go new file mode 100644 index 00000000..0b93b027 --- /dev/null +++ b/x/mint/abci.go @@ -0,0 +1,49 @@ +package mint + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/enterprise" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// BeginBlocker mints new tokens for the previous block. +func BeginBlocker(ctx sdk.Context, k Keeper, keeper enterprise.Keeper) { + // fetch stored minter & params + minter := k.GetMinter(ctx) + params := k.GetParams(ctx) + + // recalculate inflation rate + totalUNDSupply := keeper.GetTotalUndSupply(ctx) + totalLockedUND := keeper.GetTotalLockedUnd(ctx) + liquidUND := totalUNDSupply.Sub(totalLockedUND) + + bondedRatio := k.BondedRatio(ctx) + minter.Inflation = minter.NextInflationRate(params, bondedRatio) + minter.AnnualProvisions = minter.NextAnnualProvisions(params, liquidUND.Amount) + k.SetMinter(ctx, minter) + + // mint coins, update supply + mintedCoin := minter.BlockProvision(params) + mintedCoins := sdk.NewCoins(mintedCoin) + + err := k.MintCoins(ctx, mintedCoins) + if err != nil { + panic(err) + } + + // send the minted coins to the fee collector account + err = k.AddCollectedFees(ctx, mintedCoins) + if err != nil { + panic(err) + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeMint, + sdk.NewAttribute(types.AttributeKeyBondedRatio, bondedRatio.String()), + sdk.NewAttribute(types.AttributeKeyInflation, minter.Inflation.String()), + sdk.NewAttribute(types.AttributeKeyAnnualProvisions, minter.AnnualProvisions.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, mintedCoin.Amount.String()), + ), + ) +} diff --git a/x/mint/alias.go b/x/mint/alias.go new file mode 100644 index 00000000..95aaf0e6 --- /dev/null +++ b/x/mint/alias.go @@ -0,0 +1,55 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/unification-com/mainchain-cosmos/x/mint/internal/keeper +// ALIASGEN: github.com/unification-com/mainchain-cosmos/x/mint/internal/types +package mint + +import ( + "github.com/unification-com/mainchain-cosmos/x/mint/internal/keeper" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +const ( + ModuleName = types.ModuleName + DefaultParamspace = types.DefaultParamspace + StoreKey = types.StoreKey + QuerierRoute = types.QuerierRoute + QueryParameters = types.QueryParameters + QueryInflation = types.QueryInflation + QueryAnnualProvisions = types.QueryAnnualProvisions +) + +var ( + // functions aliases + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + ValidateGenesis = types.ValidateGenesis + NewMinter = types.NewMinter + InitialMinter = types.InitialMinter + DefaultInitialMinter = types.DefaultInitialMinter + ValidateMinter = types.ValidateMinter + ParamKeyTable = types.ParamKeyTable + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ValidateParams = types.ValidateParams + + // variable aliases + ModuleCdc = types.ModuleCdc + MinterKey = types.MinterKey + KeyMintDenom = types.KeyMintDenom + KeyInflationRateChange = types.KeyInflationRateChange + KeyInflationMax = types.KeyInflationMax + KeyInflationMin = types.KeyInflationMin + KeyGoalBonded = types.KeyGoalBonded + KeyBlocksPerYear = types.KeyBlocksPerYear +) + +type ( + Keeper = keeper.Keeper + GenesisState = types.GenesisState + Minter = types.Minter + Params = types.Params +) diff --git a/x/mint/client/cli/query.go b/x/mint/client/cli/query.go new file mode 100644 index 00000000..d6a41135 --- /dev/null +++ b/x/mint/client/cli/query.go @@ -0,0 +1,112 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// GetQueryCmd returns the cli query commands for the minting module. +func GetQueryCmd(cdc *codec.Codec) *cobra.Command { + mintingQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the minting module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + mintingQueryCmd.AddCommand( + client.GetCommands( + GetCmdQueryParams(cdc), + GetCmdQueryInflation(cdc), + GetCmdQueryAnnualProvisions(cdc), + )..., + ) + + return mintingQueryCmd +} + +// GetCmdQueryParams implements a command to return the current minting +// parameters. +func GetCmdQueryParams(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "params", + Short: "Query the current minting parameters", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParameters) + res, _, err := cliCtx.QueryWithData(route, nil) + if err != nil { + return err + } + + var params types.Params + if err := cdc.UnmarshalJSON(res, ¶ms); err != nil { + return err + } + + return cliCtx.PrintOutput(params) + }, + } +} + +// GetCmdQueryInflation implements a command to return the current minting +// inflation value. +func GetCmdQueryInflation(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "inflation", + Short: "Query the current minting inflation value", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryInflation) + res, _, err := cliCtx.QueryWithData(route, nil) + if err != nil { + return err + } + + var inflation sdk.Dec + if err := cdc.UnmarshalJSON(res, &inflation); err != nil { + return err + } + + return cliCtx.PrintOutput(inflation) + }, + } +} + +// GetCmdQueryAnnualProvisions implements a command to return the current minting +// annual provisions value. +func GetCmdQueryAnnualProvisions(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "annual-provisions", + Short: "Query the current minting annual provisions value", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryAnnualProvisions) + res, _, err := cliCtx.QueryWithData(route, nil) + if err != nil { + return err + } + + var inflation sdk.Dec + if err := cdc.UnmarshalJSON(res, &inflation); err != nil { + return err + } + + return cliCtx.PrintOutput(inflation) + }, + } +} diff --git a/x/mint/client/rest/query.go b/x/mint/client/rest/query.go new file mode 100644 index 00000000..02665b3c --- /dev/null +++ b/x/mint/client/rest/query.go @@ -0,0 +1,89 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc( + "/minting/parameters", + queryParamsHandlerFn(cliCtx), + ).Methods("GET") + + r.HandleFunc( + "/minting/inflation", + queryInflationHandlerFn(cliCtx), + ).Methods("GET") + + r.HandleFunc( + "/minting/annual-provisions", + queryAnnualProvisionsHandlerFn(cliCtx), + ).Methods("GET") +} + +func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParameters) + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryInflationHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryInflation) + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryAnnualProvisionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryAnnualProvisions) + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/mint/client/rest/rest.go b/x/mint/client/rest/rest.go new file mode 100644 index 00000000..556e1468 --- /dev/null +++ b/x/mint/client/rest/rest.go @@ -0,0 +1,12 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" +) + +// RegisterRoutes registers minting module REST handlers on the provided router. +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + registerQueryRoutes(cliCtx, r) +} diff --git a/x/mint/genesis.go b/x/mint/genesis.go new file mode 100644 index 00000000..331852b4 --- /dev/null +++ b/x/mint/genesis.go @@ -0,0 +1,18 @@ +package mint + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitGenesis new mint genesis +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + keeper.SetMinter(ctx, data.Minter) + keeper.SetParams(ctx, data.Params) +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + minter := keeper.GetMinter(ctx) + params := keeper.GetParams(ctx) + return NewGenesisState(minter, params) +} diff --git a/x/mint/internal/keeper/integration_test.go b/x/mint/internal/keeper/integration_test.go new file mode 100644 index 00000000..930b122b --- /dev/null +++ b/x/mint/internal/keeper/integration_test.go @@ -0,0 +1,20 @@ +package keeper_test + +import ( + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// returns context and an app with updated mint keeper +func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) { + app := simapp.Setup(isCheckTx) + + ctx := app.BaseApp.NewContext(isCheckTx, abci.Header{}) + app.MintKeeper.SetParams(ctx, types.DefaultParams()) + app.MintKeeper.SetMinter(ctx, types.DefaultInitialMinter()) + + return app, ctx +} diff --git a/x/mint/internal/keeper/keeper.go b/x/mint/internal/keeper/keeper.go new file mode 100644 index 00000000..2028468f --- /dev/null +++ b/x/mint/internal/keeper/keeper.go @@ -0,0 +1,111 @@ +package keeper + +import ( + "fmt" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Keeper of the mint store +type Keeper struct { + cdc *codec.Codec + storeKey sdk.StoreKey + paramSpace params.Subspace + sk types.StakingKeeper + supplyKeeper types.SupplyKeeper + feeCollectorName string +} + +// NewKeeper creates a new mint Keeper instance +func NewKeeper( + cdc *codec.Codec, key sdk.StoreKey, paramSpace params.Subspace, + sk types.StakingKeeper, supplyKeeper types.SupplyKeeper, feeCollectorName string) Keeper { + + // ensure mint module account is set + if addr := supplyKeeper.GetModuleAddress(types.ModuleName); addr == nil { + panic("the mint module account has not been set") + } + + return Keeper{ + cdc: cdc, + storeKey: key, + paramSpace: paramSpace.WithKeyTable(types.ParamKeyTable()), + sk: sk, + supplyKeeper: supplyKeeper, + feeCollectorName: feeCollectorName, + } +} + +//______________________________________________________________________ + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// get the minter +func (k Keeper) GetMinter(ctx sdk.Context) (minter types.Minter) { + store := ctx.KVStore(k.storeKey) + b := store.Get(types.MinterKey) + if b == nil { + panic("stored minter should not have been nil") + } + + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &minter) + return +} + +// set the minter +func (k Keeper) SetMinter(ctx sdk.Context, minter types.Minter) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(minter) + store.Set(types.MinterKey, b) +} + +//______________________________________________________________________ + +// GetParams returns the total set of minting parameters. +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramSpace.GetParamSet(ctx, ¶ms) + return params +} + +// SetParams sets the total set of minting parameters. +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSpace.SetParamSet(ctx, ¶ms) +} + +//______________________________________________________________________ + +// StakingTokenSupply implements an alias call to the underlying staking keeper's +// StakingTokenSupply to be used in BeginBlocker. +func (k Keeper) StakingTokenSupply(ctx sdk.Context) sdk.Int { + return k.sk.StakingTokenSupply(ctx) +} + +// BondedRatio implements an alias call to the underlying staking keeper's +// BondedRatio to be used in BeginBlocker. +func (k Keeper) BondedRatio(ctx sdk.Context) sdk.Dec { + return k.sk.BondedRatio(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) sdk.Error { + if newCoins.Empty() { + // skip as no coins need to be minted + return nil + } + return k.supplyKeeper.MintCoins(ctx, types.ModuleName, 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) sdk.Error { + return k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, k.feeCollectorName, fees) +} diff --git a/x/mint/internal/keeper/querier.go b/x/mint/internal/keeper/querier.go new file mode 100644 index 00000000..76a88ff9 --- /dev/null +++ b/x/mint/internal/keeper/querier.go @@ -0,0 +1,63 @@ +package keeper + +import ( + "fmt" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// NewQuerier returns a minting Querier handler. +func NewQuerier(k Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, _ abci.RequestQuery) ([]byte, sdk.Error) { + switch path[0] { + case types.QueryParameters: + return queryParams(ctx, k) + + case types.QueryInflation: + return queryInflation(ctx, k) + + case types.QueryAnnualProvisions: + return queryAnnualProvisions(ctx, k) + + default: + return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown minting query endpoint: %s", path[0])) + } + } +} + +func queryParams(ctx sdk.Context, k Keeper) ([]byte, sdk.Error) { + params := k.GetParams(ctx) + + res, err := codec.MarshalJSONIndent(k.cdc, params) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to marshal JSON", err.Error())) + } + + return res, nil +} + +func queryInflation(ctx sdk.Context, k Keeper) ([]byte, sdk.Error) { + minter := k.GetMinter(ctx) + + res, err := codec.MarshalJSONIndent(k.cdc, minter.Inflation) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to marshal JSON", err.Error())) + } + + return res, nil +} + +func queryAnnualProvisions(ctx sdk.Context, k Keeper) ([]byte, sdk.Error) { + minter := k.GetMinter(ctx) + + res, err := codec.MarshalJSONIndent(k.cdc, minter.AnnualProvisions) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to marshal JSON", err.Error())) + } + + return res, nil +} diff --git a/x/mint/internal/keeper/querier_test.go b/x/mint/internal/keeper/querier_test.go new file mode 100644 index 00000000..2c931fda --- /dev/null +++ b/x/mint/internal/keeper/querier_test.go @@ -0,0 +1,80 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + keep "github.com/unification-com/mainchain-cosmos/x/mint/internal/keeper" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" + + abci "github.com/tendermint/tendermint/abci/types" +) + +func TestNewQuerier(t *testing.T) { + app, ctx := createTestApp(true) + querier := keep.NewQuerier(app.MintKeeper) + + query := abci.RequestQuery{ + Path: "", + Data: []byte{}, + } + + _, err := querier(ctx, []string{types.QueryParameters}, query) + require.NoError(t, err) + + _, err = querier(ctx, []string{types.QueryInflation}, query) + require.NoError(t, err) + + _, err = querier(ctx, []string{types.QueryAnnualProvisions}, query) + require.NoError(t, err) + + _, err = querier(ctx, []string{"foo"}, query) + require.Error(t, err) +} + +func TestQueryParams(t *testing.T) { + app, ctx := createTestApp(true) + querier := keep.NewQuerier(app.MintKeeper) + + var params types.Params + + res, sdkErr := querier(ctx, []string{types.QueryParameters}, abci.RequestQuery{}) + require.NoError(t, sdkErr) + + err := app.Codec().UnmarshalJSON(res, ¶ms) + require.NoError(t, err) + + require.Equal(t, app.MintKeeper.GetParams(ctx), params) +} + +func TestQueryInflation(t *testing.T) { + app, ctx := createTestApp(true) + querier := keep.NewQuerier(app.MintKeeper) + + var inflation sdk.Dec + + res, sdkErr := querier(ctx, []string{types.QueryInflation}, abci.RequestQuery{}) + require.NoError(t, sdkErr) + + err := app.Codec().UnmarshalJSON(res, &inflation) + require.NoError(t, err) + + require.Equal(t, app.MintKeeper.GetMinter(ctx).Inflation, inflation) +} + +func TestQueryAnnualProvisions(t *testing.T) { + app, ctx := createTestApp(true) + querier := keep.NewQuerier(app.MintKeeper) + + var annualProvisions sdk.Dec + + res, sdkErr := querier(ctx, []string{types.QueryAnnualProvisions}, abci.RequestQuery{}) + require.NoError(t, sdkErr) + + err := app.Codec().UnmarshalJSON(res, &annualProvisions) + require.NoError(t, err) + + require.Equal(t, app.MintKeeper.GetMinter(ctx).AnnualProvisions, annualProvisions) +} diff --git a/x/mint/internal/types/codec.go b/x/mint/internal/types/codec.go new file mode 100644 index 00000000..5787f242 --- /dev/null +++ b/x/mint/internal/types/codec.go @@ -0,0 +1,14 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// generic sealed codec to be used throughout this module +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + codec.RegisterCrypto(ModuleCdc) + ModuleCdc.Seal() +} diff --git a/x/mint/internal/types/events.go b/x/mint/internal/types/events.go new file mode 100644 index 00000000..dce85bdb --- /dev/null +++ b/x/mint/internal/types/events.go @@ -0,0 +1,10 @@ +package types + +// Minting module event types +const ( + EventTypeMint = ModuleName + + AttributeKeyBondedRatio = "bonded_ratio" + AttributeKeyInflation = "inflation" + AttributeKeyAnnualProvisions = "annual_provisions" +) diff --git a/x/mint/internal/types/expected_keepers.go b/x/mint/internal/types/expected_keepers.go new file mode 100644 index 00000000..564a0136 --- /dev/null +++ b/x/mint/internal/types/expected_keepers.go @@ -0,0 +1,24 @@ +package types // noalias + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply/exported" +) + +// StakingKeeper defines the expected staking keeper +type StakingKeeper interface { + StakingTokenSupply(ctx sdk.Context) sdk.Int + BondedRatio(ctx sdk.Context) sdk.Dec +} + +// SupplyKeeper defines the expected supply keeper +type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + + // TODO remove with genesis 2-phases refactor https://github.com/cosmos/cosmos-sdk/issues/2862 + SetModuleAccount(sdk.Context, exported.ModuleAccountI) + + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error +} diff --git a/x/mint/internal/types/genesis.go b/x/mint/internal/types/genesis.go new file mode 100644 index 00000000..43ee15f1 --- /dev/null +++ b/x/mint/internal/types/genesis.go @@ -0,0 +1,34 @@ +package types + +// GenesisState - minter state +type GenesisState struct { + Minter Minter `json:"minter" yaml:"minter"` // minter object + Params Params `json:"params" yaml:"params"` // inflation params +} + +// NewGenesisState creates a new GenesisState object +func NewGenesisState(minter Minter, params Params) GenesisState { + return GenesisState{ + Minter: minter, + Params: params, + } +} + +// DefaultGenesisState creates a default GenesisState object +func DefaultGenesisState() GenesisState { + return GenesisState{ + Minter: DefaultInitialMinter(), + Params: DefaultParams(), + } +} + +// ValidateGenesis validates the provided genesis state to ensure the +// expected invariants holds. +func ValidateGenesis(data GenesisState) error { + err := ValidateParams(data.Params) + if err != nil { + return err + } + + return ValidateMinter(data.Minter) +} diff --git a/x/mint/internal/types/keys.go b/x/mint/internal/types/keys.go new file mode 100644 index 00000000..cf395bc4 --- /dev/null +++ b/x/mint/internal/types/keys.go @@ -0,0 +1,24 @@ +package types + +// the one key to use for the keeper store +var MinterKey = []byte{0x00} + +// nolint +const ( + // module name + ModuleName = "mint" + + // default paramspace for params keeper + DefaultParamspace = ModuleName + + // StoreKey is the default store key for mint + StoreKey = ModuleName + + // QuerierRoute is the querier route for the minting store. + QuerierRoute = StoreKey + + // Query endpoints supported by the minting querier + QueryParameters = "parameters" + QueryInflation = "inflation" + QueryAnnualProvisions = "annual_provisions" +) diff --git a/x/mint/internal/types/minter.go b/x/mint/internal/types/minter.go new file mode 100644 index 00000000..3654ef72 --- /dev/null +++ b/x/mint/internal/types/minter.go @@ -0,0 +1,86 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Minter represents the minting state. +type Minter struct { + Inflation sdk.Dec `json:"inflation" yaml:"inflation"` // current annual inflation rate + AnnualProvisions sdk.Dec `json:"annual_provisions" yaml:"annual_provisions"` // current annual expected provisions +} + +// NewMinter returns a new Minter object with the given inflation and annual +// provisions values. +func NewMinter(inflation, annualProvisions sdk.Dec) Minter { + return Minter{ + Inflation: inflation, + AnnualProvisions: annualProvisions, + } +} + +// InitialMinter returns an initial Minter object with a given inflation value. +func InitialMinter(inflation sdk.Dec) Minter { + return NewMinter( + inflation, + sdk.NewDec(0), + ) +} + +// DefaultInitialMinter returns a default initial Minter object for a new chain +// which uses an inflation rate of 13%. +func DefaultInitialMinter() Minter { + return InitialMinter( + sdk.NewDecWithPrec(13, 2), + ) +} + +// validate minter +func ValidateMinter(minter Minter) error { + if minter.Inflation.IsNegative() { + return fmt.Errorf("mint parameter Inflation should be positive, is %s", + minter.Inflation.String()) + } + return nil +} + +// NextInflationRate returns the new inflation rate for the next hour. +func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) 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 + // 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%. + + // (1 - bondedRatio/GoalBonded) * InflationRateChange + inflationRateChangePerYear := sdk.OneDec(). + Sub(bondedRatio.Quo(params.GoalBonded)). + Mul(params.InflationRateChange) + inflationRateChange := inflationRateChangePerYear.Quo(sdk.NewDec(int64(params.BlocksPerYear))) + + // adjust the new annual inflation for this next cycle + inflation := m.Inflation.Add(inflationRateChange) // note inflationRateChange may be negative + if inflation.GT(params.InflationMax) { + inflation = params.InflationMax + } + if inflation.LT(params.InflationMin) { + inflation = params.InflationMin + } + + return inflation +} + +// NextAnnualProvisions returns the annual provisions based on current total +// supply and inflation rate. +func (m Minter) NextAnnualProvisions(_ Params, totalSupply sdk.Int) sdk.Dec { + return m.Inflation.MulInt(totalSupply) +} + +// BlockProvision returns 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/internal/types/minter_test.go b/x/mint/internal/types/minter_test.go new file mode 100644 index 00000000..8760a66f --- /dev/null +++ b/x/mint/internal/types/minter_test.go @@ -0,0 +1,131 @@ +package types + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestNextInflation(t *testing.T) { + minter := DefaultInitialMinter() + params := DefaultParams() + blocksPerYr := sdk.NewDec(int64(params.BlocksPerYear)) + + // Governing Mechanism: + // inflationRateChangePerYear = (1- BondedRatio/ GoalBonded) * MaxInflationRateChange + + tests := []struct { + bondedRatio, setInflation, expChange sdk.Dec + }{ + // with 0% bonded atom supply the inflation should increase by InflationRateChange + {sdk.ZeroDec(), sdk.NewDecWithPrec(7, 2), params.InflationRateChange.Quo(blocksPerYr)}, + + // 100% bonded, starting at 20% inflation and being reduced + // (1 - (1/0.67))*(0.13/8667) + {sdk.OneDec(), sdk.NewDecWithPrec(20, 2), + sdk.OneDec().Sub(sdk.OneDec().Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(blocksPerYr)}, + + // 50% bonded, starting at 10% inflation and being increased + {sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(10, 2), + sdk.OneDec().Sub(sdk.NewDecWithPrec(5, 1).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(blocksPerYr)}, + + // test 7% minimum stop (testing with 100% bonded) + {sdk.OneDec(), sdk.NewDecWithPrec(7, 2), sdk.ZeroDec()}, + {sdk.OneDec(), sdk.NewDecWithPrec(700000001, 10), sdk.NewDecWithPrec(-1, 10)}, + + // test 20% maximum stop (testing with 0% bonded) + {sdk.ZeroDec(), sdk.NewDecWithPrec(20, 2), sdk.ZeroDec()}, + {sdk.ZeroDec(), sdk.NewDecWithPrec(1999999999, 10), sdk.NewDecWithPrec(1, 10)}, + + // perfect balance shouldn't change inflation + {sdk.NewDecWithPrec(67, 2), sdk.NewDecWithPrec(15, 2), sdk.ZeroDec()}, + } + for i, tc := range tests { + minter.Inflation = tc.setInflation + + 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)) + + // run the BlockProvision function b.N times + for n := 0; n < b.N; n++ { + minter.BlockProvision(params) + } +} + +// Next inflation benchmarking +// BenchmarkNextInflation-4 1000000 1828 ns/op +func BenchmarkNextInflation(b *testing.B) { + minter := InitialMinter(sdk.NewDecWithPrec(1, 1)) + params := DefaultParams() + bondedRatio := sdk.NewDecWithPrec(1, 1) + + // run the NextInflationRate function b.N times + for n := 0; n < b.N; n++ { + minter.NextInflationRate(params, bondedRatio) + } + +} + +// Next annual provisions benchmarking +// BenchmarkNextAnnualProvisions-4 5000000 251 ns/op +func BenchmarkNextAnnualProvisions(b *testing.B) { + minter := InitialMinter(sdk.NewDecWithPrec(1, 1)) + params := DefaultParams() + totalSupply := sdk.NewInt(100000000000000) + + // run the NextAnnualProvisions function b.N times + for n := 0; n < b.N; n++ { + minter.NextAnnualProvisions(params, totalSupply) + } + +} diff --git a/x/mint/internal/types/params.go b/x/mint/internal/types/params.go new file mode 100644 index 00000000..122f88bf --- /dev/null +++ b/x/mint/internal/types/params.go @@ -0,0 +1,101 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Parameter store keys +var ( + KeyMintDenom = []byte("MintDenom") + KeyInflationRateChange = []byte("InflationRateChange") + KeyInflationMax = []byte("InflationMax") + KeyInflationMin = []byte("InflationMin") + KeyGoalBonded = []byte("GoalBonded") + KeyBlocksPerYear = []byte("BlocksPerYear") +) + +// mint parameters +type Params struct { + MintDenom string `json:"mint_denom" yaml:"mint_denom"` // type of coin to mint + InflationRateChange sdk.Dec `json:"inflation_rate_change" yaml:"inflation_rate_change"` // maximum annual change in inflation rate + InflationMax sdk.Dec `json:"inflation_max" yaml:"inflation_max"` // maximum inflation rate + InflationMin sdk.Dec `json:"inflation_min" yaml:"inflation_min"` // minimum inflation rate + GoalBonded sdk.Dec `json:"goal_bonded" yaml:"goal_bonded"` // goal of percent bonded atoms + BlocksPerYear uint64 `json:"blocks_per_year" yaml:"blocks_per_year"` // expected blocks per year +} + +// ParamTable for minting module. +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +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 +func DefaultParams() Params { + return Params{ + MintDenom: sdk.DefaultBondDenom, + InflationRateChange: sdk.NewDecWithPrec(13, 2), + 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 + } +} + +// validate params +func ValidateParams(params Params) error { + if params.GoalBonded.IsNegative() { + return fmt.Errorf("mint parameter GoalBonded should be positive, is %s ", params.GoalBonded.String()) + } + if params.GoalBonded.GT(sdk.OneDec()) { + return fmt.Errorf("mint parameter GoalBonded must be <= 1, is %s", params.GoalBonded.String()) + } + if params.InflationMax.LT(params.InflationMin) { + return fmt.Errorf("mint parameter Max inflation must be greater than or equal to min inflation") + } + if params.MintDenom == "" { + return fmt.Errorf("mint parameter MintDenom can't be an empty string") + } + return nil +} + +func (p Params) String() string { + return fmt.Sprintf(`Minting Params: + Mint Denom: %s + Inflation Rate Change: %s + Inflation Max: %s + Inflation Min: %s + Goal Bonded: %s + Blocks Per Year: %d +`, + p.MintDenom, p.InflationRateChange, p.InflationMax, + p.InflationMin, p.GoalBonded, p.BlocksPerYear, + ) +} + +// Implements params.ParamSet +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyMintDenom, Value: &p.MintDenom}, + {Key: KeyInflationRateChange, Value: &p.InflationRateChange}, + {Key: KeyInflationMax, Value: &p.InflationMax}, + {Key: KeyInflationMin, Value: &p.InflationMin}, + {Key: KeyGoalBonded, Value: &p.GoalBonded}, + {Key: KeyBlocksPerYear, Value: &p.BlocksPerYear}, + } +} diff --git a/x/mint/module.go b/x/mint/module.go new file mode 100644 index 00000000..94f12b1a --- /dev/null +++ b/x/mint/module.go @@ -0,0 +1,161 @@ +package mint + +import ( + "encoding/json" + "math/rand" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/unification-com/mainchain-cosmos/x/enterprise" + "github.com/unification-com/mainchain-cosmos/x/mint/client/cli" + "github.com/unification-com/mainchain-cosmos/x/mint/client/rest" + "github.com/unification-com/mainchain-cosmos/x/mint/simulation" + sim "github.com/cosmos/cosmos-sdk/x/simulation" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleSimulation = AppModuleSimulation{} +) + +// AppModuleBasic defines the basic application module used by the mint module. +type AppModuleBasic struct{} + +var _ module.AppModuleBasic = AppModuleBasic{} + +// Name returns the mint module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the mint module's types for the given codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {} + +// DefaultGenesis returns default genesis state as raw bytes for the mint +// module. +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the mint module. +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// RegisterRESTRoutes registers the REST routes for the mint module. +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// GetTxCmd returns no root tx command for the mint module. +func (AppModuleBasic) GetTxCmd(_ *codec.Codec) *cobra.Command { return nil } + +// GetQueryCmd returns the root query command for the mint module. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(cdc) +} + +//____________________________________________________________________________ + +// AppModuleSimulation defines the module simulation functions used by the mint module. +type AppModuleSimulation struct{} + +// RegisterStoreDecoder registers a decoder for mint module's types. +func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[StoreKey] = simulation.DecodeStore +} + +// GenerateGenesisState creates a randomized GenState of the mint module. +func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// RandomizedParams creates randomized mint param changes for the simulator. +func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { + return simulation.ParamChanges(r) +} + +//____________________________________________________________________________ + +// AppModule implements an application module for the mint module. +type AppModule struct { + AppModuleBasic + AppModuleSimulation + + keeper Keeper + enterpriseKeeper enterprise.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper, enterpriseKeeper enterprise.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + AppModuleSimulation: AppModuleSimulation{}, + keeper: keeper, + enterpriseKeeper: enterpriseKeeper, + } +} + +// Name returns the mint module's name. +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants registers the mint module invariants. +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route returns the message routing key for the mint module. +func (AppModule) Route() string { return "" } + +// NewHandler returns an sdk.Handler for the mint module. +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// QuerierRoute returns the mint module's querier route name. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// NewQuerierHandler returns the mint module sdk.Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +// InitGenesis performs genesis initialization for the mint module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the mint +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// BeginBlock returns the begin blocker for the mint module. +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + BeginBlocker(ctx, am.keeper, am.enterpriseKeeper) +} + +// EndBlock returns the end blocker for the mint module. It returns no validator +// updates. +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/mint/simulation/decoder.go b/x/mint/simulation/decoder.go new file mode 100644 index 00000000..a5acb695 --- /dev/null +++ b/x/mint/simulation/decoder.go @@ -0,0 +1,24 @@ +package simulation + +import ( + "bytes" + "fmt" + + cmn "github.com/tendermint/tendermint/libs/common" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// DecodeStore unmarshals the KVPair's Value to the corresponding mint type +func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string { + switch { + case bytes.Equal(kvA.Key, types.MinterKey): + var minterA, minterB types.Minter + cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &minterA) + cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &minterB) + return fmt.Sprintf("%v\n%v", minterA, minterB) + default: + panic(fmt.Sprintf("invalid mint key %X", kvA.Key)) + } +} diff --git a/x/mint/simulation/decoder_test.go b/x/mint/simulation/decoder_test.go new file mode 100644 index 00000000..8424a65e --- /dev/null +++ b/x/mint/simulation/decoder_test.go @@ -0,0 +1,49 @@ +package simulation + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + cmn "github.com/tendermint/tendermint/libs/common" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +func makeTestCodec() (cdc *codec.Codec) { + cdc = codec.New() + sdk.RegisterCodec(cdc) + return +} + +func TestDecodeStore(t *testing.T) { + cdc := makeTestCodec() + minter := types.NewMinter(sdk.OneDec(), sdk.NewDec(15)) + + kvPairs := cmn.KVPairs{ + cmn.KVPair{Key: types.MinterKey, Value: cdc.MustMarshalBinaryLengthPrefixed(minter)}, + cmn.KVPair{Key: []byte{0x99}, Value: []byte{0x99}}, + } + tests := []struct { + name string + expectedLog string + }{ + {"Minter", fmt.Sprintf("%v\n%v", minter, minter)}, + {"other", ""}, + } + + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + switch i { + case len(tests) - 1: + require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name) + } + }) + } +} diff --git a/x/mint/simulation/genesis.go b/x/mint/simulation/genesis.go new file mode 100644 index 00000000..4e08c3e9 --- /dev/null +++ b/x/mint/simulation/genesis.go @@ -0,0 +1,92 @@ +package simulation + +// DONTCOVER + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/codec" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" +) + +// Simulation parameter constants +const ( + Inflation = "inflation" + InflationRateChange = "inflation_rate_change" + InflationMax = "inflation_max" + InflationMin = "inflation_min" + GoalBonded = "goal_bonded" +) + +// GenInflation randomized Inflation +func GenInflation(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(r.Intn(99)), 2) +} + +// GenInflationRateChange randomized InflationRateChange +func GenInflationRateChange(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(int64(r.Intn(99)), 2) +} + +// GenInflationMax randomized InflationMax +func GenInflationMax(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(20, 2) +} + +// GenInflationMin randomized InflationMin +func GenInflationMin(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(7, 2) +} + +// GenGoalBonded randomized GoalBonded +func GenGoalBonded(r *rand.Rand) sdk.Dec { + return sdk.NewDecWithPrec(67, 2) +} + +// RandomizedGenState generates a random GenesisState for mint +func RandomizedGenState(simState *module.SimulationState) { + // minter + var inflation sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, Inflation, &inflation, simState.Rand, + func(r *rand.Rand) { inflation = GenInflation(r) }, + ) + + // params + var inflationRateChange sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, InflationRateChange, &inflationRateChange, simState.Rand, + func(r *rand.Rand) { inflationRateChange = GenInflationRateChange(r) }, + ) + + var inflationMax sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, InflationMax, &inflationMax, simState.Rand, + func(r *rand.Rand) { inflationMax = GenInflationMax(r) }, + ) + + var inflationMin sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, InflationMin, &inflationMin, simState.Rand, + func(r *rand.Rand) { inflationMin = GenInflationMin(r) }, + ) + + var goalBonded sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, GoalBonded, &goalBonded, simState.Rand, + func(r *rand.Rand) { goalBonded = GenGoalBonded(r) }, + ) + + mintDenom := sdk.DefaultBondDenom + blocksPerYear := uint64(60 * 60 * 8766 / 5) + params := types.NewParams(mintDenom, inflationRateChange, inflationMax, inflationMin, goalBonded, blocksPerYear) + + mintGenesis := types.NewGenesisState(types.InitialMinter(inflation), params) + + fmt.Printf("Selected randomly generated minting parameters:\n%s\n", codec.MustMarshalJSONIndent(simState.Cdc, mintGenesis)) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(mintGenesis) +} diff --git a/x/mint/simulation/params.go b/x/mint/simulation/params.go new file mode 100644 index 00000000..65fca2e3 --- /dev/null +++ b/x/mint/simulation/params.go @@ -0,0 +1,45 @@ +package simulation + +// DONTCOVER + +import ( + "fmt" + "math/rand" + + "github.com/unification-com/mainchain-cosmos/x/mint/internal/types" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +const ( + keyInflationRateChange = "InflationRateChange" + keyInflationMax = "InflationMax" + keyInflationMin = "InflationMin" + keyGoalBonded = "GoalBonded" +) + +// ParamChanges defines the parameters that can be modified by param change proposals +// on the simulation +func ParamChanges(r *rand.Rand) []simulation.ParamChange { + return []simulation.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, keyInflationRateChange, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenInflationRateChange(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keyInflationMax, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenInflationMax(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keyInflationMin, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenInflationMin(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keyGoalBonded, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenGoalBonded(r)) + }, + ), + } +} diff --git a/x/mint/spec/01_concepts.md b/x/mint/spec/01_concepts.md new file mode 100644 index 00000000..14b2c6ab --- /dev/null +++ b/x/mint/spec/01_concepts.md @@ -0,0 +1,22 @@ +# Concepts + +## The Minting Mechanism + +The minting mechanism was designed to: + - allow for a flexible inflation rate determined by market demand targeting a particular bonded-stake ratio + - effect a balance between market liquidity and staked supply + +In order to best determine the appropriate market rate for inflation rewards, a +moving change rate is used. The moving change rate mechanism ensures that if +the % bonded is either over or under the goal %-bonded, the inflation rate will +adjust to further incentivize or disincentivize being bonded, respectively. Setting the goal +%-bonded at less than 100% encourages the network to maintain some non-staked tokens +which should help provide some liquidity. + +It can be broken down in the following way: + - If the inflation rate is below the goal %-bonded the inflation rate will + increase until a maximum value is reached + - If the goal % bonded (67% in Cosmos-Hub) is maintained, then the inflation + rate will stay constant + - If the inflation rate is above the goal %-bonded the inflation rate will + decrease until a minimum value is reached diff --git a/x/mint/spec/02_state.md b/x/mint/spec/02_state.md new file mode 100644 index 00000000..19e6f38e --- /dev/null +++ b/x/mint/spec/02_state.md @@ -0,0 +1,31 @@ +# State + +## Minter + +The minter is a space for holding current inflation information. + + - Minter: `0x00 -> amino(minter)` + +```go +type Minter struct { + Inflation sdk.Dec // current annual inflation rate + AnnualProvisions sdk.Dec // current annual exptected provisions +} +``` + +## Params + +Minting params are held in the global params store. + + - Params: `mint/params -> amino(params)` + +```go +type Params struct { + MintDenom string // type of coin to mint + InflationRateChange sdk.Dec // maximum annual change in inflation rate + 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/x/mint/spec/03_begin_block.md b/x/mint/spec/03_begin_block.md new file mode 100644 index 00000000..427c0142 --- /dev/null +++ b/x/mint/spec/03_begin_block.md @@ -0,0 +1,50 @@ +# Begin-Block + +Minting parameters are recalculated and inflation +paid at the beginning of each block. + +## NextInflationRate + +The target annual inflation rate is recalculated each block. +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%. + +``` +NextInflationRate(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { + inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange + inflationRateChange = inflationRateChangePerYear/blocksPerYr + + // increase the new annual inflation for this next cycle + inflation += inflationRateChange + if inflation > params.InflationMax { + inflation = params.InflationMax + } + if inflation < params.InflationMin { + inflation = params.InflationMin + } + + 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. The provisions are then minted by the `mint` module's `ModuleMinterAccount` and then transferred to the `auth`'s `FeeCollector` `ModuleAccount`. + +``` +BlockProvision(params Params) sdk.Coin { + provisionAmt = AnnualProvisions/ params.BlocksPerYear + return sdk.NewCoin(params.MintDenom, provisionAmt.Truncate()) +``` diff --git a/x/mint/spec/04_params.md b/x/mint/spec/04_params.md new file mode 100644 index 00000000..c743045e --- /dev/null +++ b/x/mint/spec/04_params.md @@ -0,0 +1,12 @@ +# Parameters + +The minting module contains the following parameters: + +| Key | Type | Example | +|---------------------|-----------------|------------------------| +| MintDenom | string | "uatom" | +| InflationRateChange | string (dec) | "0.130000000000000000" | +| InflationMax | string (dec) | "0.200000000000000000" | +| InflationMin | string (dec) | "0.070000000000000000" | +| GoalBonded | string (dec) | "0.670000000000000000" | +| BlocksPerYear | string (uint64) | "6311520" | diff --git a/x/mint/spec/05_events.md b/x/mint/spec/05_events.md new file mode 100644 index 00000000..a98d8db7 --- /dev/null +++ b/x/mint/spec/05_events.md @@ -0,0 +1,12 @@ +# Events + +The minting module emits the following events: + +## BeginBlocker + +| Type | Attribute Key | Attribute Value | +|------|-------------------|--------------------| +| mint | bonded_ratio | {bondedRatio} | +| mint | inflation | {inflation} | +| mint | annual_provisions | {annualProvisions} | +| mint | amount | {amount} | diff --git a/x/mint/spec/README.md b/x/mint/spec/README.md new file mode 100644 index 00000000..b7e0ef9e --- /dev/null +++ b/x/mint/spec/README.md @@ -0,0 +1,16 @@ +# Mint Specification + +## Contents + +1. **[Concept](01_concepts.md)** +2. **[State](02_state.md)** + - [Minter](02_state.md#minter) + - [Params](02_state.md#params) +3. **[Begin-Block](03_begin_block.md)** + - [NextInflationRate](03_begin_block.md#nextinflationrate) + - [NextAnnualProvisions](03_begin_block.md#nextannualprovisions) + - [BlockProvision](03_begin_block.md#blockprovision) +4. **[Parameters](04_params.md)** +5. **[Events](05_events.md)** + - [BeginBlocker](05_events.md#beginblocker) + diff --git a/x/wrkchain/ante.go b/x/wrkchain/ante.go index e10c6222..2388a860 100644 --- a/x/wrkchain/ante.go +++ b/x/wrkchain/ante.go @@ -150,7 +150,7 @@ func checkFeePayerHasFunds(ctx sdk.Context, ak auth.AccountKeeper, ek enterprise potentialCoins := coins //get any locked enterprise UND - lockedUnd := ek.GetLockedUnd(ctx, feePayer).Amount + lockedUnd := ek.GetLockedUndForAccount(ctx, feePayer).Amount lockedUndCoins := sdk.NewCoins(lockedUnd) // include any locked UND in potential coins. We need to do this because if these checks pass,