package keeper_test import ( "strings" "testing" "time" "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" tmprototypes "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/crypto" 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 tApp app.TestApp keeper keeper.Keeper ctx sdk.Context addrs []string modAccount sdk.AccAddress } // The default state used by each test func (suite *KeeperTestSuite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, tmprototypes.Header{Height: 1, Time: tmtime.Now()}) tApp.InitializeFromGenesisStates() _, addrs := app.GeneratePrivKeyAddressPairs(5) var strAddrs []string for _, addr := range addrs { acc := tApp.GetAccountKeeper().NewAccountWithAddress(ctx, addr) tApp.GetAccountKeeper().SetAccount(ctx, acc) strAddrs = append(strAddrs, addr.String()) } keeper := tApp.GetIssuanceKeeper() modAccount, err := sdk.AccAddressFromBech32("kava1cj7njkw2g9fqx4e768zc75dp9sks8u9znxrf0w") suite.Require().NoError(err) suite.tApp = tApp suite.ctx = ctx suite.keeper = keeper suite.addrs = strAddrs suite.modAccount = modAccount } func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authtypes.AccountI { ak := suite.tApp.GetAccountKeeper() return ak.GetAccount(suite.ctx, addr) } func (suite *KeeperTestSuite) getBalance(addr sdk.AccAddress, denom string) sdk.Coin { bk := suite.tApp.GetBankKeeper() return bk.GetBalance(suite.ctx, addr, denom) } func (suite *KeeperTestSuite) getModuleAccount(name string) authtypes.ModuleAccountI { sk := suite.tApp.GetAccountKeeper() return sk.GetModuleAccount(suite.ctx, name) } func (suite *KeeperTestSuite) TestGetSetParams() { params := suite.keeper.GetParams(suite.ctx) suite.Require().Equal(types.Params{Assets: []types.Asset(nil)}, params) asset := types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))) params = types.NewParams([]types.Asset{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.Asset sender string tokens sdk.Coin receiver string } type errArgs struct { expectPass bool contains string } testCases := []struct { name string args args errArgs errArgs }{ { "valid issuance", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], tokens: sdk.NewCoin("usdtoken", sdk.NewInt(100000)), receiver: suite.modAccount.String(), }, errArgs{ expectPass: false, contains: "cannot issue tokens to module account", }, }, { "paused issuance", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) receiver, _ := sdk.AccAddressFromBech32(tc.args.receiver) err := suite.keeper.IssueTokens(suite.ctx, tc.args.tokens, sender, receiver) if tc.errArgs.expectPass { suite.Require().NoError(err, tc.name) suite.Require().Equal(sdk.NewCoins(tc.args.tokens), sdk.NewCoins(suite.getBalance(receiver, tc.args.tokens.Denom))) } else { suite.Require().Error(err, tc.name) suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) } }) } } func (suite *KeeperTestSuite) TestIssueTokensRateLimited() { type args struct { assets []types.Asset supplies []types.AssetSupply sender string tokens sdk.Coin receiver string blockTime time.Time } type errArgs struct { expectPass bool contains string } testCases := []struct { name string args args errArgs errArgs }{ { "valid issuance", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)), }, supplies: []types.AssetSupply{ 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(true, sdk.NewInt(10000000000), time.Hour*24)), }, supplies: []types.AssetSupply{ 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) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) receiver, _ := sdk.AccAddressFromBech32(tc.args.receiver) err := suite.keeper.IssueTokens(suite.ctx, tc.args.tokens, sender, receiver) if tc.errArgs.expectPass { suite.Require().NoError(err, tc.name) suite.Require().Equal(sdk.NewCoins(tc.args.tokens), sdk.NewCoins(suite.getBalance(receiver, tc.args.tokens.Denom))) } 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.Asset sender string 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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.tApp.GetBankKeeper() err := sk.MintCoins(suite.ctx, types.ModuleAccountName, sdk.NewCoins(tc.args.initialTokens)) suite.Require().NoError(err) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleAccountName, sender, sdk.NewCoins(tc.args.initialTokens)) suite.Require().NoError(err) err = suite.keeper.RedeemTokens(suite.ctx, tc.args.redeemTokens, sender) if tc.errArgs.expectPass { suite.Require().NoError(err) initialSupply := sdk.NewCoins(tc.args.redeemTokens) moduleAccount := suite.getModuleAccount(types.ModuleAccountName) suite.Require().Equal(sdk.NewCoins(initialSupply.Sub(sdk.NewCoins(tc.args.redeemTokens))...), sdk.NewCoins(suite.getBalance(moduleAccount.GetAddress(), tc.args.redeemTokens.Denom))) } else { suite.Require().Error(err) suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) } }) } } func (suite *KeeperTestSuite) TestBlockAddress() { type args struct { assets []types.Asset sender string blockedAddr string denom string } type errArgs struct { expectPass bool contains string } testCases := []struct { name string args args errArgs errArgs }{ { "valid block", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], blockedAddr: suite.addrs[1], denom: "usdtoken", }, errArgs{ expectPass: true, contains: "", }, }, { "unblockable token", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[2], blockedAddr: suite.addrs[1], denom: "usdtoken", }, errArgs{ expectPass: false, contains: "account not authorized", }, }, { "invalid denom block", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], blockedAddr: suite.addrs[1], denom: "othertoken", }, errArgs{ expectPass: false, contains: "no asset with input denom found", }, }, { "block non-existing account", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], blockedAddr: sdk.AccAddress(crypto.AddressHash([]byte("RandomAddr"))).String(), denom: "usdtoken", }, errArgs{ expectPass: false, contains: "cannot block account that does not exist in state", }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() params := types.NewParams(tc.args.assets) suite.keeper.SetParams(suite.ctx, params) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) blockedAddr, _ := sdk.AccAddressFromBech32(tc.args.blockedAddr) err := suite.keeper.BlockAddress(suite.ctx, tc.args.denom, sender, 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 == 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.Asset sender string blockedAddr string denom string } type errArgs struct { expectPass bool contains string } testCases := []struct { name string args args errArgs errArgs }{ { "valid unblock", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], blockedAddr: suite.addrs[1], denom: "usdtoken", }, errArgs{ expectPass: true, contains: "", }, }, { "non-owner unblock", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[2], blockedAddr: suite.addrs[1], denom: "usdtoken", }, errArgs{ expectPass: false, contains: "account not authorized", }, }, { "invalid denom block", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{suite.addrs[1]}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) blockedAddr, _ := sdk.AccAddressFromBech32(tc.args.blockedAddr) err := suite.keeper.UnblockAddress(suite.ctx, tc.args.denom, sender, 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 == 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.Asset sender string 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.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], startStatus: false, endStatus: true, denom: "usdtoken", }, errArgs{ expectPass: true, contains: "", }, }, { "valid unpause", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, true, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[0], startStatus: true, endStatus: false, denom: "usdtoken", }, errArgs{ expectPass: true, contains: "", }, }, { "non-owner pause", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, sender: suite.addrs[2], startStatus: false, endStatus: true, denom: "usdtoken", }, errArgs{ expectPass: false, contains: "account not authorized", }, }, { "invalid denom pause", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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) sender, _ := sdk.AccAddressFromBech32(tc.args.sender) err := suite.keeper.SetPauseStatus(suite.ctx, 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.Asset initialCoins sdk.Coin blockedAddrs []string denom string } type errArgs struct { expectPass bool contains string } testCases := []struct { name string args args errArgs errArgs }{ { "valid seize", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)), denom: "usdtoken", blockedAddrs: []string{suite.addrs[1], suite.addrs[2]}, }, errArgs{ expectPass: true, contains: "", }, }, { "invalid denom seize", args{ assets: []types.Asset{ types.NewAsset(suite.addrs[0], "usdtoken", []string{}, false, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, initialCoins: sdk.NewCoin("usdtoken", sdk.NewInt(100000000)), denom: "othertoken", blockedAddrs: []string{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.Asset{} 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.tApp.GetBankKeeper() for _, addrStr := range tc.args.blockedAddrs { addr, _ := sdk.AccAddressFromBech32(addrStr) 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) owner, _ := sdk.AccAddressFromBech32(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)), sdk.NewCoins(suite.getBalance(owner, tc.args.initialCoins.Denom))) } 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)) }