package keeper_test import ( "strings" "testing" "time" "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" "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 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) 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) 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, 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) 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, 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.Assets{ 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)), 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, 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.Assets{ 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)), 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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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, 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) 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) 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 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, 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.Assets{ 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)), 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, 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.Assets{ 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)), 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, 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.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, 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.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, 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.Assets{ 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], denom: "othertoken", }, errArgs{ expectPass: false, 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() { 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, 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.Assets{ 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], 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, 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) 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, 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.Assets{ types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, 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.Assets{ types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, 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.Assets{ types.NewAsset(suite.addrs[0], "usdtoken", []sdk.AccAddress{}, 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) 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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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, true, types.NewRateLimit(false, sdk.ZeroInt(), time.Duration(0))), }, 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)) }