Issuance module (#599)

* wip: issuance module

* add keeper and module methods

* add begin blocker

* add client

* update events

* add simulations

* ignore v0.8 migration tests for now

* ignore migration tests in ci

* add test suite

* update spec to match implementation details

* add unblock method

* address review comments

* fix typos
This commit is contained in:
Kevin Davis 2020-08-17 13:09:02 -04:00 committed by GitHub
parent 790753f156
commit e14466547d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3260 additions and 5 deletions

View File

@ -71,7 +71,7 @@ jobs:
export VERSION="$(git describe --tags --long | sed 's/v\(.*\)/\1/')"
export GO111MODULE=on
mkdir -p /tmp/logs /tmp/workspace/profiles
for pkg in $(go list ./... | grep -v '/simulation' | circleci tests split); do
for pkg in $(go list ./... | grep -v 'simulation\|migrate\|contrib' | circleci tests split); do
id=$(echo "$pkg" | sed 's|[/.]|_|g')
go test -mod=readonly -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic -tags='ledger test_ledger_mock' "$pkg" | tee "/tmp/logs/$id-$RANDOM.log"
done

View File

@ -171,7 +171,7 @@ test-basic: test
@go test ./app -run TestAppStateDeterminism -Enabled -Commit -NumBlocks=5 -BlockSize=200 -Seed 4 -v -timeout 2m
test:
@go test ./...
@go test $$(go list ./... | grep -v 'migrate\|contrib')
test-rest:
rest_test/./run_all_tests_from_make.sh

View File

@ -38,6 +38,7 @@ import (
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -80,6 +81,7 @@ var (
bep3.AppModuleBasic{},
kavadist.AppModuleBasic{},
incentive.AppModuleBasic{},
issuance.AppModuleBasic{},
)
// module account permissions
@ -97,6 +99,7 @@ var (
cdp.SavingsRateMacc: {supply.Minter},
bep3.ModuleName: nil,
kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
}
// module accounts that are allowed to receive tokens
@ -140,6 +143,7 @@ type App struct {
bep3Keeper bep3.Keeper
kavadistKeeper kavadist.Keeper
incentiveKeeper incentive.Keeper
issuanceKeeper issuance.Keeper
// the module manager
mm *module.Manager
@ -164,7 +168,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, committee.StoreKey,
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -193,6 +197,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -350,6 +355,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.cdpKeeper,
app.accountKeeper,
)
app.issuanceKeeper = issuance.NewKeeper(
app.cdc,
keys[issuance.StoreKey],
issuanceSubspace,
app.accountKeeper,
app.supplyKeeper,
)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -379,6 +391,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
@ -389,7 +402,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.mm.SetOrderBeginBlockers(
upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName,
validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName,
bep3.ModuleName, incentive.ModuleName, committee.ModuleName,
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
@ -400,7 +413,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
@ -430,6 +443,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
)
app.sm.RegisterStoreDecoders()

View File

@ -13,5 +13,9 @@ const (
DefaultWeightMsgUpdatePrices int = 20
DefaultWeightMsgCdp int = 20
DefaultWeightMsgClaimReward int = 20
DefaultWeightMsgIssue int = 20
DefaultWeightMsgRedeem int = 20
DefaultWeightMsgBlock int = 20
DefaultWeightMsgPause int = 20
OpWeightSubmitCommitteeChangeProposal int = 20
)

View File

@ -35,6 +35,7 @@ import (
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -84,6 +85,7 @@ func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keep
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {

17
x/issuance/abci.go Normal file
View File

@ -0,0 +1,17 @@
package issuance
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/issuance/keeper"
)
// BeginBlocker iterates over each asset and seizes coins from blocked addresses by returning them to the asset owner
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
params := k.GetParams(ctx)
for _, asset := range params.Assets {
err := k.SeizeCoinsFromBlockedAddresses(ctx, asset.Denom)
if err != nil {
panic(err)
}
}
}

81
x/issuance/alias.go Normal file
View File

@ -0,0 +1,81 @@
package issuance
import (
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/issuance/keeper
// ALIASGEN: github.com/kava-labs/kava/x/issuance/types
const (
EventTypeIssue = types.EventTypeIssue
EventTypeRedeem = types.EventTypeRedeem
EventTypeBlock = types.EventTypeBlock
EventTypeUnblock = types.EventTypeUnblock
EventTypePause = types.EventTypePause
EventTypeSeize = types.EventTypeSeize
AttributeValueCategory = types.AttributeValueCategory
AttributeKeyDenom = types.AttributeKeyDenom
AttributeKeyIssueAmount = types.AttributeKeyIssueAmount
AttributeKeyRedeemAmount = types.AttributeKeyRedeemAmount
AttributeKeyBlock = types.AttributeKeyBlock
AttributeKeyUnblock = types.AttributeKeyUnblock
AttributeKeyAddress = types.AttributeKeyAddress
AttributeKeyPauseStatus = types.AttributeKeyPauseStatus
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QuerierRoute = types.QuerierRoute
QueryGetParams = types.QueryGetParams
QueryGetAsset = types.QueryGetAsset
)
var (
// functions aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
NewMsgIssueTokens = types.NewMsgIssueTokens
NewMsgRedeemTokens = types.NewMsgRedeemTokens
NewMsgBlockAddress = types.NewMsgBlockAddress
NewMsgUnblockAddress = types.NewMsgUnblockAddress
NewMsgSetPauseStatus = types.NewMsgSetPauseStatus
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewAsset = types.NewAsset
// variable aliases
ModuleCdc = types.ModuleCdc
ErrAssetNotFound = types.ErrAssetNotFound
ErrNotAuthorized = types.ErrNotAuthorized
ErrAssetPaused = types.ErrAssetPaused
ErrAccountBlocked = types.ErrAccountBlocked
ErrAccountAlreadyBlocked = types.ErrAccountAlreadyBlocked
ErrAccountAlreadyUnblocked = types.ErrAccountAlreadyUnblocked
ErrIssueToModuleAccount = types.ErrIssueToModuleAccount
KeyAssets = types.KeyAssets
DefaultAssets = types.DefaultAssets
ModuleAccountName = types.ModuleAccountName
)
type (
Keeper = keeper.Keeper
GenesisState = types.GenesisState
MsgIssueTokens = types.MsgIssueTokens
MsgRedeemTokens = types.MsgRedeemTokens
MsgBlockAddress = types.MsgBlockAddress
MsgUnblockAddress = types.MsgUnblockAddress
MsgSetPauseStatus = types.MsgSetPauseStatus
Params = types.Params
Asset = types.Asset
Assets = types.Assets
QueryAssetParams = types.QueryAssetParams
)

View File

@ -0,0 +1,55 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/issuance/types"
)
// GetQueryCmd returns the cli query commands for the issuance module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
issuanceQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
}
issuanceQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
)...)
return issuanceQueryCmd
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: fmt.Sprintf("get the %s module parameters", types.ModuleName),
Long: "Get the current global issuance module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var params types.Params
if err := cdc.UnmarshalJSON(res, &params); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
return cliCtx.PrintOutput(params)
},
}
}

192
x/issuance/client/cli/tx.go Normal file
View File

@ -0,0 +1,192 @@
package cli
import (
"bufio"
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/issuance/types"
)
// GetTxCmd returns the transaction cli commands for the issuance module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
issuanceTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "transaction commands for the issuance module",
}
issuanceTxCmd.AddCommand(flags.PostCommands(
getCmdIssueTokens(cdc),
getCmdRedeemTokens(cdc),
getCmdBlockAddress(cdc),
getCmdUnblockAddress(cdc),
getCmdPauseAsset(cdc),
)...)
return issuanceTxCmd
}
func getCmdIssueTokens(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "issue [tokens] [receiver]",
Short: "issue new tokens to the receiver address",
Long: "The asset owner issues new tokens that will be credited to the receiver address",
Example: fmt.Sprintf(`$ %s tx %s issue 20000000usdtoken kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw
`, version.ClientName, types.ModuleName),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
tokens, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
receiver, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgIssueTokens(cliCtx.GetFromAddress(), tokens, receiver)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdRedeemTokens(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "redeem [tokens]",
Short: "redeem tokens",
Long: "The asset owner redeems (burns) tokens, removing them from the circulating supply",
Example: fmt.Sprintf(`$ %s tx %s redeem 20000000usdtoken
`, version.ClientName, types.ModuleName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
tokens, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
msg := types.NewMsgRedeemTokens(cliCtx.GetFromAddress(), tokens)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdBlockAddress(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "block [address] [denom]",
Short: "block an address for the input denom",
Long: "The asset owner blocks an address from holding coins of that denomination. Any tokens of the input denomination held by the address will be sent to the owner address",
Example: fmt.Sprintf(`$ %s tx %s block kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw usdtoken
`, version.ClientName, types.ModuleName),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
address, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
err = sdk.ValidateDenom(args[1])
if err != nil {
return err
}
msg := types.NewMsgBlockAddress(cliCtx.GetFromAddress(), args[1], address)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdUnblockAddress(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "unblock [address] [denom]",
Short: "unblock an address for the input denom",
Long: "The asset owner unblocks an address from holding coins of that denomination.",
Example: fmt.Sprintf(`$ %s tx %s unblock kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw usdtoken
`, version.ClientName, types.ModuleName),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
address, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
err = sdk.ValidateDenom(args[1])
if err != nil {
return err
}
msg := types.NewMsgUnblockAddress(cliCtx.GetFromAddress(), args[1], address)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdPauseAsset(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "set-pause-status [denom] [status]",
Short: "pause or unpause an asset",
Long: "The asset owner pauses or unpauses the input asset, halting new issuance and redemption",
Example: fmt.Sprintf(`$ %s tx %s pause usdtoken true
`, version.ClientName, types.ModuleName),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
err := sdk.ValidateDenom(args[0])
if err != nil {
return err
}
var status bool
if args[1] == "true" {
status = true
} else if args[1] == "false" {
status = false
} else {
return fmt.Errorf(fmt.Sprintf("status must be true or false, got %s", args[1]))
}
msg := types.NewMsgSetPauseStatus(cliCtx.GetFromAddress(), args[0], status)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,34 @@
package rest
import (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/issuance/types"
)
// define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), getParamsHandlerFn(cliCtx)).Methods("GET")
}
func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetParams), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,49 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}
// PostIssueReq defines the properties of an issue token request's body
type PostIssueReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
}
// PostRedeemReq defines the properties of a redeem token request's body
type PostRedeemReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
}
// PostBlockAddressReq defines the properties of a block address request's body
type PostBlockAddressReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
Denom string `json:"denom" yaml:"denom"`
}
// PostUnblockAddressReq defines the properties of a unblock address request's body
type PostUnblockAddressReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
Denom string `json:"denom" yaml:"denom"`
}
// PostPauseReq defines the properties of a pause request's body
type PostPauseReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Denom string `json:"denom" yaml:"denom"`
Status bool `json:"status" yaml:"status"`
}

View File

@ -0,0 +1,181 @@
package rest
import (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/issuance/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/issue", types.ModuleName), postIssueTokensHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/redeem", types.ModuleName), postRedeemTokensHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/block", types.ModuleName), postBlockAddressHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/unblock", types.ModuleName), postUnblockAddressHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/pause", types.ModuleName), postPauseHandlerFn(cliCtx)).Methods("POST")
}
func postIssueTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostIssueReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgIssueTokens(
fromAddr,
requestBody.Tokens,
requestBody.Receiver,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postRedeemTokensHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostRedeemReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgRedeemTokens(
fromAddr,
requestBody.Tokens,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postBlockAddressHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostBlockAddressReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgBlockAddress(
fromAddr,
requestBody.Denom,
requestBody.Address,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postUnblockAddressHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostUnblockAddressReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgUnblockAddress(
fromAddr,
requestBody.Denom,
requestBody.Address,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postPauseHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostPauseReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgSetPauseStatus(
fromAddr,
requestBody.Denom,
requestBody.Status,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}

28
x/issuance/genesis.go Normal file
View File

@ -0,0 +1,28 @@
package issuance
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, gs types.GenesisState) {
k.SetParams(ctx, gs.Params)
// check if the module account exists
moduleAcc := supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleAccountName))
}
}
// ExportGenesis export genesis state for issuance module
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
params := k.GetParams(ctx)
return types.NewGenesisState(params)
}

116
x/issuance/handler.go Normal file
View File

@ -0,0 +1,116 @@
package issuance
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
// NewHandler creates an sdk.Handler for issuance messages
func NewHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case types.MsgIssueTokens:
return handleMsgIssueTokens(ctx, k, msg)
case types.MsgRedeemTokens:
return handleMsgRedeemTokens(ctx, k, msg)
case types.MsgBlockAddress:
return handleMsgBlockAddress(ctx, k, msg)
case types.MsgUnblockAddress:
return handleMsgUnblockAddress(ctx, k, msg)
case types.MsgSetPauseStatus:
return handleMsgSetPauseStatus(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg)
}
}
}
func handleMsgIssueTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgIssueTokens) (*sdk.Result, error) {
err := k.IssueTokens(ctx, msg.Tokens, msg.Sender, msg.Receiver)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgRedeemTokens(ctx sdk.Context, k keeper.Keeper, msg types.MsgRedeemTokens) (*sdk.Result, error) {
err := k.RedeemTokens(ctx, msg.Tokens, msg.Sender)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgBlockAddress(ctx sdk.Context, k keeper.Keeper, msg types.MsgBlockAddress) (*sdk.Result, error) {
err := k.BlockAddress(ctx, msg.Denom, msg.Sender, msg.Address)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgUnblockAddress(ctx sdk.Context, k keeper.Keeper, msg types.MsgUnblockAddress) (*sdk.Result, error) {
err := k.UnblockAddress(ctx, msg.Denom, msg.Sender, msg.Address)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgSetPauseStatus(ctx sdk.Context, k keeper.Keeper, msg types.MsgSetPauseStatus) (*sdk.Result, error) {
err := k.SetPauseStatus(ctx, msg.Sender, msg.Denom, msg.Status)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}

View File

@ -0,0 +1,206 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/issuance/types"
)
// IssueTokens mints new tokens and sends them to the receiver address
func (k Keeper) IssueTokens(ctx sdk.Context, tokens sdk.Coin, owner, receiver sdk.AccAddress) error {
asset, found := k.GetAsset(ctx, tokens.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", tokens.Denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
if asset.Paused {
return sdkerrors.Wrapf(types.ErrAssetPaused, "denom: %s", tokens.Denom)
}
blocked, _ := k.checkBlockedAddress(ctx, asset, receiver)
if blocked {
return sdkerrors.Wrapf(types.ErrAccountBlocked, "address: %s", receiver)
}
acc := k.accountKeeper.GetAccount(ctx, receiver)
_, ok := acc.(supplyexported.ModuleAccountI)
if ok {
return sdkerrors.Wrapf(types.ErrIssueToModuleAccount, "address: %s", receiver)
}
// mint new tokens
err := k.supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(tokens))
if err != nil {
return err
}
// send to receiver
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, receiver, sdk.NewCoins(tokens))
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeIssue,
sdk.NewAttribute(types.AttributeKeyIssueAmount, tokens.String()),
),
)
return nil
}
// RedeemTokens sends tokens from the owner address to the module account and burns them
func (k Keeper) RedeemTokens(ctx sdk.Context, tokens sdk.Coin, owner sdk.AccAddress) error {
asset, found := k.GetAsset(ctx, tokens.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", tokens.Denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
if asset.Paused {
return sdkerrors.Wrapf(types.ErrAssetPaused, "denom: %s", tokens.Denom)
}
coins := sdk.NewCoins(tokens)
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleAccountName, coins)
if err != nil {
return err
}
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleAccountName, coins)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeRedeem,
sdk.NewAttribute(types.AttributeKeyRedeemAmount, tokens.String()),
),
)
return nil
}
// BlockAddress adds an address to the blocked list
func (k Keeper) BlockAddress(ctx sdk.Context, denom string, owner, blockedAddress sdk.AccAddress) error {
asset, found := k.GetAsset(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
blocked, _ := k.checkBlockedAddress(ctx, asset, blockedAddress)
if blocked {
return sdkerrors.Wrapf(types.ErrAccountAlreadyBlocked, "address: %s", blockedAddress)
}
asset.BlockedAddresses = append(asset.BlockedAddresses, blockedAddress)
k.SetAsset(ctx, asset)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeBlock,
sdk.NewAttribute(types.AttributeKeyBlock, blockedAddress.String()),
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
),
)
return nil
}
// UnblockAddress removes an address from the blocked list
func (k Keeper) UnblockAddress(ctx sdk.Context, denom string, owner, addr sdk.AccAddress) error {
asset, found := k.GetAsset(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
blocked, i := k.checkBlockedAddress(ctx, asset, addr)
if !blocked {
if blocked {
return sdkerrors.Wrapf(types.ErrAccountAlreadyUnblocked, "address: %s", addr)
}
}
blockedAddrs := k.removeBlockedAddress(ctx, asset.BlockedAddresses, i)
asset.BlockedAddresses = blockedAddrs
k.SetAsset(ctx, asset)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeUnblock,
sdk.NewAttribute(types.AttributeKeyUnblock, addr.String()),
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
),
)
return nil
}
// SetPauseStatus pauses/un-pauses an asset
func (k Keeper) SetPauseStatus(ctx sdk.Context, owner sdk.AccAddress, denom string, status bool) error {
asset, found := k.GetAsset(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
if asset.Paused == status {
return nil
}
asset.Paused = !asset.Paused
k.SetAsset(ctx, asset)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypePause,
sdk.NewAttribute(types.AttributeKeyPauseStatus, fmt.Sprintf("%t", status)),
sdk.NewAttribute(types.AttributeKeyDenom, asset.Denom),
),
)
return nil
}
// SeizeCoinsFromBlockedAddresses checks blocked addresses for coins of the input denom and transfers them to the owner account
func (k Keeper) SeizeCoinsFromBlockedAddresses(ctx sdk.Context, denom string) error {
asset, found := k.GetAsset(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
for _, address := range asset.BlockedAddresses {
account := k.accountKeeper.GetAccount(ctx, address)
coinsAmount := account.GetCoins().AmountOf(denom)
if !coinsAmount.IsPositive() {
continue
}
coins := sdk.NewCoins(sdk.NewCoin(denom, coinsAmount))
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, address, types.ModuleAccountName, coins)
if err != nil {
return err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, asset.Owner, coins)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSeize,
sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()),
sdk.NewAttribute(types.AttributeKeyAddress, address.String()),
),
)
}
return nil
}
func (k Keeper) checkBlockedAddress(ctx sdk.Context, asset types.Asset, checkAddress sdk.AccAddress) (bool, int) {
for i, address := range asset.BlockedAddresses {
if address.Equals(checkAddress) {
return true, i
}
}
return false, 0
}
func (k Keeper) removeBlockedAddress(ctx sdk.Context, blockedAddrs []sdk.AccAddress, i int) []sdk.AccAddress {
blockedAddrs[len(blockedAddrs)-1], blockedAddrs[i] = blockedAddrs[i], blockedAddrs[len(blockedAddrs)-1]
return blockedAddrs[:len(blockedAddrs)-1]
}

View File

@ -0,0 +1,671 @@
package keeper_test
import (
"strings"
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
// Test suite used for all keeper tests
type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
modAccount sdk.AccAddress
}
// The default state used by each test
func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIssuanceKeeper()
modAccount, err := sdk.AccAddressFromBech32("kava1cj7njkw2g9fqx4e768zc75dp9sks8u9znxrf0w")
suite.Require().NoError(err)
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.addrs = addrs
suite.modAccount = modAccount
}
func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.app.GetAccountKeeper()
return ak.GetAccount(suite.ctx, addr)
}
func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
sk := suite.app.GetSupplyKeeper()
return sk.GetModuleAccount(suite.ctx, name)
}
func (suite *KeeperTestSuite) TestGetSetParams() {
params := suite.keeper.GetParams(suite.ctx)
suite.Require().Equal(types.Params{Assets: types.Assets(nil)}, params)
asset := types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false)
params = types.NewParams(types.Assets{asset})
suite.keeper.SetParams(suite.ctx, params)
newParams := suite.keeper.GetParams(suite.ctx)
suite.Require().Equal(params, newParams)
}
func (suite *KeeperTestSuite) TestIssueTokens() {
type args struct {
assets types.Assets
sender sdk.AccAddress
tokens sdk.Coin
receiver sdk.AccAddress
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid issuance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.addrs[2],
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"non-owner issuance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[2],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.addrs[3],
},
errArgs{
expectPass: false,
contains: "account not authorized",
},
},
{
"invalid denom",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("othertoken", sdk.NewInt(100000)),
receiver: suite.addrs[2],
},
errArgs{
expectPass: false,
contains: "no asset with input denom found",
},
},
{
"issue to blocked address",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "account is blocked",
},
},
{
"issue to module account",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.modAccount,
},
errArgs{
expectPass: false,
contains: "cannot issue tokens to module account",
},
},
{
"paused issuance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "asset is paused",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
err := suite.keeper.IssueTokens(suite.ctx, tc.args.tokens, tc.args.sender, tc.args.receiver)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
receiverAccount := suite.getAccount(tc.args.receiver)
suite.Require().Equal(sdk.NewCoins(tc.args.tokens), receiverAccount.GetCoins())
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestRedeemTokens() {
type args struct {
assets types.Assets
sender sdk.AccAddress
initialTokens sdk.Coin
redeemTokens sdk.Coin
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid redemption",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid denom redemption",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
redeemTokens: sdk.NewCoin("othertoken", sdk.NewInt(100000)),
},
errArgs{
expectPass: false,
contains: "",
},
},
{
"non-owner redemption",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[2],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
},
errArgs{
expectPass: false,
contains: "account not authorized",
},
},
{
"paused redemption",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
},
errArgs{
expectPass: false,
contains: "asset is paused",
},
},
{
"redeem amount greater than balance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
redeemTokens: sdk.NewCoin("usdtoken", sdk.NewInt(200000)),
},
errArgs{
expectPass: false,
contains: "insufficient funds",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, types.ModuleAccountName, sdk.NewCoins(tc.args.initialTokens))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleAccountName, tc.args.sender, sdk.NewCoins(tc.args.initialTokens))
suite.Require().NoError(err)
err = suite.keeper.RedeemTokens(suite.ctx, tc.args.redeemTokens, tc.args.sender)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
initialSupply := sdk.NewCoins(tc.args.redeemTokens)
moduleAccount := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(initialSupply.Sub(sdk.NewCoins(tc.args.redeemTokens)), moduleAccount.GetCoins())
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestBlockAddress() {
type args struct {
assets types.Assets
sender sdk.AccAddress
blockedAddr sdk.AccAddress
denom string
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
denom: "usdtoken",
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"non-owner block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[2],
blockedAddr: suite.addrs[1],
denom: "usdtoken",
},
errArgs{
expectPass: false,
contains: "account not authorized",
},
},
{
"invalid denom block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
denom: "othertoken",
},
errArgs{
expectPass: false,
contains: "no asset with input denom found",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
err := suite.keeper.BlockAddress(suite.ctx, tc.args.denom, tc.args.sender, tc.args.blockedAddr)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
blocked := false
suite.Require().True(found)
for _, blockedAddr := range asset.BlockedAddresses {
if blockedAddr.Equals(tc.args.blockedAddr) {
blocked = true
}
}
suite.Require().True(blocked)
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestUnblockAddress() {
type args struct {
assets types.Assets
sender sdk.AccAddress
blockedAddr sdk.AccAddress
denom string
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid unblock",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
denom: "usdtoken",
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"non-owner unblock",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[2],
blockedAddr: suite.addrs[1],
denom: "usdtoken",
},
errArgs{
expectPass: false,
contains: "account not authorized",
},
},
{
"invalid denom block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
denom: "othertoken",
},
errArgs{
expectPass: false,
contains: "no asset with input denom found",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
err := suite.keeper.UnblockAddress(suite.ctx, tc.args.denom, tc.args.sender, tc.args.blockedAddr)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
blocked := false
suite.Require().True(found)
for _, blockedAddr := range asset.BlockedAddresses {
if blockedAddr.Equals(tc.args.blockedAddr) {
blocked = true
}
}
suite.Require().False(blocked)
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestChangePauseStatus() {
type args struct {
assets types.Assets
sender sdk.AccAddress
startStatus bool
endStatus bool
denom string
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[0],
startStatus: false,
endStatus: true,
denom: "usdtoken",
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid unpause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, true),
},
sender: suite.addrs[0],
startStatus: true,
endStatus: false,
denom: "usdtoken",
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"non-owner pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[2],
startStatus: false,
endStatus: true,
denom: "usdtoken",
},
errArgs{
expectPass: false,
contains: "account not authorized",
},
},
{
"invalid denom pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
sender: suite.addrs[0],
startStatus: true,
endStatus: false,
denom: "othertoken",
},
errArgs{
expectPass: false,
contains: "no asset with input denom found",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
err := suite.keeper.SetPauseStatus(suite.ctx, tc.args.sender, tc.args.denom, tc.args.endStatus)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
suite.Require().True(found)
suite.Require().Equal(tc.args.endStatus, asset.Paused)
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestSeizeCoinsFromBlockedAddress() {
type args struct {
assets types.Assets
initialCoins sdk.Coin
blockedAddrs []sdk.AccAddress
denom string
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid seize",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
denom: "usdtoken",
blockedAddrs: []sdk.AccAddress{suite.addrs[1], suite.addrs[2]},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid denom seize",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
},
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
denom: "othertoken",
blockedAddrs: []sdk.AccAddress{suite.addrs[1], suite.addrs[2]},
},
errArgs{
expectPass: false,
contains: "no asset with input denom found",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
assetsWithBlockedAddrs := types.Assets{}
for _, asset := range tc.args.assets {
asset.BlockedAddresses = tc.args.blockedAddrs
assetsWithBlockedAddrs = append(assetsWithBlockedAddrs, asset)
}
params := types.NewParams(assetsWithBlockedAddrs)
suite.keeper.SetParams(suite.ctx, params)
sk := suite.app.GetSupplyKeeper()
for _, addr := range tc.args.blockedAddrs {
err := sk.MintCoins(suite.ctx, types.ModuleAccountName, sdk.NewCoins(tc.args.initialCoins))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleAccountName, addr, sdk.NewCoins(tc.args.initialCoins))
}
err := suite.keeper.SeizeCoinsFromBlockedAddresses(suite.ctx, tc.args.denom)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
asset, found := suite.keeper.GetAsset(suite.ctx, tc.args.denom)
suite.Require().True(found)
ownerAccount := suite.getAccount(asset.Owner)
ownerCoinAmount := tc.args.initialCoins.Amount.Mul(sdk.NewInt(int64(len(tc.args.blockedAddrs))))
suite.Require().Equal(sdk.NewCoins(sdk.NewCoin(tc.args.denom, ownerCoinAmount)), ownerAccount.GetCoins())
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

View File

@ -0,0 +1,33 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/issuance/types"
)
// Keeper keeper for the issuance module
type Keeper struct {
key sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
}
// NewKeeper returns a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper) Keeper {
if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
key: key,
cdc: cdc,
paramSubspace: paramstore,
accountKeeper: ak,
supplyKeeper: sk,
}
}

View File

@ -0,0 +1,41 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/issuance/types"
)
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.Params
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetAsset returns an asset from the params and a boolean for if it was found
func (k Keeper) GetAsset(ctx sdk.Context, denom string) (types.Asset, bool) {
params := k.GetParams(ctx)
for _, asset := range params.Assets {
if asset.Denom == denom {
return asset, true
}
}
return types.Asset{}, false
}
// SetAsset sets an asset in the params
func (k Keeper) SetAsset(ctx sdk.Context, asset types.Asset) {
params := k.GetParams(ctx)
for i := range params.Assets {
if params.Assets[i].Denom == asset.Denom {
params.Assets[i] = asset
}
}
k.SetParams(ctx, params)
}

View File

@ -0,0 +1,36 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/issuance/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(k Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetParams:
return queryGetParams(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
// query params in the store
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
// Get params
params := k.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(k.cdc, params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -0,0 +1,19 @@
package keeper_test
import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
func (suite *KeeperTestSuite) TestQuerierGetParams() {
querier := keeper.NewQuerier(suite.keeper)
bz, err := querier(suite.ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Require().NoError(err)
suite.NotNil(bz)
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
suite.Require().Equal(types.Params{Assets: types.Assets(nil)}, p)
}

170
x/issuance/module.go Normal file
View File

@ -0,0 +1,170 @@
package issuance
import (
"encoding/json"
"math/rand"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"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"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/issuance/client/cli"
"github.com/kava-labs/kava/x/issuance/client/rest"
"github.com/kava-labs/kava/x/issuance/simulation"
"github.com/kava-labs/kava/x/issuance/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers REST routes for the issuance module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the issuance module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns no root query command for the issuance module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
//____________________________________________________________________________
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, accountKeeper types.AccountKeeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
accountKeeper: accountKeeper,
supplyKeeper: supplyKeeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler returns no sdk.Querier.
func (am AppModule) NewQuerierHandler() sdk.Querier {
return nil
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
//____________________________________________________________________________
// GenerateGenesisState creates a randomized GenState of the issuance module
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// ProposalContents doesn't return any content functions for governance proposals.
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
}
// RandomizedParams returns nil because issuance has no params.
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return simulation.ParamChanges(r)
}
// RegisterStoreDecoder registers a decoder for issuance module's types
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// WeightedOperations returns the all the issuance module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
}

View File

@ -0,0 +1,16 @@
package simulation
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/issuance/types"
"github.com/tendermint/tendermint/libs/kv"
)
// DecodeStore the issuance module has no store keys -- all state is stored in params
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}

View File

@ -0,0 +1,58 @@
package simulation
import (
"fmt"
"math/rand"
"strings"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/issuance/types"
)
var (
accs []simulation.Account
)
// RandomizedGenState generates a random GenesisState for the module
func RandomizedGenState(simState *module.SimulationState) {
accs = simState.Accounts
params := randomizedParams(simState.Rand)
gs := types.NewGenesisState(params)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, gs))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(gs)
}
func randomizedParams(r *rand.Rand) types.Params {
assets := randomizedAssets(r)
return types.NewParams(assets)
}
func randomizedAssets(r *rand.Rand) types.Assets {
randomAssets := types.Assets{}
numAssets := Max(1, r.Intn(5))
for i := 0; i < numAssets; i++ {
denom := strings.ToLower(simulation.RandStringOfLength(r, (r.Intn(3) + 3)))
owner := randomOwner(r)
paused := r.Intn(2) == 0
randomAsset := types.NewAsset(owner.Address, denom, []sdk.AccAddress{}, paused)
randomAssets = append(randomAssets, randomAsset)
}
return randomAssets
}
func randomOwner(r *rand.Rand) simulation.Account {
acc, _ := simulation.RandomAcc(r, accs)
return acc
}
// Max return max of two ints
func Max(x, y int) int {
if x > y {
return x
}
return y
}

View File

@ -0,0 +1,305 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
simappparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/issuance/keeper"
"github.com/kava-labs/kava/x/issuance/types"
)
// Simulation operation weights constants
const (
OpWeightMsgIssue = "op_weight_msg_issue"
OpWeightMsgRedeem = "op_weight_msg_redeem"
OpWeightMsgBlock = "op_weight_msg_block"
OpWeightMsgPause = "op_weight_msg_pause"
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
var (
weightMsgIssue int
weightMsgReedem int
weightMsgBlock int
weightMsgPause int
)
appParams.GetOrGenerate(cdc, OpWeightMsgIssue, &weightMsgIssue, nil,
func(_ *rand.Rand) {
weightMsgIssue = simappparams.DefaultWeightMsgIssue
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgRedeem, &weightMsgReedem, nil,
func(_ *rand.Rand) {
weightMsgReedem = simappparams.DefaultWeightMsgRedeem
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgBlock, &weightMsgBlock, nil,
func(_ *rand.Rand) {
weightMsgBlock = simappparams.DefaultWeightMsgBlock
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgPause, &weightMsgPause, nil,
func(_ *rand.Rand) {
weightMsgPause = simappparams.DefaultWeightMsgPause
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgIssue,
SimulateMsgIssueTokens(ak, k),
),
simulation.NewWeightedOperation(
weightMsgReedem,
SimulateMsgRedeemTokens(ak, k),
),
simulation.NewWeightedOperation(
weightMsgBlock,
SimulateMsgBlockAddress(ak, k),
),
simulation.NewWeightedOperation(
weightMsgPause,
SimulateMsgPause(ak, k),
),
}
}
// SimulateMsgIssueTokens generates a MsgIssueTokens with random values
func SimulateMsgIssueTokens(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// shuffle the assets and get a random one
assets := k.GetParams(ctx).Assets
r.Shuffle(len(assets), func(i, j int) {
assets[i], assets[j] = assets[j], assets[i]
})
asset := assets[0]
if asset.Paused {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
// make sure owner account exists
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
}
// issue new tokens to the owner 50% of the time so we have funds to redeem
ownerAcc := ak.GetAccount(ctx, asset.Owner)
recipient := ownerAcc
if r.Intn(2) == 0 {
simAccount, _ := simulation.RandomAcc(r, accs)
recipient = ak.GetAccount(ctx, simAccount.Address)
}
if recipient == nil {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
for _, blockedAddr := range asset.BlockedAddresses {
if recipient.GetAddress().Equals(blockedAddr) {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
}
randomAmount := simulation.RandIntBetween(r, 10000000, 1000000000000)
msg := types.NewMsgIssueTokens(asset.Owner, sdk.NewCoin(asset.Denom, sdk.NewInt(int64(randomAmount))), recipient.GetAddress())
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{ownerAcc.GetAccountNumber()},
[]uint64{ownerAcc.GetSequence()},
ownerSimAcc.PrivKey,
)
_, _, err = app.Deliver(tx)
if err != nil {
fmt.Printf("error on issue %s\n%s\n", msg, asset)
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
}
// SimulateMsgRedeemTokens generates a MsgRedeemTokens with random values
func SimulateMsgRedeemTokens(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// shuffle the assets and get a random one
assets := k.GetParams(ctx).Assets
r.Shuffle(len(assets), func(i, j int) {
assets[i], assets[j] = assets[j], assets[i]
})
asset := assets[0]
if asset.Paused {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
// make sure owner account exists
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
}
ownerAcc := ak.GetAccount(ctx, asset.Owner)
spendableCoinAmount := ownerAcc.SpendableCoins(ctx.BlockTime()).AmountOf(asset.Denom)
if spendableCoinAmount.IsZero() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
var redeemAmount sdk.Int
if spendableCoinAmount.Equal(sdk.OneInt()) {
redeemAmount = sdk.OneInt()
} else {
redeemAmount = sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoinAmount.Int64()))))
}
msg := types.NewMsgRedeemTokens(asset.Owner, sdk.NewCoin(asset.Denom, redeemAmount))
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime()).Sub(sdk.NewCoins(sdk.NewCoin(asset.Denom, redeemAmount)))
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
if err != nil {
fmt.Printf("%s\n", msg)
return simulation.NoOpMsg(types.ModuleName), nil, err
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{ownerAcc.GetAccountNumber()},
[]uint64{ownerAcc.GetSequence()},
ownerSimAcc.PrivKey,
)
_, _, err = app.Deliver(tx)
if err != nil {
fmt.Printf("error on redeem %s\n%s\n", msg, asset)
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
}
// SimulateMsgBlockAddress generates a MsgBlockAddress with random values
func SimulateMsgBlockAddress(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// shuffle the assets and get a random one
assets := k.GetParams(ctx).Assets
r.Shuffle(len(assets), func(i, j int) {
assets[i], assets[j] = assets[j], assets[i]
})
asset := assets[0]
// make sure owner account exists
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
}
ownerAcc := ak.GetAccount(ctx, asset.Owner)
// find an account to block
simAccount, _ := simulation.RandomAcc(r, accs)
blockedAccount := ak.GetAccount(ctx, simAccount.Address)
if blockedAccount.GetAddress().Equals(asset.Owner) {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
for _, blockedAddr := range asset.BlockedAddresses {
if blockedAccount.GetAddress().Equals(blockedAddr) {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
}
msg := types.NewMsgBlockAddress(asset.Owner, asset.Denom, blockedAccount.GetAddress())
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas*2,
chainID,
[]uint64{ownerAcc.GetAccountNumber()},
[]uint64{ownerAcc.GetSequence()},
ownerSimAcc.PrivKey,
)
_, _, err = app.Deliver(tx)
if err != nil {
fmt.Printf("error on block %s\n%s\n", msg, asset)
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
}
// SimulateMsgPause generates a MsgChangePauseStatus with random values
func SimulateMsgPause(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// shuffle the assets and get a random one
assets := k.GetParams(ctx).Assets
r.Shuffle(len(assets), func(i, j int) {
assets[i], assets[j] = assets[j], assets[i]
})
asset := assets[0]
// make sure owner account exists
ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
}
ownerAcc := ak.GetAccount(ctx, asset.Owner)
// set status to paused/un-paused
status := r.Intn(2) == 0
msg := types.NewMsgSetPauseStatus(asset.Owner, asset.Denom, status)
spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas*2,
chainID,
[]uint64{ownerAcc.GetAccountNumber()},
[]uint64{ownerAcc.GetSequence()},
ownerSimAcc.PrivKey,
)
_, _, err = app.Deliver(tx)
if err != nil {
fmt.Printf("error on pause %s\n%s\n", msg, asset)
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
}

View File

@ -0,0 +1,25 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/issuance/types"
)
const (
keyAssets = "Assets"
)
// ParamChanges defines the parameters that can be modified by param change proposals
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []simulation.ParamChange{
simulation.NewSimParamChange(types.ModuleName, keyAssets,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%s\"", randomizedAssets(r))
},
),
}
}

View File

@ -0,0 +1,7 @@
<!--
order: 1
-->
# Concepts
The issuance mechanism in this module is designed to allow a trusted party to issue an asset on to the Kava blockchain. The issuer has sole discretion over the minting and redemption (burning) of the asset, as well as restricting access to the asset via asset seizure. The functionality of this module is similar to that of ERC-20 contracts for stablecoins that have a single issuer.

View File

@ -0,0 +1,31 @@
<!--
order: 2
-->
# State
## Parameters and Genesis State
```go
// Asset type for assets in the issuance module
type Asset struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
BlockedAddresses []sdk.AccAddress `json:"blocked_addresses" yaml:"blocked_addresses"`
Paused bool `json:"paused" yaml:"paused"`
}
// Assets array of Asset
type Assets []Asset
// Params governance parameters for the issuance module
type Params struct {
Assets Assets `json:"assets" yaml:"assets"`
}
// GenesisState state that must be provided at genesis
type GenesisState struct {
Assets Assets `json:"assets" yaml:"assets"`
}
```

View File

@ -0,0 +1,68 @@
<!--
order: 3
-->
# Messages
The issuer can issue new tokens using a `MsgIssueTokens`
```go
// MsgIssueTokens message type used to issue tokens
type MsgIssueTokens struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
}
```
## State Modifications
* New tokens are minted from the issuance module account
* New tokens are transferred from the module account to the receiver
The issuer can redeem (burn) tokens using `MsgRedeemTokens`.
```go
// MsgRedeemTokens message type used to redeem (burn) tokens
type MsgRedeemTokens struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
}
```
## State Modifications
* Tokens are transferred from the owner address to the issuer module account
* Tokens are burned
Addresses can be added to the blocked list using `MsgBlockAddress`
```go
// MsgBlockAddress message type used by the issuer to block an address from holding or transferring tokens
type MsgBlockAddress struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
BlockedAddress sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
}
```
## State Modifications
* The address is added to the block list, which prevents the account from holding coins of that denom
* Tokens are sent back to the issuer
The issuer can pause or un-pause the contract using `MsgChangePauseStatus`
```go
// MsgChangePauseStatus message type used by the issuer to issue new tokens
type MsgChangePauseStatus struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
Status bool `json:"status" yaml:"status"`
}
```
## State Modifications
* The `Paused` value of the correspond asset is updated to `Status`.
* Issuance and redemption are paused if `Paused` is false

View File

@ -0,0 +1,18 @@
<!--
order: 4
-->
# Events
The `x/issuance` module emits the following events:
## BeginBlock
| Type | Attribute Key | Attribute Value |
|----------------------|---------------------|-----------------|
| issue_tokens | amount_issued | `{amount}` |
| redeem_tokens | amount_redeemed | `{amount}` |
| block_address | address_blocked | `{address}` |
| block_address | denom | `{denom}` |
| change_pause_status | pause_status | `{bool}` |
| change_pause_status | denom | `{denom}` |

View File

@ -0,0 +1,21 @@
<!--
order: 5
-->
# Parameters
The issuance module has the following parameters:
| Key | Type | Example | Description |
|------------|----------------|-----------------|---------------------------------------------|
| Assets | array (Asset) | `[{see below}]` | array of assets created via issuance module |
Each `Asset` has the following parameters
| Key | Type | Example | Description |
|-------------------|------------------------|-------------------------------------------------|-------------------------------------------------------|
| Owner | sdk.AccAddress | "kava1cd8z53n7gh2hvz0lmmkzxkysfp5pghufat3h4a" | the address that controls the issuance of the asset |
| Denom | string | "usdtoken" | the denomination or exchange symbol of the asset |
| BlockedAccounts | array (sdk.AccAddress) | ["kava1tp9u8t8ang53a8tjh2mhqvvwdngqzjvmp3mamc"] | addresses which are blocked from holding the asset |
| Paused | boolean | false | boolean for if issuance and redemption are paused |

View File

@ -0,0 +1,16 @@
<!--
order: 6
-->
# Begin Block
At the start of each block, coins held by blocked addresses are redeemed
```go
func BeginBlocker(ctx sdk.Context, k Keeper) {
err := k.RedeemTokensFromBlockedAddresses(ctx, k)
if err != nil {
panic(err)
}
}
```

20
x/issuance/spec/README.md Normal file
View File

