From 43f49a9245245c05799154c5da95163ca66c9fc7 Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Thu, 26 Oct 2023 16:04:20 +0300 Subject: [PATCH] Use frozen coins for bank keeper SpendableBalance. (#683) --- integration-tests/modules/assetft_test.go | 219 ++++++++++++++++++++++ x/asset/ft/keeper/keeper.go | 3 + x/wbank/keeper/keeper.go | 55 ++++++ x/wbank/keeper/keeper_test.go | 179 ++++++++++++++++++ x/wbank/types/expected_keepers.go | 1 + 5 files changed, 457 insertions(+) create mode 100644 x/wbank/keeper/keeper_test.go diff --git a/integration-tests/modules/assetft_test.go b/integration-tests/modules/assetft_test.go index c9708c8b5..00d2a823a 100644 --- a/integration-tests/modules/assetft_test.go +++ b/integration-tests/modules/assetft_test.go @@ -311,6 +311,7 @@ func TestBalanceQuery(t *testing.T) { &assetfttypes.MsgIssue{}, &assetfttypes.MsgSetWhitelistedLimit{}, &assetfttypes.MsgFreeze{}, + &assetfttypes.MsgGloballyFreeze{}, &banktypes.MsgSend{}, }, Amount: issueFee, @@ -391,6 +392,224 @@ func TestBalanceQuery(t *testing.T) { assertT.Equal(frozenCoin.Amount.String(), resp.Frozen.String()) assertT.Equal(sendCoin.Amount.String(), resp.Balance.String()) assertT.Equal("0", resp.Locked.String()) + + // freeze globally now + + msgGloballyFreeze := &assetfttypes.MsgGloballyFreeze{ + Sender: issuer.String(), + Denom: denom, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgGloballyFreeze)), + msgGloballyFreeze, + ) + require.NoError(t, err) + + resp, err = ftClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{ + Account: recipient.String(), + Denom: denom, + }) + require.NoError(t, err) + + bankClient := banktypes.NewQueryClient(chain.ClientContext) + recipientBalanceRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: recipient.String(), + Denom: denom, + }) + require.NoError(t, err) + + assertT.Equal(whitelistedCoin.Amount.String(), resp.Whitelisted.String()) + assertT.Equal(recipientBalanceRes.Balance.Amount.String(), resp.Frozen.String()) + assertT.Equal(sendCoin.Amount.String(), resp.Balance.String()) + assertT.Equal("0", resp.Locked.String()) +} + +func TestSpendableBalanceQuery(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + bankClient := banktypes.NewQueryClient(chain.ClientContext) + + issueFee := chain.QueryAssetFTParams(ctx, t).IssueFee.Amount + + issuer := chain.GenAccount() + recipient1 := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetfttypes.MsgIssue{}, + &assetfttypes.MsgIssue{}, + &banktypes.MsgSend{}, + &banktypes.MsgSend{}, + &assetfttypes.MsgFreeze{}, + &assetfttypes.MsgFreeze{}, + &assetfttypes.MsgGloballyFreeze{}, + }, + Amount: issueFee.MulRaw(2), + }) + + // issue the new fungible token form issuer + msgIssue := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "WBTC", + Subunit: "wsatoshi", + Precision: 8, + InitialAmount: sdkmath.NewInt(200), + BurnRate: sdk.NewDec(0), + SendCommissionRate: sdk.NewDec(0), + Features: []assetfttypes.Feature{assetfttypes.Feature_freezing}, + } + _, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgIssue)), + msgIssue, + ) + requireT.NoError(err) + + denom1 := assetfttypes.BuildDenom(msgIssue.Subunit, issuer) + frozenCoin1 := sdk.NewInt64Coin(denom1, 20) + sendCoin1 := sdk.NewInt64Coin(denom1, 100) + + msgSend := &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: recipient1.String(), + Amount: sdk.NewCoins(sendCoin1), + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgSend)), + msgSend, + ) + requireT.NoError(err) + + recipientSpendableBalanceBeforeFreezeRes, err := bankClient.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient1.String(), + Denom: denom1, + }) + requireT.NoError(err) + requireT.Equal(sendCoin1.Amount.String(), recipientSpendableBalanceBeforeFreezeRes.Balance.Amount.String()) + + msgFreeze := &assetfttypes.MsgFreeze{ + Sender: issuer.String(), + Account: recipient1.String(), + Coin: frozenCoin1, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgFreeze)), + msgFreeze, + ) + requireT.NoError(err) + + recipientSpendableBalanceAfterFreezeRes, err := bankClient.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient1.String(), + Denom: denom1, + }) + requireT.NoError(err) + requireT.Equal(sendCoin1.Amount.Sub(frozenCoin1.Amount).String(), recipientSpendableBalanceAfterFreezeRes.Balance.Amount.String()) + + // freeze globally now + msgGloballyFreeze := &assetfttypes.MsgGloballyFreeze{ + Sender: issuer.String(), + Denom: denom1, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgGloballyFreeze)), + msgGloballyFreeze, + ) + requireT.NoError(err) + + recipientSpendableBalanceAfterGlobalFreezeRes, err := bankClient.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient1.String(), + Denom: denom1, + }) + requireT.NoError(err) + requireT.Equal(sdkmath.ZeroInt().String(), recipientSpendableBalanceAfterGlobalFreezeRes.Balance.Amount.String()) + + // issue one more token + msgIssue = &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "WBTC2", + Subunit: "wsatoshi2", + Precision: 8, + InitialAmount: sdkmath.NewInt(200), + BurnRate: sdk.NewDec(0), + SendCommissionRate: sdk.NewDec(0), + Features: []assetfttypes.Feature{assetfttypes.Feature_freezing}, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgIssue)), + msgIssue, + ) + requireT.NoError(err) + + denom2 := assetfttypes.BuildDenom(msgIssue.Subunit, issuer) + frozenCoin2 := sdk.NewInt64Coin(denom2, 20) + sendCoin2 := sdk.NewInt64Coin(denom2, 100) + + msgSend = &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: recipient1.String(), + Amount: sdk.NewCoins(sendCoin2), + } + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgSend)), + msgSend, + ) + requireT.NoError(err) + + recipientSpendableBalancesBeforeFreezeRes, err := bankClient.SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{ + Address: recipient1.String(), + }) + requireT.NoError(err) + requireT.Len(recipientSpendableBalancesBeforeFreezeRes.Balances, 2) + requireT.Equal(sendCoin2.Amount.String(), recipientSpendableBalancesBeforeFreezeRes.Balances.AmountOf(denom2).String()) + + msgFreeze = &assetfttypes.MsgFreeze{ + Sender: issuer.String(), + Account: recipient1.String(), + Coin: frozenCoin2, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgFreeze)), + msgFreeze, + ) + requireT.NoError(err) + + recipientSpendableBalancesBeforeFreezeRes, err = bankClient.SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{ + Address: recipient1.String(), + }) + requireT.NoError(err) + requireT.Equal(sendCoin2.Amount.Sub(frozenCoin2.Amount).String(), recipientSpendableBalancesBeforeFreezeRes.Balances.AmountOf(denom2).String()) + + // check the native denom + recipient2 := chain.GenAccount() + amountToFund := sdkmath.NewInt(100) + chain.FundAccountWithOptions(ctx, t, recipient2, integration.BalancesOptions{ + Amount: amountToFund, + }) + recipient2SpendableBalance, err := bankClient.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient2.String(), + Denom: chain.Chain.ChainSettings.Denom, + }) + requireT.NoError(err) + requireT.Equal(amountToFund.String(), recipient2SpendableBalance.Balance.Amount.String()) } // TestEmptyBalanceQuery tests balance query. diff --git a/x/asset/ft/keeper/keeper.go b/x/asset/ft/keeper/keeper.go index 2e8634412..34dd95d90 100644 --- a/x/asset/ft/keeper/keeper.go +++ b/x/asset/ft/keeper/keeper.go @@ -460,6 +460,9 @@ func (k Keeper) GetFrozenBalances(ctx sdk.Context, addr sdk.AccAddress, paginati // GetFrozenBalance returns the frozen balance of a denom and account. func (k Keeper) GetFrozenBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { + if k.isGloballyFrozen(ctx, denom) { + return k.bankKeeper.GetBalance(ctx, addr, denom) + } return k.frozenAccountBalanceStore(ctx, addr).Balance(denom) } diff --git a/x/wbank/keeper/keeper.go b/x/wbank/keeper/keeper.go index 341ea910c..1ae226183 100644 --- a/x/wbank/keeper/keeper.go +++ b/x/wbank/keeper/keeper.go @@ -1,7 +1,10 @@ package keeper import ( + "context" + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -98,3 +101,55 @@ func (k BaseKeeperWrapper) InputOutputCoins(ctx sdk.Context, inputs []banktypes. return k.BaseKeeper.InputOutputCoins(ctx, inputs, outputs) } + +// ********** Query server ********** + +// SpendableBalances implements a gRPC query handler for retrieving an account's spendable balances including asset ft +// frozen coins. +func (k BaseKeeperWrapper) SpendableBalances(ctx context.Context, req *banktypes.QuerySpendableBalancesRequest) (*banktypes.QuerySpendableBalancesResponse, error) { + res, err := k.BaseKeeper.SpendableBalances(ctx, req) + if err != nil { + return nil, err + } + addr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, sdkerrors.Wrapf(cosmoserrors.ErrInvalidAddress, "invalid address %s", req.Address) + } + for i := range res.Balances { + res.Balances[i] = k.getSpendableCoin(sdk.UnwrapSDKContext(ctx), addr, res.Balances[i]) + } + + return res, nil +} + +// SpendableBalanceByDenom implements a gRPC query handler for retrieving an account's spendable balance for a specific +// denom, including asset ft frozen coins. +func (k BaseKeeperWrapper) SpendableBalanceByDenom(ctx context.Context, req *banktypes.QuerySpendableBalanceByDenomRequest) (*banktypes.QuerySpendableBalanceByDenomResponse, error) { + res, err := k.BaseKeeper.SpendableBalanceByDenom(ctx, req) + if err != nil { + return nil, err + } + addr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, sdkerrors.Wrapf(cosmoserrors.ErrInvalidAddress, "invalid address %s", req.Address) + } + if res.Balance == nil { + return res, nil + } + + spendableCoin := k.getSpendableCoin(sdk.UnwrapSDKContext(ctx), addr, *res.Balance) + res.Balance = &spendableCoin + + return res, nil +} + +func (k BaseKeeperWrapper) getSpendableCoin(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) sdk.Coin { + denom := coin.Denom + frozenCoin := k.ftProvider.GetFrozenBalance(ctx, addr, denom) + spendableAmount := coin.Amount.Sub(frozenCoin.Amount) + if spendableAmount.IsNegative() { + return sdk.NewCoin(denom, sdkmath.ZeroInt()) + } + + return sdk.NewCoin(denom, spendableAmount) +} diff --git a/x/wbank/keeper/keeper_test.go b/x/wbank/keeper/keeper_test.go new file mode 100644 index 000000000..afd1c7cbf --- /dev/null +++ b/x/wbank/keeper/keeper_test.go @@ -0,0 +1,179 @@ +package keeper_test + +import ( + "fmt" + "testing" + + sdkmath "cosmossdk.io/math" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/stretchr/testify/require" + + "github.com/CoreumFoundation/coreum/v3/testutil/simapp" + "github.com/CoreumFoundation/coreum/v3/x/asset/ft/types" +) + +func TestBaseKeeperWrapper_SpendableBalances(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContext(false, tmproto.Header{}) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + totalTokens := 10 + amountToSend := sdkmath.NewInt(100) + denoms := make([]string, 0, totalTokens) + for i := 0; i < totalTokens; i++ { + settings := types.IssueSettings{ + Issuer: issuer, + Symbol: fmt.Sprintf("DEF%d", i), + Subunit: fmt.Sprintf("def%d", i), + Precision: 1, + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{types.Feature_freezing}, + } + denom, err := ftKeeper.Issue(ctx, settings) + requireT.NoError(err) + denoms = append(denoms, denom) + + coinToSend := sdk.NewCoin(denom, amountToSend) + err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins( + coinToSend, + )) + requireT.NoError(err) + } + + balances := bankKeeper.GetAllBalances(ctx, recipient) + spendableBalancesRes, err := bankKeeper.SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{ + Address: recipient.String(), + }) + requireT.NoError(err) + requireT.Equal(balances.String(), spendableBalancesRes.Balances.String()) + + denom := denoms[5] + // freeze tokens + coinToFreeze := sdk.NewCoin(denom, sdkmath.NewInt(10)) + err = ftKeeper.Freeze(ctx, issuer, recipient, coinToFreeze) + requireT.NoError(err) + + // check that after the freezing the spendable balance is different + spendableBalancesRes, err = bankKeeper.SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{ + Address: recipient.String(), + }) + requireT.NoError(err) + requireT.Equal( + balances.AmountOf(denom).Sub(coinToFreeze.Amount).String(), + spendableBalancesRes.Balances.AmountOf(denom).String(), + ) + + // check with global freeze + err = ftKeeper.GloballyFreeze(ctx, issuer, denom) + requireT.NoError(err) + spendableBalancesRes, err = bankKeeper.SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{ + Address: recipient.String(), + }) + requireT.NoError(err) + requireT.Equal( + sdk.ZeroInt().String(), + spendableBalancesRes.Balances.AmountOf(denom).String(), + ) +} + +func TestBaseKeeperWrapper_SpendableBalanceByDenom(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContext(false, tmproto.Header{}) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 1, + Description: "DEF Desc", + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{types.Feature_freezing}, + } + + denom, err := ftKeeper.Issue(ctx, settings) + requireT.NoError(err) + + recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + coinToSend := sdk.NewCoin(denom, sdkmath.NewInt(100)) + err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins( + coinToSend, + )) + requireT.NoError(err) + + // check that before the freezing the balance is correct + balance := bankKeeper.GetBalance(ctx, recipient, denom) + requireT.Equal(coinToSend, balance) + spendableBalanceRes, err := bankKeeper.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient.String(), + Denom: denom, + }) + requireT.NoError(err) + + requireT.Equal(balance.String(), spendableBalanceRes.Balance.String()) + + // freeze tokens + coinToFreeze := sdk.NewCoin(denom, sdkmath.NewInt(10)) + err = ftKeeper.Freeze(ctx, issuer, recipient, coinToFreeze) + requireT.NoError(err) + + // check that after the freezing the balance is the same + balance = bankKeeper.GetBalance(ctx, recipient, denom) + requireT.Equal(coinToSend, balance) + + // check that after the freezing the spendable balance is different + spendableBalanceRes, err = bankKeeper.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient.String(), + Denom: denom, + }) + requireT.NoError(err) + requireT.Equal(balance.Sub(coinToFreeze).String(), spendableBalanceRes.Balance.String()) + + // freeze globally + err = ftKeeper.GloballyFreeze(ctx, issuer, denom) + requireT.NoError(err) + // check that it is fully frozen now + spendableBalanceRes, err = bankKeeper.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient.String(), + Denom: denom, + }) + requireT.NoError(err) + requireT.Equal(sdkmath.ZeroInt().String(), spendableBalanceRes.Balance.Amount.String()) + + // query for the non-existing denom + spendableBalanceRes, err = bankKeeper.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ + Address: recipient.String(), + Denom: "nondenom", + }) + requireT.NoError(err) + requireT.Equal(sdkmath.ZeroInt().String(), spendableBalanceRes.Balance.Amount.String()) + + // tests native denom + nativeDenom := "ucore" + coinToMindAndSend := sdk.NewCoin(nativeDenom, sdkmath.NewInt(100)) + err = bankKeeper.MintCoins(ctx, minttypes.ModuleName, sdk.NewCoins(coinToMindAndSend)) + requireT.NoError(err) + err = bankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, recipient, sdk.NewCoins( + coinToMindAndSend, + )) + requireT.NoError(err) + balance = bankKeeper.GetBalance(ctx, recipient, nativeDenom) + requireT.Equal(coinToMindAndSend, balance) +} diff --git a/x/wbank/types/expected_keepers.go b/x/wbank/types/expected_keepers.go index 808ab3ce6..fcd563b3a 100644 --- a/x/wbank/types/expected_keepers.go +++ b/x/wbank/types/expected_keepers.go @@ -9,4 +9,5 @@ import ( type FungibleTokenProvider interface { BeforeSendCoins(ctx sdk.Context, fromAddress, toAddress sdk.AccAddress, coins sdk.Coins) error BeforeInputOutputCoins(ctx sdk.Context, inputs []banktypes.Input, outputs []banktypes.Output) error + GetFrozenBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin }