Add optional rate limits to issuance (#627)

* add rate-limiting and optional blocklists

* fix: check account is not nil

* add tests for rate-limiting

* update simulations

* fix typos

* remove unsued function arg
This commit is contained in:
Kevin Davis 2020-08-21 18:56:20 -04:00 committed by GitHub
parent 2a3192fa0e
commit b356309d90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1228 additions and 64 deletions

View File

@ -74,6 +74,9 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
}
// pick a committee that has permissions for proposal
pp := types.PubProposal(contentSim(r, ctx, accs))
if pp == nil {
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation (conent generation function returned nil)", "", false, nil), nil, nil
}
var selectedCommittee types.Committee
var found bool
for _, c := range committees {

View File

@ -7,11 +7,10 @@ import (
// 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)
}
err := k.SeizeCoinsForBlockableAssets(ctx)
if err != nil {
panic(err)
}
k.SynchronizeBlockList(ctx)
k.UpdateTimeBasedSupplyLimits(ctx)
}

111
x/issuance/abci_test.go Normal file
View File

@ -0,0 +1,111 @@
package issuance_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
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"
)
// Test suite used for all keeper tests
type ABCITestSuite struct {
suite.Suite
keeper issuance.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
modAccount sdk.AccAddress
blockTime time.Time
}
// The default state used by each test
func (suite *ABCITestSuite) SetupTest() {
tApp := app.NewTestApp()
blockTime := tmtime.Now()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: blockTime})
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
suite.blockTime = blockTime
}
func (suite *ABCITestSuite) TestRateLimitingTimePassage() {
type args struct {
assets issuance.Assets
supplies issuance.AssetSupplies
blockTimes []time.Duration
expectedSupply issuance.AssetSupply
}
testCases := []struct {
name string
args args
}{
{
"time passage same period",
args{
assets: issuance.Assets{
issuance.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, issuance.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: issuance.AssetSupplies{
issuance.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
blockTimes: []time.Duration{time.Hour},
expectedSupply: issuance.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour*2),
},
},
{
"time passage new period",
args{
assets: issuance.Assets{
issuance.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, issuance.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: issuance.AssetSupplies{
issuance.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
blockTimes: []time.Duration{time.Hour * 12, time.Hour * 12},
expectedSupply: issuance.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Duration(0)),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := issuance.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
for _, supply := range tc.args.supplies {
suite.keeper.SetAssetSupply(suite.ctx, supply, supply.GetDenom())
}
suite.keeper.SetPreviousBlockTime(suite.ctx, suite.blockTime)
for _, bt := range tc.args.blockTimes {
nextBlockTime := suite.ctx.BlockTime().Add(bt)
suite.ctx = suite.ctx.WithBlockTime(nextBlockTime)
suite.Require().NotPanics(func() {
issuance.BeginBlocker(suite.ctx, suite.keeper)
})
}
actualSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.expectedSupply.GetDenom())
suite.Require().True(found)
suite.Require().Equal(tc.args.expectedSupply, actualSupply)
})
}
}
func TestABCITestSuite(t *testing.T) {
suite.Run(t, new(ABCITestSuite))
}

View File

@ -51,6 +51,8 @@ var (
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewAsset = types.NewAsset
NewRateLimit = types.NewRateLimit
NewAssetSupply = types.NewAssetSupply
// variable aliases
ModuleCdc = types.ModuleCdc
@ -61,6 +63,10 @@ var (
ErrAccountAlreadyBlocked = types.ErrAccountAlreadyBlocked
ErrAccountAlreadyUnblocked = types.ErrAccountAlreadyUnblocked
ErrIssueToModuleAccount = types.ErrIssueToModuleAccount
ErrExceedsSupplyLimit = types.ErrExceedsSupplyLimit
ErrAssetUnblockable = types.ErrAssetUnblockable
AssetSupplyPrefix = types.AssetSupplyPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
KeyAssets = types.KeyAssets
DefaultAssets = types.DefaultAssets
ModuleAccountName = types.ModuleAccountName
@ -77,5 +83,8 @@ type (
Params = types.Params
Asset = types.Asset
Assets = types.Assets
RateLimit = types.RateLimit
QueryAssetParams = types.QueryAssetParams
AssetSupply = types.AssetSupply
AssetSupplies = types.AssetSupplies
)

View File

@ -160,7 +160,7 @@ 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",
Long: "The asset owner pauses or un-pauses 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),

View File

@ -12,17 +12,27 @@ import (
// 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)
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
// 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))
}
k.SetParams(ctx, gs.Params)
for _, supply := range gs.Supplies {
k.SetAssetSupply(ctx, supply, supply.GetDenom())
}
}
// 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)
supplies := k.GetAllAssetSupplies(ctx)
return types.NewGenesisState(params, supplies)
}

View File

@ -21,17 +21,26 @@ func (k Keeper) IssueTokens(ctx sdk.Context, tokens sdk.Coin, owner, receiver sd
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)
if asset.Blockable {
blocked, _ := k.checkBlockedAddress(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)
}
// for rate-limited assets, check that the issuance isn't over the limit
if asset.RateLimit.Active {
err := k.IncrementCurrentAssetSupply(ctx, tokens)
if err != nil {
return err
}
}
// mint new tokens
err := k.supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(tokens))
if err != nil {
@ -87,13 +96,20 @@ func (k Keeper) BlockAddress(ctx sdk.Context, denom string, owner, blockedAddres
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
if !asset.Blockable {
return sdkerrors.Wrap(types.ErrAssetUnblockable, denom)
}
if !owner.Equals(asset.Owner) {
return sdkerrors.Wrapf(types.ErrNotAuthorized, "owner: %s, address: %s", asset.Owner, owner)
}
blocked, _ := k.checkBlockedAddress(ctx, asset, blockedAddress)
blocked, _ := k.checkBlockedAddress(asset, blockedAddress)
if blocked {
return sdkerrors.Wrapf(types.ErrAccountAlreadyBlocked, "address: %s", blockedAddress)
}
account := k.accountKeeper.GetAccount(ctx, blockedAddress)
if account == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, "address: %s", blockedAddress)
}
asset.BlockedAddresses = append(asset.BlockedAddresses, blockedAddress)
k.SetAsset(ctx, asset)
ctx.EventManager().EmitEvent(
@ -112,17 +128,20 @@ func (k Keeper) UnblockAddress(ctx sdk.Context, denom string, owner, addr sdk.Ac
if !found {
return sdkerrors.Wrapf(types.ErrAssetNotFound, "denom: %s", denom)
}
if !asset.Blockable {
return sdkerrors.Wrap(types.ErrAssetUnblockable, 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)
blocked, i := k.checkBlockedAddress(asset, addr)
if !blocked {
if blocked {
return sdkerrors.Wrapf(types.ErrAccountAlreadyUnblocked, "address: %s", addr)
}
}
blockedAddrs := k.removeBlockedAddress(ctx, asset.BlockedAddresses, i)
blockedAddrs := k.removeBlockedAddress(asset.BlockedAddresses, i)
asset.BlockedAddresses = blockedAddrs
k.SetAsset(ctx, asset)
ctx.EventManager().EmitEvent(
@ -159,6 +178,20 @@ func (k Keeper) SetPauseStatus(ctx sdk.Context, owner sdk.AccAddress, denom stri
return nil
}
// SeizeCoinsForBlockableAssets seizes coins from blocked addresses for assets that have blocking enabled
func (k Keeper) SeizeCoinsForBlockableAssets(ctx sdk.Context) error {
params := k.GetParams(ctx)
for _, asset := range params.Assets {
if asset.Blockable {
err := k.SeizeCoinsFromBlockedAddresses(ctx, asset.Denom)
if err != nil {
return err
}
}
}
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)
@ -167,6 +200,11 @@ func (k Keeper) SeizeCoinsFromBlockedAddresses(ctx sdk.Context, denom string) er
}
for _, address := range asset.BlockedAddresses {
account := k.accountKeeper.GetAccount(ctx, address)
if account == nil {
// avoids a potential panic
// this could happen if, for example, an account was pruned from state but remained in the block list,
continue
}
coinsAmount := account.GetCoins().AmountOf(denom)
if !coinsAmount.IsPositive() {
continue
@ -191,7 +229,7 @@ func (k Keeper) SeizeCoinsFromBlockedAddresses(ctx sdk.Context, denom string) er
return nil
}
func (k Keeper) checkBlockedAddress(ctx sdk.Context, asset types.Asset, checkAddress sdk.AccAddress) (bool, int) {
func (k Keeper) checkBlockedAddress(asset types.Asset, checkAddress sdk.AccAddress) (bool, int) {
for i, address := range asset.BlockedAddresses {
if address.Equals(checkAddress) {
return true, i
@ -200,7 +238,7 @@ func (k Keeper) checkBlockedAddress(ctx sdk.Context, asset types.Asset, checkAdd
return false, 0
}
func (k Keeper) removeBlockedAddress(ctx sdk.Context, blockedAddrs []sdk.AccAddress, i int) []sdk.AccAddress {
func (k Keeper) removeBlockedAddress(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

@ -3,6 +3,7 @@ package keeper_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -11,6 +12,7 @@ import (
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
@ -35,6 +37,10 @@ func (suite *KeeperTestSuite) SetupTest() {
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(5)
for _, addr := range addrs {
acc := tApp.GetAccountKeeper().NewAccountWithAddress(ctx, addr)
tApp.GetAccountKeeper().SetAccount(ctx, acc)
}
keeper := tApp.GetIssuanceKeeper()
modAccount, err := sdk.AccAddressFromBech32("kava1cj7njkw2g9fqx4e768zc75dp9sks8u9znxrf0w")
suite.Require().NoError(err)
@ -58,7 +64,7 @@ func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.Modul
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)
asset := types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0)))
params = types.NewParams(types.Assets{asset})
suite.keeper.SetParams(suite.ctx, params)
newParams := suite.keeper.GetParams(suite.ctx)
@ -85,7 +91,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"valid issuance",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -100,7 +106,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"non-owner issuance",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[2],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -115,7 +121,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"invalid denom",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("othertoken", sdk.NewInt(100000)),
@ -130,7 +136,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"issue to blocked address",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -145,7 +151,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"issue to module account",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -160,7 +166,7 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
"paused issuance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -190,6 +196,85 @@ func (suite *KeeperTestSuite) TestIssueTokens() {
}
}
func (suite *KeeperTestSuite) TestIssueTokensRateLimited() {
type args struct {
assets types.Assets
supplies types.AssetSupplies
sender sdk.AccAddress
tokens sdk.Coin
receiver sdk.AccAddress
blockTime time.Time
}
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, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: types.AssetSupplies{
types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
receiver: suite.addrs[2],
blockTime: suite.ctx.BlockTime().Add(time.Hour),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over-limit issuance",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: types.AssetSupplies{
types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
sender: suite.addrs[0],
tokens: sdk.NewCoin("usdtoken", sdk.NewInt(10000000001)),
receiver: suite.addrs[2],
blockTime: suite.ctx.BlockTime().Add(time.Hour),
},
errArgs{
expectPass: false,
contains: "asset supply over limit",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
for _, supply := range tc.args.supplies {
suite.keeper.SetAssetSupply(suite.ctx, supply, supply.GetDenom())
}
suite.ctx = suite.ctx.WithBlockTime(tc.args.blockTime)
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
@ -210,7 +295,7 @@ func (suite *KeeperTestSuite) TestRedeemTokens() {
"valid redemption",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -225,7 +310,7 @@ func (suite *KeeperTestSuite) TestRedeemTokens() {
"invalid denom redemption",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -240,7 +325,7 @@ func (suite *KeeperTestSuite) TestRedeemTokens() {
"non-owner redemption",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[2],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -255,7 +340,7 @@ func (suite *KeeperTestSuite) TestRedeemTokens() {
"paused redemption",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -270,7 +355,7 @@ func (suite *KeeperTestSuite) TestRedeemTokens() {
"redeem amount greater than balance",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
initialTokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
@ -328,7 +413,7 @@ func (suite *KeeperTestSuite) TestBlockAddress() {
"valid block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
@ -339,11 +424,26 @@ func (suite *KeeperTestSuite) TestBlockAddress() {
contains: "",
},
},
{
"unblockable token",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, false, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
denom: "usdtoken",
},
errArgs{
expectPass: false,
contains: "asset does not support block/unblock functionality",
},
},
{
"non-owner block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[2],
blockedAddr: suite.addrs[1],
@ -358,7 +458,7 @@ func (suite *KeeperTestSuite) TestBlockAddress() {
"invalid denom block",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
@ -369,6 +469,21 @@ func (suite *KeeperTestSuite) TestBlockAddress() {
contains: "no asset with input denom found",
},
},
{
"block non-existing account",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: sdk.AccAddress(crypto.AddressHash([]byte("RandomAddr"))),
denom: "usdtoken",
},
errArgs{
expectPass: false,
contains: "cannot block account that does not exist in state",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
@ -416,7 +531,7 @@ func (suite *KeeperTestSuite) TestUnblockAddress() {
"valid unblock",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
@ -431,7 +546,7 @@ func (suite *KeeperTestSuite) TestUnblockAddress() {
"non-owner unblock",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[2],
blockedAddr: suite.addrs[1],
@ -446,7 +561,7 @@ func (suite *KeeperTestSuite) TestUnblockAddress() {
"invalid denom block",
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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
blockedAddr: suite.addrs[1],
@ -505,7 +620,7 @@ func (suite *KeeperTestSuite) TestChangePauseStatus() {
"valid pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
startStatus: false,
@ -521,7 +636,7 @@ func (suite *KeeperTestSuite) TestChangePauseStatus() {
"valid unpause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, true),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
startStatus: true,
@ -537,7 +652,7 @@ func (suite *KeeperTestSuite) TestChangePauseStatus() {
"non-owner pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[2],
startStatus: false,
@ -553,7 +668,7 @@ func (suite *KeeperTestSuite) TestChangePauseStatus() {
"invalid denom pause",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
sender: suite.addrs[0],
startStatus: true,
@ -606,7 +721,7 @@ func (suite *KeeperTestSuite) TestSeizeCoinsFromBlockedAddress() {
"valid seize",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
denom: "usdtoken",
@ -621,7 +736,7 @@ func (suite *KeeperTestSuite) TestSeizeCoinsFromBlockedAddress() {
"invalid denom seize",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)),
denom: "othertoken",

View File

@ -1,7 +1,10 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
@ -31,3 +34,62 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
supplyKeeper: sk,
}
}
// GetAssetSupply gets an asset's current supply from the store.
func (k Keeper) GetAssetSupply(ctx sdk.Context, denom string) (types.AssetSupply, bool) {
var assetSupply types.AssetSupply
store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.AssetSupply{}, false
}
k.cdc.MustUnmarshalBinaryBare(bz, &assetSupply)
return assetSupply, true
}
// SetAssetSupply updates an asset's supply
func (k Keeper) SetAssetSupply(ctx sdk.Context, supply types.AssetSupply, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyPrefix)
store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(supply))
}
// IterateAssetSupplies provides an iterator over all stored AssetSupplies.
func (k Keeper) IterateAssetSupplies(ctx sdk.Context, cb func(supply types.AssetSupply) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AssetSupplyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var supply types.AssetSupply
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &supply)
if cb(supply) {
break
}
}
}
// GetAllAssetSupplies returns all asset supplies from the store
func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSupplies) {
k.IterateAssetSupplies(ctx, func(supply types.AssetSupply) bool {
supplies = append(supplies, supply)
return false
})
return
}
// GetPreviousBlockTime get the blocktime for the previous block
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b := store.Get([]byte{})
if b == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
return blockTime, true
}
// SetPreviousBlockTime set the time of the previous block
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime))
}

View File

@ -2,6 +2,7 @@ package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/issuance/types"
)
@ -39,3 +40,23 @@ func (k Keeper) SetAsset(ctx sdk.Context, asset types.Asset) {
}
k.SetParams(ctx, params)
}
// GetRateLimit returns the rete-limit parameters for the input denom
func (k Keeper) GetRateLimit(ctx sdk.Context, denom string) (types.RateLimit, error) {
asset, found := k.GetAsset(ctx, denom)
if !found {
sdkerrors.Wrap(types.ErrAssetNotFound, denom)
}
return asset.RateLimit, nil
}
// SynchronizeBlockList resets the block list to empty for any asset that is not blockable - could happen if this value is changed via governance
func (k Keeper) SynchronizeBlockList(ctx sdk.Context) {
params := k.GetParams(ctx)
for _, asset := range params.Assets {
if !asset.Blockable && len(asset.BlockedAddresses) > 0 {
asset.BlockedAddresses = []sdk.AccAddress{}
k.SetAsset(ctx, asset)
}
}
}

View File

@ -0,0 +1,73 @@
package keeper
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/issuance/types"
)
// CreateNewAssetSupply creates a new AssetSupply in the store for the input denom
func (k Keeper) CreateNewAssetSupply(ctx sdk.Context, denom string) types.AssetSupply {
supply := types.NewAssetSupply(
sdk.NewCoin(denom, sdk.ZeroInt()), time.Duration(0))
k.SetAssetSupply(ctx, supply, denom)
return supply
}
// IncrementCurrentAssetSupply increments an asset's supply by the coin
func (k Keeper) IncrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return sdkerrors.Wrap(types.ErrAssetNotFound, coin.Denom)
}
limit, err := k.GetRateLimit(ctx, coin.Denom)
if err != nil {
return err
}
if limit.Active {
supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
// Resulting current supply must be under asset's limit
if supplyLimit.IsLT(supply.CurrentSupply.Add(coin)) {
return sdkerrors.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, supply.CurrentSupply, supplyLimit)
}
supply.CurrentSupply = supply.CurrentSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
}
return nil
}
// UpdateTimeBasedSupplyLimits updates the time based supply for each asset, resetting it if the current time window has elapsed.
func (k Keeper) UpdateTimeBasedSupplyLimits(ctx sdk.Context) {
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
}
timeElapsed := ctx.BlockTime().Sub(previousBlockTime)
for _, asset := range params.Assets {
supply, found := k.GetAssetSupply(ctx, asset.Denom)
// if a new asset has been added by governance, create a new asset supply for it in the store
if !found {
supply = k.CreateNewAssetSupply(ctx, asset.Denom)
}
if asset.RateLimit.Active {
if asset.RateLimit.TimePeriod <= supply.TimeElapsed+timeElapsed {
supply.TimeElapsed = time.Duration(0)
supply.CurrentSupply = sdk.NewCoin(asset.Denom, sdk.ZeroInt())
} else {
supply.TimeElapsed = supply.TimeElapsed + timeElapsed
}
} else {
supply.CurrentSupply = sdk.NewCoin(asset.Denom, sdk.ZeroInt())
supply.TimeElapsed = time.Duration(0)
}
k.SetAssetSupply(ctx, supply, asset.Denom)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}

View File

@ -0,0 +1,83 @@
package keeper_test
import (
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/issuance/types"
)
func (suite *KeeperTestSuite) TestIncrementCurrentAssetSupply() {
type args struct {
assets types.Assets
supplies types.AssetSupplies
coin sdk.Coin
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"valid supply increase",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: types.AssetSupplies{
types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
coin: sdk.NewCoin("usdtoken", sdk.NewInt(100000)),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over limit increase",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)),
},
supplies: types.AssetSupplies{
types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour),
},
coin: sdk.NewCoin("usdtoken", sdk.NewInt(10000000001)),
},
errArgs{
expectPass: false,
contains: "asset supply over limit",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
params := types.NewParams(tc.args.assets)
suite.keeper.SetParams(suite.ctx, params)
for _, supply := range tc.args.supplies {
suite.keeper.SetAssetSupply(suite.ctx, supply, supply.GetDenom())
}
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin)
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)
for _, expectedSupply := range tc.args.supplies {
expectedSupply.CurrentSupply = expectedSupply.CurrentSupply.Add(tc.args.coin)
actualSupply, found := suite.keeper.GetAssetSupply(suite.ctx, expectedSupply.GetDenom())
suite.Require().True(found)
suite.Require().Equal(expectedSupply, actualSupply, tc.name)
}
} else {
suite.Require().Error(err, tc.name)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}

View File

@ -1,7 +1,9 @@
package simulation
import (
"bytes"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
@ -12,5 +14,20 @@ import (
// 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]))
switch {
case bytes.Equal(kvA.Key[:1], types.AssetSupplyPrefix):
var supplyA, supplyB types.AssetSupply
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &supplyA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &supplyB)
return fmt.Sprintf("%s\n%s", supplyA, supplyB)
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}
}

View File

@ -0,0 +1,54 @@
package simulation
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/issuance/types"
)
func makeTestCodec() (cdc *codec.Codec) {
cdc = codec.New()
sdk.RegisterCodec(cdc)
types.RegisterCodec(cdc)
return
}
func TestDecodeIssuanceStore(t *testing.T) {
cdc := makeTestCodec()
supply := types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour)
prevBlockTime := time.Now().UTC()
kvPairs := kv.Pairs{
kv.Pair{Key: types.AssetSupplyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(supply)},
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
}
tests := []struct {
name string
expectedLog string
}{
{"AssetSupply", fmt.Sprintf("%v\n%v", supply, supply)},
{"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
{"other", ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
}
})
}
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -21,7 +22,7 @@ var (
func RandomizedGenState(simState *module.SimulationState) {
accs = simState.Accounts
params := randomizedParams(simState.Rand)
gs := types.NewGenesisState(params)
gs := types.NewGenesisState(params, types.AssetSupplies{})
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)
}
@ -38,7 +39,14 @@ func randomizedAssets(r *rand.Rand) types.Assets {
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)
rateLimited := r.Intn(2) == 0
rateLimit := types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))
if rateLimited {
timeLimit := time.Duration(3600000000000 + (r.Intn(24) + 1))
assetLimit := simulation.RandIntBetween(r, 100000000000, 1000000000000)
rateLimit = types.NewRateLimit(true, sdk.NewInt(int64(assetLimit)), timeLimit)
}
randomAsset := types.NewAsset(owner.Address, denom, []sdk.AccAddress{}, paused, true, rateLimit)
randomAssets = append(randomAssets, randomAsset)
}
return randomAssets

View File

@ -116,6 +116,21 @@ func SimulateMsgIssueTokens(ak types.AccountKeeper, k keeper.Keeper) simulation.
}
}
randomAmount := simulation.RandIntBetween(r, 10000000, 1000000000000)
if asset.RateLimit.Active {
supply, found := k.GetAssetSupply(ctx, asset.Denom)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("issuance - no asset supply for %s", asset.Denom)
}
if asset.RateLimit.Limit.LT(supply.CurrentSupply.Amount.Add(sdk.NewInt(int64(randomAmount)))) {
maxAmount := asset.RateLimit.Limit.Sub(supply.CurrentSupply.Amount)
if maxAmount.IsPositive() && maxAmount.GT(sdk.OneInt()) {
randomAmount = simulation.RandIntBetween(r, 1, int(maxAmount.Int64()))
} else {
randomAmount = 1
}
}
}
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)

View File

@ -15,4 +15,7 @@ var (
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")
ErrExceedsSupplyLimit = sdkerrors.Register(ModuleName, 9, "asset supply over limit")
ErrAssetUnblockable = sdkerrors.Register(ModuleName, 10, "asset does not support block/unblock functionality")
ErrAccountNotFound = sdkerrors.Register(ModuleName, 11, "cannot block account that does not exist in state")
)

View File

@ -4,26 +4,35 @@ 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"`
Params Params `json:"params" yaml:"params"`
Supplies AssetSupplies `json:"supplies" yaml:"supplies"`
}
// NewGenesisState returns a new GenesisState
func NewGenesisState(params Params) GenesisState {
func NewGenesisState(params Params, supplies AssetSupplies) GenesisState {
return GenesisState{
Params: params,
Params: params,
Supplies: supplies,
}
}
// DefaultGenesisState returns the default GenesisState for the issuance module
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
Params: DefaultParams(),
Supplies: AssetSupplies{},
}
}
// Validate performs basic validation of genesis data returning an
// error for any failed validation criteria.
func (gs GenesisState) Validate() error {
for _, supply := range gs.Supplies {
err := supply.Validate()
if err != nil {
return err
}
}
return gs.Params.Validate()
}

View File

@ -3,6 +3,7 @@ package types_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -28,7 +29,8 @@ func (suite *GenesisTestSuite) SetupTest() {
func (suite *GenesisTestSuite) TestValidate() {
type args struct {
assets types.Assets
assets types.Assets
supplies types.AssetSupplies
}
type errArgs struct {
expectPass bool
@ -42,7 +44,8 @@ func (suite *GenesisTestSuite) TestValidate() {
{
"default",
args{
assets: types.DefaultAssets,
assets: types.DefaultAssets,
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: true,
@ -53,8 +56,36 @@ func (suite *GenesisTestSuite) TestValidate() {
"with 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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.NewInt(1000000)), time.Hour)},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"with asset rate limit",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(1000000000), time.Hour*24)),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"with multiple assets",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
types.NewAsset(suite.addrs[0], "pegtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: true,
@ -65,8 +96,9 @@ func (suite *GenesisTestSuite) TestValidate() {
"blocked owner",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
@ -77,8 +109,9 @@ func (suite *GenesisTestSuite) TestValidate() {
"empty owner",
args{
assets: types.Assets{
types.NewAsset(sdk.AccAddress{}, "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false),
types.NewAsset(sdk.AccAddress{}, "usdtoken", []sdk.AccAddress{suite.addrs[0]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
@ -89,8 +122,9 @@ func (suite *GenesisTestSuite) TestValidate() {
"empty blocked address",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{sdk.AccAddress{}}, false),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{nil}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
@ -101,8 +135,9 @@ func (suite *GenesisTestSuite) TestValidate() {
"invalid denom",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "USD2T ", []sdk.AccAddress{}, false),
types.NewAsset(suite.addrs[0], "USD2T ", []sdk.AccAddress{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
@ -113,9 +148,10 @@ func (suite *GenesisTestSuite) TestValidate() {
"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),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
types.NewAsset(suite.addrs[1], "usdtoken", []sdk.AccAddress{}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
@ -126,19 +162,33 @@ func (suite *GenesisTestSuite) TestValidate() {
"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),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{},
},
errArgs{
expectPass: false,
contains: "duplicate asset denoms",
},
},
{
"invalid block list",
args{
assets: types.Assets{
types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{suite.addrs[1]}, false, false, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))),
},
supplies: types.AssetSupplies{types.NewAssetSupply(sdk.NewCoin("usdtoken", sdk.ZeroInt()), time.Hour)},
},
errArgs{
expectPass: false,
contains: "blocked-list should be empty",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
gs := types.NewGenesisState(types.NewParams(tc.args.assets))
gs := types.NewGenesisState(types.NewParams(tc.args.assets), tc.args.supplies)
err := gs.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err, tc.name)

View File

@ -16,3 +16,9 @@ const (
// QuerierRoute route used for abci queries
QuerierRoute = ModuleName
)
// KVStore key prefixes
var (
AssetSupplyPrefix = []byte{0x01}
PreviousBlockTimeKey = []byte{0x02}
)

View File

@ -0,0 +1,393 @@
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 MsgTestSuite struct {
suite.Suite
addrs []sdk.AccAddress
}
func (suite *MsgTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
_, addrs := app.GeneratePrivKeyAddressPairs(2)
suite.addrs = addrs
}
func (suite *MsgTestSuite) TestMsgIssueTokens() {
type args struct {
sender sdk.AccAddress
tokens sdk.Coin
receiver sdk.AccAddress
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
sender: suite.addrs[0],
tokens: sdk.NewCoin("valid", sdk.NewInt(100)),
receiver: suite.addrs[1],
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid sender",
args{
sender: sdk.AccAddress{},
tokens: sdk.NewCoin("valid", sdk.NewInt(100)),
receiver: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "sender address cannot be empty",
},
},
{
"invalid receiver",
args{
sender: suite.addrs[0],
tokens: sdk.NewCoin("valid", sdk.NewInt(100)),
receiver: sdk.AccAddress{},
},
errArgs{
expectPass: false,
contains: "receiver address cannot be empty",
},
},
{
"invalid tokens",
args{
sender: suite.addrs[0],
tokens: sdk.Coin{Denom: "Invalid", Amount: sdk.NewInt(100)},
receiver: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "invalid tokens",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
testMsg := types.NewMsgIssueTokens(tc.args.sender, tc.args.tokens, tc.args.receiver)
err := testMsg.ValidateBasic()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *MsgTestSuite) TestMsgRedeemTokens() {
type args struct {
sender sdk.AccAddress
tokens sdk.Coin
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
sender: suite.addrs[0],
tokens: sdk.NewCoin("valid", sdk.NewInt(100)),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid sender",
args{
sender: sdk.AccAddress{},
tokens: sdk.NewCoin("valid", sdk.NewInt(100)),
},
errArgs{
expectPass: false,
contains: "sender address cannot be empty",
},
},
{
"invalid tokens",
args{
sender: suite.addrs[0],
tokens: sdk.Coin{Denom: "Invalid", Amount: sdk.NewInt(100)},
},
errArgs{
expectPass: false,
contains: "invalid tokens",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
testMsg := types.NewMsgRedeemTokens(tc.args.sender, tc.args.tokens)
err := testMsg.ValidateBasic()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *MsgTestSuite) TestMsgBlockAddress() {
type args struct {
sender sdk.AccAddress
denom string
address sdk.AccAddress
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
sender: suite.addrs[0],
denom: "valid",
address: suite.addrs[1],
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid sender",
args{
sender: sdk.AccAddress{},
denom: "valid",
address: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "sender address cannot be empty",
},
},
{
"invalid blocked",
args{
sender: suite.addrs[0],
denom: "valid",
address: sdk.AccAddress{},
},
errArgs{
expectPass: false,
contains: "blocked address cannot be empty",
},
},
{
"invalid denom",
args{
sender: suite.addrs[0],
denom: "Invalid",
address: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "invalid denom",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
testMsg := types.NewMsgBlockAddress(tc.args.sender, tc.args.denom, tc.args.address)
err := testMsg.ValidateBasic()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *MsgTestSuite) TestMsgUnblockAddress() {
type args struct {
sender sdk.AccAddress
denom string
address sdk.AccAddress
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
sender: suite.addrs[0],
denom: "valid",
address: suite.addrs[1],
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid sender",
args{
sender: sdk.AccAddress{},
denom: "valid",
address: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "sender address cannot be empty",
},
},
{
"invalid blocked",
args{
sender: suite.addrs[0],
denom: "valid",
address: sdk.AccAddress{},
},
errArgs{
expectPass: false,
contains: "blocked address cannot be empty",
},
},
{
"invalid denom",
args{
sender: suite.addrs[0],
denom: "Invalid",
address: suite.addrs[1],
},
errArgs{
expectPass: false,
contains: "invalid denom",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
testMsg := types.NewMsgUnblockAddress(tc.args.sender, tc.args.denom, tc.args.address)
err := testMsg.ValidateBasic()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *MsgTestSuite) TestMsgSetPauseStatus() {
type args struct {
sender sdk.AccAddress
denom string
status bool
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"default",
args{
sender: suite.addrs[0],
denom: "valid",
status: true,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid sender",
args{
sender: sdk.AccAddress{},
denom: "valid",
status: true,
},
errArgs{
expectPass: false,
contains: "sender address cannot be empty",
},
},
{
"invalid denom",
args{
sender: suite.addrs[0],
denom: "Invalid",
status: true,
},
errArgs{
expectPass: false,
contains: "invalid denom",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
testMsg := types.NewMsgSetPauseStatus(tc.args.sender, tc.args.denom, tc.args.status)
err := testMsg.ValidateBasic()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func TestMsgTestSuite(t *testing.T) {
suite.Run(t, new(MsgTestSuite))
}

View File

@ -2,6 +2,7 @@ package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
@ -67,15 +68,19 @@ type Asset struct {
Denom string `json:"denom" yaml:"denom"`
BlockedAddresses []sdk.AccAddress `json:"blocked_addresses" yaml:"blocked_addresses"`
Paused bool `json:"paused" yaml:"paused"`
Blockable bool `json:"blockable" yaml:"blockable"`
RateLimit RateLimit `json:"rate_limit" yaml:"rate_limit"`
}
// NewAsset returns a new Asset
func NewAsset(owner sdk.AccAddress, denom string, blockedAddresses []sdk.AccAddress, paused bool) Asset {
func NewAsset(owner sdk.AccAddress, denom string, blockedAddresses []sdk.AccAddress, paused bool, blockable bool, limit RateLimit) Asset {
return Asset{
Owner: owner,
Denom: denom,
BlockedAddresses: blockedAddresses,
Paused: paused,
Blockable: blockable,
RateLimit: limit,
}
}
@ -84,6 +89,9 @@ func (a Asset) Validate() error {
if a.Owner.Empty() {
return fmt.Errorf("owner must not be empty")
}
if !a.Blockable && len(a.BlockedAddresses) > 0 {
return fmt.Errorf("asset %s does not support blocking, blocked-list should be empty: %s", a.Denom, a.BlockedAddresses)
}
for _, address := range a.BlockedAddresses {
if address.Empty() {
return fmt.Errorf("blocked address must not be empty")
@ -101,8 +109,9 @@ func (a Asset) String() string {
Owner: %s
Paused: %t
Denom: %s
Blocked Addresses: %s`,
a.Owner, a.Paused, a.Denom, a.BlockedAddresses)
Blocked Addresses: %s
Rate limits: %s`,
a.Owner, a.Paused, a.Denom, a.BlockedAddresses, a.RateLimit)
}
// Assets array of Asset
@ -131,3 +140,28 @@ func (as Assets) String() string {
}
return out
}
// RateLimit parameters for rate-limiting the supply of an issued asset
type RateLimit struct {
Active bool `json:"active" yaml:"active"`
Limit sdk.Int `json:"limit" yaml:"limit"`
TimePeriod time.Duration `json:"time_period" yaml:"time_period"`
}
// NewRateLimit initializes a new RateLimit
func NewRateLimit(active bool, limit sdk.Int, timePeriod time.Duration) RateLimit {
return RateLimit{
Active: active,
Limit: limit,
TimePeriod: timePeriod,
}
}
// String implements fmt.Stringer
func (r RateLimit) String() string {
return fmt.Sprintf(`
Active: %t
Limit: %s
Time Period: %s`,
r.Active, r.Limit, r.TimePeriod)
}

View File

@ -0,0 +1,51 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// AssetSupply contains information about an asset's rate-limited supply (the total supply of the asset is tracked in the top-level supply module)
type AssetSupply struct {
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
TimeElapsed time.Duration `json:"time_elapsed" yaml:"time_elapsed"`
}
// NewAssetSupply initializes a new AssetSupply
func NewAssetSupply(currentSupply sdk.Coin, timeElapsed time.Duration) AssetSupply {
return AssetSupply{
CurrentSupply: currentSupply,
TimeElapsed: timeElapsed,
}
}
// Validate performs a basic validation of an asset supply fields.
func (a AssetSupply) Validate() error {
if !a.CurrentSupply.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "outgoing supply %s", a.CurrentSupply)
}
return nil
}
// String implements stringer
func (a AssetSupply) String() string {
return fmt.Sprintf(`
asset supply:
Current supply: %s
Time elapsed: %s
`,
a.CurrentSupply, a.TimeElapsed)
}
// GetDenom getter method for the denom of the asset supply
func (a AssetSupply) GetDenom() string {
return a.CurrentSupply.Denom
}
// AssetSupplies is a slice of AssetSupply
type AssetSupplies []AssetSupply
// TODO copy over supply tests from bep3