@ -0,0 +1,20 @@
<!--
order: 0
title: "Issuance Overview"
parent:
title: "issuance"
-->
# `issuance`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
6. **[BeginBlock](06_begin_block.md)**
## Abstract
`x/issuance` is an implementation of a Cosmos SDK Module that allows for an issuer to control the minting and burning of an asset. The issuer is a white-listed address, which may be a multisig address, that can mint and burn coins of a particular denomination. The issuer is the only entity that can mint or burn tokens of that asset type (i.e., there is no outside inflation or deflation). Additionally, the issuer can place transfer-restrictions on addresses, which prevents addresses from transferring and holding tokens with the issued denomination. Coins are minted and burned at the discretion of the issuer, using `MsgIssueTokens` and `MsgRedeemTokens` transaction types, respectively.

23
x/issuance/types/codec.go Normal file
View File

@ -0,0 +1,23 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc generic sealed codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers the necessary types for issuance module
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgIssueTokens{}, "issuance/MsgIssueTokens", nil)
cdc.RegisterConcrete(MsgRedeemTokens{}, "issuance/MsgRedeemTokens", nil)
cdc.RegisterConcrete(MsgBlockAddress{}, "issuance/MsgBlockAddress", nil)
cdc.RegisterConcrete(MsgUnblockAddress{}, "issuance/MsgUnblockAddress", nil)
cdc.RegisterConcrete(MsgSetPauseStatus{}, "issuance/MsgChangePauseStatus", nil)
cdc.RegisterConcrete(Asset{}, "issuance/Asset", nil)
}

View File

@ -0,0 +1,18 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DONTCOVER
// Errors used by the issuance module
var (
ErrAssetNotFound = sdkerrors.Register(ModuleName, 2, "no asset with input denom found")
ErrNotAuthorized = sdkerrors.Register(ModuleName, 3, "account not authorized")
ErrAssetPaused = sdkerrors.Register(ModuleName, 4, "asset is paused")
ErrAccountBlocked = sdkerrors.Register(ModuleName, 5, "account is blocked")
ErrAccountAlreadyBlocked = sdkerrors.Register(ModuleName, 6, "account is already blocked")
ErrAccountAlreadyUnblocked = sdkerrors.Register(ModuleName, 7, "account is already unblocked")
ErrIssueToModuleAccount = sdkerrors.Register(ModuleName, 8, "cannot issue tokens to module account")
)

View File

@ -0,0 +1,19 @@
package types
// Events emitted by the issuance module
const (
EventTypeIssue = "issue_tokens"
EventTypeRedeem = "redeem_tokens"
EventTypeBlock = "block_address"
EventTypeUnblock = "unblock_address"
EventTypePause = "change_pause_status"
EventTypeSeize = "seize_coins_from_blocked_address"
AttributeValueCategory = ModuleName
AttributeKeyDenom = "denom"
AttributeKeyIssueAmount = "amount_issued"
AttributeKeyRedeemAmount = "amount_redeemed"
AttributeKeyBlock = "address_blocked"
AttributeKeyUnblock = "address_unblocked"
AttributeKeyAddress = "address"
AttributeKeyPauseStatus = "pause_status"
)

View File

@ -0,0 +1,21 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// SupplyKeeper defines the expected supply keeper for module accounts (noalias)
type SupplyKeeper interface {
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
// AccountKeeper expected interface for the account keeper (noalias)
type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
}

View File

@ -0,0 +1,40 @@
package types
import "bytes"
// GenesisState is the state that must be provided at genesis for the issuance module
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
}
// NewGenesisState returns a new GenesisState
func NewGenesisState(params Params) GenesisState {
return GenesisState{
Params: params,
}
}
// DefaultGenesisState returns the default GenesisState for the issuance module
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
}
}
// Validate performs basic validation of genesis data returning an
// error for any failed validation criteria.
func (gs GenesisState) Validate() error {
return gs.Params.Validate()
}
// Equal checks whether two GenesisState structs are equivalent
func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}

View File

@ -0,0 +1,155 @@
package types_test
import (
"strings"
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/issuance/types"
)
type GenesisTestSuite struct {
suite.Suite
addrs []sdk.AccAddress
}
func (suite *GenesisTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
_, addrs := app.GeneratePrivKeyAddressPairs(2)
suite.addrs = addrs
}
func (suite *GenesisTestSuite) TestValidate() {
type args struct {
assets types.Assets
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
assets: types.DefaultAssets,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"with asset",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"blocked owner",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
},
},
errArgs{
expectPass: false,
contains: "asset owner cannot be blocked",
},
},
{
"empty owner",
args{
assets: types.Assets{
types.NewAsset(sdk.AccAddress{}, "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
},
},
errArgs{
expectPass: false,
contains: "owner must not be empty",
},
},
{
"empty blocked address",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{sdk.AccAddress{}}, false),
},
},
errArgs{
expectPass: false,
contains: "blocked address must not be empty",
},
},
{
"invalid denom",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "USD2T ", []sdk.AccAddress{}, false),
},
},
errArgs{
expectPass: false,
contains: "invalid denom",
},
},
{
"duplicate denom",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
types.NewAsset(suite.addrs[1], "usdtoken", []sdk.AccAddress{}, true),
},
},
errArgs{
expectPass: false,
contains: "duplicate asset denoms",
},
},
{
"duplicate asset",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false),
},
},
errArgs{
expectPass: false,
contains: "duplicate asset denoms",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
gs := types.NewGenesisState(types.NewParams(tc.args.assets))
err := gs.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

18
x/issuance/types/keys.go Normal file
View File

@ -0,0 +1,18 @@
package types
const (
// ModuleName The name that will be used throughout the module
ModuleName = "issuance"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
// QuerierRoute route used for abci queries
QuerierRoute = ModuleName
)

282
x/issuance/types/msg.go Normal file
View File

@ -0,0 +1,282 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgIssueTokens{}
var _ sdk.Msg = &MsgRedeemTokens{}
var _ sdk.Msg = &MsgBlockAddress{}
var _ sdk.Msg = &MsgUnblockAddress{}
var _ sdk.Msg = &MsgSetPauseStatus{}
// MsgIssueTokens message type used by the issuer to issue new tokens
type MsgIssueTokens struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
}
// NewMsgIssueTokens returns a new MsgIssueTokens
func NewMsgIssueTokens(sender sdk.AccAddress, tokens sdk.Coin, receiver sdk.AccAddress) MsgIssueTokens {
return MsgIssueTokens{
Sender: sender,
Tokens: tokens,
Receiver: receiver,
}
}
// Route return the message type used for routing the message.
func (msg MsgIssueTokens) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgIssueTokens) Type() string { return "issue_tokens" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgIssueTokens) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Tokens.IsZero() || !msg.Tokens.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "invalid tokens %s", msg.Tokens)
}
if msg.Receiver.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty")
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg
func (msg MsgIssueTokens) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign
func (msg MsgIssueTokens) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements fmt.Stringer
func (msg MsgIssueTokens) String() string {
return fmt.Sprintf(`Issue Tokens:
Sender %s
Tokens %s
Receiver %s
`, msg.Sender, msg.Tokens, msg.Receiver,
)
}
// MsgRedeemTokens message type used by the issuer to redeem (burn) tokens
type MsgRedeemTokens struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Tokens sdk.Coin `json:"tokens" yaml:"tokens"`
}
// NewMsgRedeemTokens returns a new MsgRedeemTokens
func NewMsgRedeemTokens(sender sdk.AccAddress, tokens sdk.Coin) MsgRedeemTokens {
return MsgRedeemTokens{
Sender: sender,
Tokens: tokens,
}
}
// Route return the message type used for routing the message.
func (msg MsgRedeemTokens) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgRedeemTokens) Type() string { return "redeem_tokens" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgRedeemTokens) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Tokens.IsZero() || !msg.Tokens.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "invalid tokens %s", msg.Tokens)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg
func (msg MsgRedeemTokens) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign
func (msg MsgRedeemTokens) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements fmt.Stringer
func (msg MsgRedeemTokens) String() string {
return fmt.Sprintf(`Redeem Tokens:
Sender %s
Tokens %s
`, msg.Sender, msg.Tokens,
)
}
// MsgBlockAddress message type used by the issuer to block an address from holding or transferring tokens
type MsgBlockAddress struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
Address sdk.AccAddress `json:"blocked_address" yaml:"blocked_address"`
}
// NewMsgBlockAddress returns a new MsgBlockAddress
func NewMsgBlockAddress(sender sdk.AccAddress, denom string, addr sdk.AccAddress) MsgBlockAddress {
return MsgBlockAddress{
Sender: sender,
Denom: denom,
Address: addr,
}
}
// Route return the message type used for routing the message.
func (msg MsgBlockAddress) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgBlockAddress) Type() string { return "block_address" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgBlockAddress) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Address.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address cannot be empty")
}
return sdk.ValidateDenom(msg.Denom)
}
// GetSignBytes gets the canonical byte representation of the Msg
func (msg MsgBlockAddress) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign
func (msg MsgBlockAddress) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements fmt.Stringer
func (msg MsgBlockAddress) String() string {
return fmt.Sprintf(`Block Address:
Sender %s
Denom %s
Address %s
`, msg.Sender, msg.Denom, msg.Address,
)
}
// MsgUnblockAddress message type used by the issuer to unblock an address from holding or transferring tokens
type MsgUnblockAddress struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
Address sdk.AccAddress `json:"address" yaml:"address"`
}
// NewMsgUnblockAddress returns a new MsgUnblockAddress
func NewMsgUnblockAddress(sender sdk.AccAddress, denom string, addr sdk.AccAddress) MsgUnblockAddress {
return MsgUnblockAddress{
Sender: sender,
Denom: denom,
Address: addr,
}
}
// Route return the message type used for routing the message.
func (msg MsgUnblockAddress) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgUnblockAddress) Type() string { return "unblock_address" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgUnblockAddress) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Address.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address cannot be empty")
}
return sdk.ValidateDenom(msg.Denom)
}
// GetSignBytes gets the canonical byte representation of the Msg
func (msg MsgUnblockAddress) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign
func (msg MsgUnblockAddress) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements fmt.Stringer
func (msg MsgUnblockAddress) String() string {
return fmt.Sprintf(`Unblock Address:
Sender %s
Denom %s
Address %s
`, msg.Sender, msg.Denom, msg.Address,
)
}
// MsgSetPauseStatus message type used by the issuer to issue new tokens
type MsgSetPauseStatus struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
Status bool `json:"status" yaml:"status"`
}
// NewMsgSetPauseStatus returns a new MsgSetPauseStatus
func NewMsgSetPauseStatus(sender sdk.AccAddress, denom string, status bool) MsgSetPauseStatus {
return MsgSetPauseStatus{
Sender: sender,
Denom: denom,
Status: status,
}
}
// Route return the message type used for routing the message.
func (msg MsgSetPauseStatus) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgSetPauseStatus) Type() string { return "change_pause_status" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgSetPauseStatus) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
return sdk.ValidateDenom(msg.Denom)
}
// GetSignBytes gets the canonical byte representation of the Msg
func (msg MsgSetPauseStatus) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign
func (msg MsgSetPauseStatus) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements fmt.Stringer
func (msg MsgSetPauseStatus) String() string {
return fmt.Sprintf(`Set Pause Status:
Sender %s
Denom %s
Status %t
`, msg.Sender, msg.Denom, msg.Status,
)
}

133
x/issuance/types/params.go Normal file
View File

@ -0,0 +1,133 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
)
// Parameter keys and default values
var (
KeyAssets = []byte("Assets")
DefaultAssets = Assets{}
ModuleAccountName = ModuleName
)
// Params governance parameters for the issuance module
type Params struct {
Assets Assets `json:"assets" yaml:"assets"`
}
// NewParams returns a new params object
func NewParams(assets Assets) Params {
return Params{Assets: assets}
}
// DefaultParams returns default params for issuance module
func DefaultParams() Params {
return NewParams(DefaultAssets)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyAssets, &p.Assets, validateAssetsParam),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
return validateAssetsParam(p.Assets)
}
func validateAssetsParam(i interface{}) error {
assets, ok := i.(Assets)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return assets.Validate()
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Assets: %s
`, p.Assets)
}
// Asset type for assets in the issuance module
type Asset struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
BlockedAddresses []sdk.AccAddress `json:"blocked_addresses" yaml:"blocked_addresses"`
Paused bool `json:"paused" yaml:"paused"`
}
// NewAsset returns a new Asset
func NewAsset(owner sdk.AccAddress, denom string, blockedAddresses []sdk.AccAddress, paused bool) Asset {
return Asset{
Owner: owner,
Denom: denom,
BlockedAddresses: blockedAddresses,
Paused: paused,
}
}
// Validate performs a basic check of asset fields
func (a Asset) Validate() error {
if a.Owner.Empty() {
return fmt.Errorf("owner must not be empty")
}
for _, address := range a.BlockedAddresses {
if address.Empty() {
return fmt.Errorf("blocked address must not be empty")
}
if a.Owner.Equals(address) {
return fmt.Errorf("asset owner cannot be blocked")
}
}
return sdk.ValidateDenom(a.Denom)
}
// String implements fmt.Stringer
func (a Asset) String() string {
return fmt.Sprintf(`Asset:
Owner: %s
Paused: %t
Denom: %s
Blocked Addresses: %s`,
a.Owner, a.Paused, a.Denom, a.BlockedAddresses)
}
// Assets array of Asset
type Assets []Asset
// Validate checks if all assets are valid and there are no duplicate entries
func (as Assets) Validate() error {
assetDenoms := make(map[string]bool)
for _, a := range as {
if assetDenoms[a.Denom] {
return fmt.Errorf("cannot have duplicate asset denoms: %s", a.Denom)
}
if err := a.Validate(); err != nil {
return err
}
assetDenoms[a.Denom] = true
}
return nil
}
// String implements fmt.Stringer
func (as Assets) String() string {
out := ""
for _, a := range as {
out += a.String()
}
return out
}

View File

@ -0,0 +1,12 @@
package types
// Querier routes for the issuance module
const (
QueryGetParams = "parameters"
QueryGetAsset = "asset"
)
// QueryAssetParams params for querying an asset by denom
type QueryAssetParams struct {
Denom string `json:"denom" yaml:"denom"`
}