diff --git a/modules/apps/transfer/keeper/relay.go b/modules/apps/transfer/keeper/relay.go index 122e4501fae..11021fa5ac2 100644 --- a/modules/apps/transfer/keeper/relay.go +++ b/modules/apps/transfer/keeper/relay.go @@ -100,7 +100,10 @@ func (k Keeper) sendTransfer( for _, coin := range coins { // Using types.UnboundedSpendLimit allows us to send the entire balance of a given denom. if coin.Amount.Equal(types.UnboundedSpendLimit()) { - coin.Amount = k.bankKeeper.GetBalance(ctx, sender, coin.Denom).Amount + coin.Amount = k.bankKeeper.SpendableCoin(ctx, sender, coin.Denom).Amount + if coin.Amount.IsZero() { + return 0, errorsmod.Wrapf(types.ErrInvalidAmount, "empty spendable balance for %s", coin.Denom) + } } token, err := k.tokenFromCoin(ctx, coin) diff --git a/modules/apps/transfer/keeper/relay_test.go b/modules/apps/transfer/keeper/relay_test.go index b1bc29f9ec0..ee7dc52f7ae 100644 --- a/modules/apps/transfer/keeper/relay_test.go +++ b/modules/apps/transfer/keeper/relay_test.go @@ -4,11 +4,14 @@ import ( "errors" "fmt" "strings" + "time" sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" @@ -123,6 +126,62 @@ func (suite *KeeperTestSuite) TestSendTransfer() { }, nil, }, + { + "successful transfer of entire spendable balance with vesting account", + func() { + // create vesting account + vestingAccPrivKey := secp256k1.GenPrivKey() + vestingAccAddress := sdk.AccAddress(vestingAccPrivKey.PubKey().Address()) + + vestingCoins := sdk.NewCoins(sdk.NewCoin(coins[0].Denom, ibctesting.DefaultCoinAmount)) + _, err := suite.chainA.SendMsgs(vestingtypes.NewMsgCreateVestingAccount( + suite.chainA.SenderAccount.GetAddress(), + vestingAccAddress, + vestingCoins, + suite.chainA.GetContext().BlockTime().Add(time.Hour).Unix(), + false, + )) + suite.Require().NoError(err) + sender = vestingAccAddress + + // transfer some spendable coins to vesting account + transferCoins := sdk.NewCoins(sdk.NewCoin(coins[0].Denom, sdkmath.NewInt(42))) + _, err = suite.chainA.SendMsgs(banktypes.NewMsgSend(suite.chainA.SenderAccount.GetAddress(), vestingAccAddress, transferCoins)) + suite.Require().NoError(err) + + coins = sdk.NewCoins(sdk.NewCoin(coins[0].Denom, types.UnboundedSpendLimit())) + expEscrowAmounts[0] = transferCoins[0].Amount + }, + nil, + }, + { + "failure: no spendable coins for vesting account", + func() { + // create vesting account + vestingAccPrivKey := secp256k1.GenPrivKey() + vestingAccAddress := sdk.AccAddress(vestingAccPrivKey.PubKey().Address()) + + vestingCoins := sdk.NewCoins(sdk.NewCoin(coins[0].Denom, ibctesting.DefaultCoinAmount)) + _, err := suite.chainA.SendMsgs(vestingtypes.NewMsgCreateVestingAccount( + suite.chainA.SenderAccount.GetAddress(), + vestingAccAddress, + vestingCoins, + suite.chainA.GetContext().BlockTime().Add(time.Hour).Unix(), + false, + )) + suite.Require().NoError(err) + sender = vestingAccAddress + + // just to prove that the vesting account has a balance (but not spendable) + vestingAccBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), vestingAccAddress, coins[0].Denom) + suite.Require().Equal(vestingCoins[0].Amount.Int64(), vestingAccBalance.Amount.Int64()) + vestinSpendableBalance := suite.chainA.GetSimApp().BankKeeper.SpendableCoins(suite.chainA.GetContext(), vestingAccAddress) + suite.Require().Zero(vestinSpendableBalance.AmountOf(coins[0].Denom).Int64()) + + coins = sdk.NewCoins(sdk.NewCoin(coins[0].Denom, types.UnboundedSpendLimit())) + }, + types.ErrInvalidAmount, + }, { "failure: source channel not found", func() { @@ -233,8 +292,8 @@ func (suite *KeeperTestSuite) TestSendTransfer() { expPass := tc.expError == nil if expPass { - suite.Require().NotNil(res) suite.Require().NoError(err) + suite.Require().NotNil(res) } else { suite.Require().Nil(res) suite.Require().Error(err) diff --git a/modules/apps/transfer/types/expected_keepers.go b/modules/apps/transfer/types/expected_keepers.go index 5125eb43315..2d2dff4c21d 100644 --- a/modules/apps/transfer/types/expected_keepers.go +++ b/modules/apps/transfer/types/expected_keepers.go @@ -30,7 +30,7 @@ type BankKeeper interface { IsSendEnabledCoins(ctx context.Context, coins ...sdk.Coin) error HasDenomMetaData(ctx context.Context, denom string) bool SetDenomMetaData(ctx context.Context, denomMetaData banktypes.Metadata) - GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin + SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins }