From 8a6828de219317812e3cf021a714255ca3657171 Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Fri, 29 Nov 2024 12:45:58 +0300 Subject: [PATCH] Update extensions integration to check asset FT rules before calling the extensions. (#1033) --- Makefile | 4 + .../ibc/asset_extension_ft_test.go | 68 +++- .../modules/assetft_extension_test.go | 102 +----- x/asset/ft/keeper/before_send.go | 69 ++-- x/asset/ft/keeper/keeper.go | 13 +- x/asset/ft/keeper/keeper_dex.go | 71 ++-- x/asset/ft/keeper/keeper_dex_test.go | 142 ++++---- ...t.go => keeper_extension_transfer_test.go} | 319 +++++++----------- .../asset-extension/src/contract.rs | 106 +----- .../asset-extension/src/error.rs | 6 - x/asset/ft/types/token.go | 12 +- x/asset/ft/types/token_test.go | 10 +- 12 files changed, 369 insertions(+), 553 deletions(-) rename x/asset/ft/keeper/{keeper_extension_test.go => keeper_extension_transfer_test.go} (81%) diff --git a/Makefile b/Makefile index e771943cd..0dfca5a80 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ znet: znet-start: $(BUILDER) znet start --profiles=3cored +.PHONY: znet-start-ibc +znet-start-ibc: + $(BUILDER) znet start --profiles=3cored,ibc + .PHONY: znet-start-stress znet-start-stress: $(BUILDER) znet start --profiles=3cored,dex diff --git a/integration-tests/ibc/asset_extension_ft_test.go b/integration-tests/ibc/asset_extension_ft_test.go index 8f5a1701d..de984d438 100644 --- a/integration-tests/ibc/asset_extension_ft_test.go +++ b/integration-tests/ibc/asset_extension_ft_test.go @@ -33,7 +33,7 @@ const ( AmountBlockIBCTrigger = 110 ) -func TestExtensionIBCFailsIfNotEnabled(t *testing.T) { +func TestExtensionIBCFailsWithIBCProhibitedAmount(t *testing.T) { t.Parallel() requireT := require.New(t) @@ -62,6 +62,7 @@ func TestExtensionIBCFailsIfNotEnabled(t *testing.T) { InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ assetfttypes.Feature_extension, + assetfttypes.Feature_ibc, }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ CodeId: codeID, @@ -89,6 +90,62 @@ func TestExtensionIBCFailsIfNotEnabled(t *testing.T) { requireT.ErrorContains(err, "IBC feature is disabled.") } +func TestExtensionIBCFailsIfNotEnabled(t *testing.T) { + t.Parallel() + + requireT := require.New(t) + + ctx, chains := integrationtests.NewChainsTestingContext(t) + coreumChain := chains.Coreum + coreumIssuer := coreumChain.GenAccount() + + issueFee := coreumChain.QueryAssetFTParams(ctx, t).IssueFee.Amount + coreumChain.FundAccountWithOptions(ctx, t, coreumIssuer, integration.BalancesOptions{ + Amount: issueFee. + Add(sdkmath.NewInt(1_000_000)). // added one million for contract upload. + Add(sdkmath.NewInt(2 * 500_000)), + }) + + codeID, err := chains.Coreum.Wasm.DeployWASMContract( + ctx, chains.Coreum.TxFactory().WithSimulateAndExecute(true), coreumIssuer, testcontracts.AssetExtensionWasm, + ) + requireT.NoError(err) + + issueMsg := &assetfttypes.MsgIssue{ + Issuer: coreumIssuer.String(), + Symbol: "mysymbol", + Subunit: "mysubunit", + Precision: 8, + InitialAmount: sdkmath.NewInt(1_000_000), + Features: []assetfttypes.Feature{ + assetfttypes.Feature_extension, + }, + ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ + CodeId: codeID, + Label: "testing-ibc", + }, + } + _, err = client.BroadcastTx( + ctx, + coreumChain.ClientContext.WithFromAddress(coreumIssuer), + coreumChain.TxFactoryAuto(), + issueMsg, + ) + require.NoError(t, err) + + gaiaChain := chains.Gaia + _, err = coreumChain.ExecuteIBCTransfer( + ctx, + t, + coreumChain.TxFactory().WithGas(500_000), + coreumIssuer, + sdk.NewCoin(assetfttypes.BuildDenom(issueMsg.Subunit, coreumIssuer), sdkmath.NewInt(10)), + gaiaChain.ChainContext, + gaiaChain.GenAccount(), + ) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) +} + func TestExtensionIBCAssetFTWhitelisting(t *testing.T) { t.Parallel() @@ -132,6 +189,7 @@ func TestExtensionIBCAssetFTWhitelisting(t *testing.T) { Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_whitelisting, assetfttypes.Feature_extension, }, @@ -396,6 +454,7 @@ func TestExtensionEscrowAddressIsResistantToFreezingAndWhitelisting(t *testing.T Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_extension, assetfttypes.Feature_freezing, assetfttypes.Feature_whitelisting, @@ -520,6 +579,7 @@ func TestExtensionIBCAssetFTTimedOutTransfer(t *testing.T) { Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_extension, }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ @@ -655,6 +715,7 @@ func TestExtensionIBCAssetFTRejectedTransfer(t *testing.T) { Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_freezing, assetfttypes.Feature_extension, }, @@ -795,6 +856,7 @@ func TestExtensionIBCAssetFTSendCommissionAndBurnRate(t *testing.T) { BurnRate: sdkmath.LegacyMustNewDecFromStr("0.1"), SendCommissionRate: sdkmath.LegacyMustNewDecFromStr("0.2"), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_extension, }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ @@ -1008,6 +1070,7 @@ func TestExtensionIBCRejectedTransferWithWhitelistingAndFreezing(t *testing.T) { Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_freezing, assetfttypes.Feature_whitelisting, assetfttypes.Feature_extension, @@ -1158,6 +1221,7 @@ func TestExtensionIBCTimedOutTransferWithWhitelistingAndFreezing(t *testing.T) { Precision: 8, InitialAmount: sdkmath.NewInt(1_000_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_whitelisting, assetfttypes.Feature_freezing, assetfttypes.Feature_extension, @@ -1344,6 +1408,7 @@ func TestExtensionIBCRejectedTransferWithBurnRateAndSendCommission(t *testing.T) Precision: 8, InitialAmount: sdkmath.NewInt(910_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_extension, }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ @@ -1479,6 +1544,7 @@ func TestExtensionIBCTimedOutTransferWithBurnRateAndSendCommission(t *testing.T) Precision: 8, InitialAmount: sdkmath.NewInt(910_000), Features: []assetfttypes.Feature{ + assetfttypes.Feature_ibc, assetfttypes.Feature_extension, }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ diff --git a/integration-tests/modules/assetft_extension_test.go b/integration-tests/modules/assetft_extension_test.go index 6cc650637..c0adfcb62 100644 --- a/integration-tests/modules/assetft_extension_test.go +++ b/integration-tests/modules/assetft_extension_test.go @@ -11,6 +11,7 @@ import ( wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" + cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" authztypes "github.com/cosmos/cosmos-sdk/x/authz" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/samber/lo" @@ -29,8 +30,6 @@ import ( var ( AmountDisallowedTrigger = sdkmath.NewInt(7) - AmountIgnoreWhitelistingTrigger = sdkmath.NewInt(49) - AmountIgnoreFreezingTrigger = sdkmath.NewInt(79) AmountBurningTrigger = sdkmath.NewInt(101) AmountMintingTrigger = sdkmath.NewInt(105) AmountIgnoreBurnRateTrigger = sdkmath.NewInt(108) @@ -198,7 +197,6 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { clientCtx := chain.ClientContext ftClient := assetfttypes.NewQueryClient(clientCtx) - bankClient := banktypes.NewQueryClient(clientCtx) issuer := chain.GenAccount() nonIssuer := chain.GenAccount() @@ -290,7 +288,7 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { chain.TxFactory().WithGas(500_000), sendMsg, ) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") + requireT.ErrorIs(err, assetfttypes.ErrWhitelistedLimitExceeded) // multi-send multiSendMsg := &banktypes.MsgMultiSend{ @@ -300,10 +298,10 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { _, err = client.BroadcastTx( ctx, chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), + chain.TxFactory().WithGas(500_000), multiSendMsg, ) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") + requireT.ErrorIs(err, assetfttypes.ErrWhitelistedLimitExceeded) // multi-send tokens with and without extension multiSendMsg = &banktypes.MsgMultiSend{ @@ -319,10 +317,10 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { _, err = client.BroadcastTx( ctx, chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), + chain.TxFactory().WithGas(500_000), multiSendMsg, ) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") + requireT.ErrorIs(err, assetfttypes.ErrWhitelistedLimitExceeded) // whitelist 400 tokens whitelistMsg := &assetfttypes.MsgSetWhitelistedLimit{ @@ -353,22 +351,7 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { requireT.NoError(err) requireT.EqualValues(sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(400))), whitelistedBalances.Balances) - // try to receive more than whitelisted (600) (possible 400) - sendMsg = &banktypes.MsgSend{ - FromAddress: issuer.String(), - ToAddress: recipient.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(600))), - } - _, err = client.BroadcastTx( - ctx, - chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), - sendMsg, - ) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - requireT.NotEqualValues(chain.GasLimitByMsgs(sendMsg), res.GasUsed) - - // try to send whitelisted balance (400) + // reverse whitelisted amount sendMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: recipient.String(), @@ -381,65 +364,6 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { sendMsg, ) requireT.NoError(err) - requireT.NotEqualValues(chain.GasLimitByMsgs(sendMsg), res.GasUsed) - balance, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ - Address: recipient.String(), - Denom: denom, - }) - requireT.NoError(err) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(400)).String(), balance.GetBalance().String()) - - // try to send one more - sendMsg = &banktypes.MsgSend{ - FromAddress: issuer.String(), - ToAddress: recipient.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(1))), - } - _, err = client.BroadcastTx( - ctx, - chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), - sendMsg, - ) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - requireT.NotEqualValues(chain.GasLimitByMsgs(sendMsg), res.GasUsed) - - // try to send trigger amount despite the whitelisted limit - sendMsg = &banktypes.MsgSend{ - FromAddress: issuer.String(), - ToAddress: recipient.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger)), - } - _, err = client.BroadcastTx( - ctx, - chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), - sendMsg, - ) - requireT.NoError(err) - requireT.NotEqualValues(chain.GasLimitByMsgs(sendMsg), res.GasUsed) - - // try to send trigger amount via Multisend - multiSendMsg = &banktypes.MsgMultiSend{ - Inputs: []banktypes.Input{{Address: issuer.String(), Coins: sdk.NewCoins( - sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger), - sdk.NewCoin(denomWithoutExtension, sdkmath.NewInt(10)), - chain.NewCoin(sdkmath.NewInt(10)), - )}}, - Outputs: []banktypes.Output{{Address: recipient.String(), Coins: sdk.NewCoins( - sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger), - sdk.NewCoin(denomWithoutExtension, sdkmath.NewInt(10)), - chain.NewCoin(sdkmath.NewInt(10)), - )}}, - } - res, err = client.BroadcastTx( - ctx, - chain.ClientContext.WithFromAddress(issuer), - chain.TxFactoryAuto(), - multiSendMsg, - ) - requireT.NoError(err) - requireT.NotEqualValues(chain.GasLimitByMsgs(multiSendMsg), res.GasUsed) } // TestAssetFTExtensionFreeze checks extension freeze functionality of fungible tokens. @@ -575,7 +499,7 @@ func TestAssetFTExtensionFreeze(t *testing.T) { chain.TxFactory().WithGas(500_000), sendMsg, ) - requireT.ErrorContains(err, "Requested transfer token is frozen.") + requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) // multi-send multiSendMsg := &banktypes.MsgMultiSend{ Inputs: []banktypes.Input{{Address: recipient.String(), Coins: coinsToSend}}, @@ -587,21 +511,21 @@ func TestAssetFTExtensionFreeze(t *testing.T) { chain.TxFactory().WithGas(500_000), multiSendMsg, ) - requireT.ErrorContains(err, "Requested transfer token is frozen.") - // send trigger amount despite frozen amount + requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) + // send allowed amount + coinsToSend = sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(75))) sendMsg = &banktypes.MsgSend{ FromAddress: recipient.String(), ToAddress: recipient2.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreFreezingTrigger)), + Amount: coinsToSend, } - res, err = client.BroadcastTx( + _, err = client.BroadcastTx( ctx, chain.ClientContext.WithFromAddress(recipient), chain.TxFactoryAuto(), sendMsg, ) requireT.NoError(err) - requireT.NotEqualValues(chain.GasLimitByMsgs(sendMsg), res.GasUsed) } // TestAssetFTExtensionBurn checks extension burn functionality of fungible tokens. diff --git a/x/asset/ft/keeper/before_send.go b/x/asset/ft/keeper/before_send.go index 67f5853f3..d9918c719 100644 --- a/x/asset/ft/keeper/before_send.go +++ b/x/asset/ft/keeper/before_send.go @@ -79,8 +79,7 @@ func (k Keeper) applyFeatures(ctx sdk.Context, input banktypes.Input, outputs [] } if def == nil { // if the token doesn't have the definition we validate DEX locking rule only. - balance := k.bankKeeper.GetBalance(ctx, sender, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, sender, balance, coin); err != nil { + if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, sender, coin); err != nil { return err } @@ -110,33 +109,10 @@ func (k Keeper) applyFeatures(ctx sdk.Context, input banktypes.Input, outputs [] burnAmount := k.CalculateRate(ctx, def.BurnRate, sender, coin) commissionAmount := k.CalculateRate(ctx, def.SendCommissionRate, sender, coin) - if def.IsFeatureEnabled(types.Feature_extension) { - balance := k.bankKeeper.GetBalance(ctx, sender, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, sender, balance, coin); err != nil { - return err - } - - if err := k.invokeAssetExtension(ctx, sender, recipient, *def, coin, commissionAmount, burnAmount); err != nil { - return err - } - // We will not enforce any policies(e.g whitelisting, burn rate), apart from DEX locking, or perform bank - // transfers if the token has extensions. It is up to the contract to enforce them as needed. - // As a result we will skip the next operations in this for loop. - continue - } - senderOrReceiverIsAdmin := def.Admin == sender.String() || def.Admin == recipient.String() - if !senderOrReceiverIsAdmin && commissionAmount.IsPositive() { - adminAddr := sdk.MustAccAddressFromBech32(def.Admin) - commissionCoin := sdk.NewCoins(sdk.NewCoin(def.Denom, commissionAmount)) - if err := k.bankKeeper.SendCoins(ctx, sender, adminAddr, commissionCoin); err != nil { - return err - } - } - - if !senderOrReceiverIsAdmin && burnAmount.IsPositive() { - if err := k.burnIfSpendable(ctx, sender, *def, burnAmount); err != nil { + if !senderOrReceiverIsAdmin && !def.IsFeatureEnabled(types.Feature_extension) { + if err := k.applyCommissionAndBurnRate(ctx, sender, def, commissionAmount, burnAmount); err != nil { return err } } @@ -149,6 +125,15 @@ func (k Keeper) applyFeatures(ctx sdk.Context, input banktypes.Input, outputs [] return err } + if def.IsFeatureEnabled(types.Feature_extension) { + if err := k.invokeAssetExtensionExtensionTransferMethod( + ctx, sender, recipient, *def, coin, commissionAmount, burnAmount, + ); err != nil { + return err + } + continue + } + if err := k.bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coin)); err != nil { return err } @@ -161,10 +146,36 @@ func (k Keeper) applyFeatures(ctx sdk.Context, input banktypes.Input, outputs [] return nil } -// invokeAssetExtension calls the smart contract of the extension. This smart contract is +func (k Keeper) applyCommissionAndBurnRate( + ctx sdk.Context, + sender sdk.AccAddress, + def *types.Definition, + commissionAmount, burnAmount sdkmath.Int, +) error { + if commissionAmount.IsPositive() { + adminAddr, err := sdk.AccAddressFromBech32(def.Admin) + if err != nil { + return err + } + commissionCoin := sdk.NewCoins(sdk.NewCoin(def.Denom, commissionAmount)) + if err := k.bankKeeper.SendCoins(ctx, sender, adminAddr, commissionCoin); err != nil { + return err + } + } + + if burnAmount.IsPositive() { + if err := k.burnIfSpendable(ctx, sender, *def, burnAmount); err != nil { + return err + } + } + + return nil +} + +// invokeAssetExtensionExtensionTransferMethod calls the smart contract of the extension. This smart contract is // responsible to enforce any policies and do the final tranfer. The amount attached to the call // is the send amount plus the burn and commission amount. -func (k Keeper) invokeAssetExtension( +func (k Keeper) invokeAssetExtensionExtensionTransferMethod( ctx sdk.Context, sender sdk.AccAddress, recipient sdk.AccAddress, diff --git a/x/asset/ft/keeper/keeper.go b/x/asset/ft/keeper/keeper.go index 2dd58f506..1cdaa5193 100644 --- a/x/asset/ft/keeper/keeper.go +++ b/x/asset/ft/keeper/keeper.go @@ -746,10 +746,8 @@ func (k Keeper) GetSpendableBalance( if err != nil { return sdk.Coin{}, err } - // the spendable balance counts the frozen balance, but if extensions are not enabled - if def != nil && - def.IsFeatureEnabled(types.Feature_freezing) && - !def.IsFeatureEnabled(types.Feature_extension) { + // the spendable balance counts the frozen balance + if def != nil && def.IsFeatureEnabled(types.Feature_freezing) { frozenBalance, err := k.GetFrozenBalance(ctx, addr, denom) if err != nil { return sdk.Coin{}, err @@ -950,8 +948,7 @@ func (k Keeper) validateCoinSpendable( ) } - balance := k.bankKeeper.GetBalance(ctx, addr, def.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, balance, sdk.NewCoin(def.Denom, amount)); err != nil { + if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, sdk.NewCoin(def.Denom, amount)); err != nil { return err } @@ -961,6 +958,7 @@ func (k Keeper) validateCoinSpendable( return err } frozenAmt := frozenBalance.Amount + balance := k.bankKeeper.GetBalance(ctx, addr, def.Denom) notFrozenAmt := balance.Amount.Sub(frozenAmt) if notFrozenAmt.LT(amount) { return sdkerrors.Wrapf(cosmoserrors.ErrInsufficientFunds, "%s%s is not available, available %s%s", @@ -1210,8 +1208,7 @@ func (k Keeper) validateClawbackAllowed(ctx sdk.Context, sender, addr sdk.AccAdd return sdkerrors.Wrap(cosmoserrors.ErrUnauthorized, "claw back from module accounts is prohibited") } - balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, balance, coin); err != nil { + if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, coin); err != nil { return err } diff --git a/x/asset/ft/keeper/keeper_dex.go b/x/asset/ft/keeper/keeper_dex.go index 2ed0e7e50..4f7fc78b2 100644 --- a/x/asset/ft/keeper/keeper_dex.go +++ b/x/asset/ft/keeper/keeper_dex.go @@ -190,7 +190,7 @@ func (k Keeper) DEXIncreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddres ) } - shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, coin.Denom) + shouldRecord, err := k.shouldRecordDEXExpectedToReceiveBalance(ctx, coin.Denom) if err != nil { return err } @@ -224,7 +224,7 @@ func (k Keeper) DEXDecreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddres ) } - shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, coin.Denom) + shouldRecord, err := k.shouldRecordDEXExpectedToReceiveBalance(ctx, coin.Denom) if err != nil { return err } @@ -291,8 +291,7 @@ func (k Keeper) DEXIncreaseLocked(ctx sdk.Context, addr sdk.AccAddress, coin sdk return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "amount to lock DEX tokens must be positive") } - balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, balance, coin); err != nil { + if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, coin); err != nil { return sdkerrors.Wrapf(types.ErrDEXInsufficientSpendableBalance, "%s", err) } @@ -395,8 +394,7 @@ func (k Keeper) dexCheckExpectedToSpend( expectedToSpend, expectedToReceive sdk.Coin, ) error { // validate that the order creator has enough balance, for both extension and non-extension coin - balance := k.bankKeeper.GetBalance(ctx, order.Creator, expectedToSpend.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, order.Creator, balance, expectedToSpend); err != nil { + if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, order.Creator, expectedToSpend); err != nil { return sdkerrors.Wrapf(types.ErrDEXInsufficientSpendableBalance, "%s", err) } @@ -409,36 +407,22 @@ func (k Keeper) dexCheckExpectedToSpend( return nil } - if spendDef.IsFeatureEnabled(types.Feature_extension) { - extensionContract, err := sdk.AccAddressFromBech32(spendDef.ExtensionCWAddress) - if err != nil { - return err - } - return k.dexCallExtensionPlaceOrder( - ctx, extensionContract, order, expectedToSpend, expectedToReceive, - ) - } - if err := k.dexChecksForDenom(ctx, order.Creator, spendDef, expectedToReceive.Denom); err != nil { return err } - if spendDef.IsFeatureEnabled(types.Feature_freezing) && !spendDef.HasAdminPrivileges(order.Creator) { - frozenCoin, err := k.GetFrozenBalance(ctx, order.Creator, expectedToSpend.Denom) + if err := k.validateCoinSpendable(ctx, order.Creator, *spendDef, expectedToSpend.Amount); err != nil { + return sdkerrors.Wrapf(types.ErrDEXInsufficientSpendableBalance, "err: %s", err) + } + + if spendDef.IsFeatureEnabled(types.Feature_extension) { + extensionContract, err := sdk.AccAddressFromBech32(spendDef.ExtensionCWAddress) if err != nil { return err } - frozenAmt := frozenCoin.Amount - notFrozenTotalAmt := balance.Amount.Sub(frozenAmt) - if notFrozenTotalAmt.LT(expectedToSpend.Amount) { - return sdkerrors.Wrapf( - types.ErrDEXInsufficientSpendableBalance, - "failed to DEX lock %s available %s%s", - expectedToSpend.String(), - notFrozenTotalAmt, - expectedToSpend.Denom, - ) - } + return k.invokeAssetExtensionPlaceOrderMethod( + ctx, extensionContract, order, expectedToSpend, expectedToReceive, + ) } return nil @@ -457,30 +441,28 @@ func (k Keeper) dexCheckExpectedToReceive( return nil } - if receiveDef.IsFeatureEnabled(types.Feature_extension) { - extensionContract, err := sdk.AccAddressFromBech32(receiveDef.ExtensionCWAddress) - if err != nil { - return err - } - return k.dexCallExtensionPlaceOrder( - ctx, extensionContract, order, expectedToSpend, expectedToReceive, - ) + if err := k.dexChecksForDenom(ctx, order.Creator, receiveDef, expectedToSpend.Denom); err != nil { + return err } - if err := k.dexChecksForDenom(ctx, order.Creator, receiveDef, expectedToSpend.Denom); err != nil { + if err := k.validateCoinReceivable(ctx, order.Creator, *receiveDef, expectedToReceive.Amount); err != nil { return err } - if receiveDef.IsFeatureEnabled(types.Feature_whitelisting) && !receiveDef.HasAdminPrivileges(order.Creator) { - if err := k.validateWhitelistedBalance(ctx, order.Creator, expectedToReceive); err != nil { + if receiveDef.IsFeatureEnabled(types.Feature_extension) { + extensionContract, err := sdk.AccAddressFromBech32(receiveDef.ExtensionCWAddress) + if err != nil { return err } + return k.invokeAssetExtensionPlaceOrderMethod( + ctx, extensionContract, order, expectedToSpend, expectedToReceive, + ) } return nil } -func (k Keeper) dexCallExtensionPlaceOrder( +func (k Keeper) invokeAssetExtensionPlaceOrderMethod( ctx sdk.Context, extensionContract sdk.AccAddress, order types.DEXOrder, @@ -647,8 +629,9 @@ func (k Keeper) updateDEXSettings( func (k Keeper) validateCoinIsNotLockedByDEXAndBank( ctx sdk.Context, addr sdk.AccAddress, - balance, coin sdk.Coin, + coin sdk.Coin, ) error { + balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) dexLockedAmt := k.GetDEXLockedBalance(ctx, addr, coin.Denom).Amount availableAmt := balance.Amount.Sub(dexLockedAmt) if availableAmt.LT(coin.Amount) { @@ -679,13 +662,13 @@ func (k Keeper) dexExpectedToReceiveAccountBalanceStore(ctx sdk.Context, addr sd return newBalanceStore(k.cdc, runtime.KVStoreAdapter(store), types.CreateDEXExpectedToReceiveBalancesKey(addr)) } -func (k Keeper) shouldRecordExpectedToReceiveBalance(ctx sdk.Context, denom string) (bool, error) { +func (k Keeper) shouldRecordDEXExpectedToReceiveBalance(ctx sdk.Context, denom string) (bool, error) { def, err := k.getDefinitionOrNil(ctx, denom) if err != nil { return false, err } // increase for FT with the whitelisting enabled only - if def != nil && (def.IsFeatureEnabled(types.Feature_whitelisting) || def.IsFeatureEnabled(types.Feature_extension)) { + if def != nil && def.IsFeatureEnabled(types.Feature_whitelisting) { return true, nil } diff --git a/x/asset/ft/keeper/keeper_dex_test.go b/x/asset/ft/keeper/keeper_dex_test.go index d11d694ad..a4f304f8b 100644 --- a/x/asset/ft/keeper/keeper_dex_test.go +++ b/x/asset/ft/keeper/keeper_dex_test.go @@ -26,6 +26,47 @@ import ( cwasmtypes "github.com/CoreumFoundation/coreum/v5/x/wasm/types" ) +func TestKeeper_ValidateSpendableNotFT(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContext(false) + + ftKeeper := testApp.AssetFTKeeper + + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + coinToSend := sdk.NewInt64Coin(denom1, 1000) + requireT.NoError(testApp.FundAccount(ctx, acc, sdk.NewCoins(coinToSend))) + + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinToSend, + sdk.NewInt64Coin(denom1, 0), + ), + ) + + // lock a half + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, sdk.NewInt64Coin(denom1, 500))) + + err := ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinToSend, + sdk.NewInt64Coin(denom1, 0), + ) + // validate one more time + requireT.ErrorIs(err, types.ErrDEXInsufficientSpendableBalance) + requireT.ErrorContains( + err, + fmt.Sprintf( + "%s is not available, available %s", coinToSend.String(), + sdk.NewInt64Coin(denom1, 500).String(), + ), + ) +} + func TestKeeper_DEXExpectedToReceive(t *testing.T) { requireT := require.New(t) @@ -120,36 +161,6 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, whitelistingDenom).IsZero()) // send without decreased amount requireT.NoError(bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend))) - - // extension - codeID, _, err := testApp.WasmPermissionedKeeper.Create( - ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, - ) - requireT.NoError(err) - settingsWithExtension := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEFEXT", - Subunit: "defext", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{types.Feature_extension}, - ExtensionSettings: &types.ExtensionIssueSettings{ - CodeId: codeID, - }, - } - extensionDenom, err := ftKeeper.Issue(ctx, settingsWithExtension) - requireT.NoError(err) - coinToIncreaseExpectedToReceive = sdk.NewCoin(extensionDenom, sdkmath.NewInt(1)) - requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) - requireT.Equal( - coinToIncreaseExpectedToReceive.String(), - ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, extensionDenom).String(), - ) - - requireT.NoError(ftKeeper.DEXDecreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) - requireT.True( - ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, extensionDenom).IsZero(), - ) } func TestKeeper_DEXLocked(t *testing.T) { @@ -326,39 +337,6 @@ func TestKeeper_DEXLocked(t *testing.T) { requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, sdk.NewInt64Coin(denom, 350))) // freeze more than balance requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, sdk.NewInt64Coin(denom, 1_000_000))) - - // check freezing with extensions - - // extension - codeID, _, err := testApp.WasmPermissionedKeeper.Create( - ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, - ) - requireT.NoError(err) - settingsWithExtension := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEFEXT", - Subunit: "defext", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_freezing, - types.Feature_extension, - }, - ExtensionSettings: &types.ExtensionIssueSettings{ - CodeId: codeID, - }, - } - extensionDenom, err := ftKeeper.Issue(ctx, settingsWithExtension) - requireT.NoError(err) - coinWithExtensionToSend := sdk.NewCoin(extensionDenom, sdkmath.NewInt(33)) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(coinWithExtensionToSend))) - - requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, coinWithExtensionToSend)) - - spendableBalance, err = ftKeeper.GetSpendableBalance(ctx, acc, extensionDenom) - requireT.NoError(err) - // the balance is frozen, not we don't count it as spendable, since the extensions control it - requireT.Equal(coinWithExtensionToSend.String(), spendableBalance.String()) } func TestKeeper_DEXBlockSmartContracts(t *testing.T) { @@ -1079,19 +1057,21 @@ func TestKeeper_DEXExtensions(t *testing.T) { // send coins now to lock requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(coinWithExtension))) - // freeze locked balance, to check that is extension controls the freezing, but asset FT ignores + // freeze locked balance, to check that we check freezing for extension as well requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, coinWithExtension)) - requireT.NoError( + requireT.ErrorContains( ftKeeper.DEXCheckOrderAmounts( - ctx, + simapp.CopyContextWithMultiStore(ctx), types.DEXOrder{Creator: acc}, coinWithExtension, coinWithoutExtension, ), + fmt.Sprintf("%s is not available, available 0%s", coinWithExtension.String(), coinWithExtension.Denom), ) - requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinWithExtension)) + requireT.NoError(ftKeeper.Unfreeze(ctx, issuer, acc, coinWithExtension)) // try with locked balance + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinWithExtension)) requireT.ErrorIs( ftKeeper.DEXCheckOrderAmounts( simapp.CopyContextWithMultiStore(ctx), @@ -1118,6 +1098,23 @@ func TestKeeper_DEXExtensions(t *testing.T) { ) coinWithExtensionProhibitedToReceive := sdk.NewCoin(extensionDenom, AmountDEXExpectToReceiveTrigger) + // check that we respect whitelisted balance + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + coinWithoutExtension, + coinWithExtensionProhibitedToReceive, + ), + types.ErrWhitelistedLimitExceeded, + ) + + whitelistedDenomBalance := bankKeeper.GetBalance(ctx, acc, extensionDenom) + requireT.NoError( + ftKeeper.SetWhitelistedBalance( + ctx, issuer, acc, whitelistedDenomBalance.Add(coinWithExtensionProhibitedToReceive), + ), + ) requireT.ErrorContains( ftKeeper.DEXCheckOrderAmounts( simapp.CopyContextWithMultiStore(ctx), @@ -1128,14 +1125,15 @@ func TestKeeper_DEXExtensions(t *testing.T) { "wasm error: DEX order placement is failed", ) - // update whitelisted balance, and check that asset FT doesn't control it, so the check passes - requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, acc, sdk.NewCoin(extensionDenom, sdkmath.NewInt(1)))) - requireT.NoError( + // increase expected to receive to check that we respect whitelisted balance + require.NoError(t, ftKeeper.DEXIncreaseExpectedToReceive(ctx, acc, sdk.NewInt64Coin(extensionDenom, 1))) + requireT.ErrorIs( ftKeeper.DEXCheckOrderAmounts( - ctx, + simapp.CopyContextWithMultiStore(ctx), types.DEXOrder{Creator: acc}, coinWithoutExtension, - coinWithExtension, + coinWithExtensionProhibitedToReceive, ), + types.ErrWhitelistedLimitExceeded, ) } diff --git a/x/asset/ft/keeper/keeper_extension_test.go b/x/asset/ft/keeper/keeper_extension_transfer_test.go similarity index 81% rename from x/asset/ft/keeper/keeper_extension_test.go rename to x/asset/ft/keeper/keeper_extension_transfer_test.go index 45e63bed6..c5eb369f3 100644 --- a/x/asset/ft/keeper/keeper_extension_test.go +++ b/x/asset/ft/keeper/keeper_extension_transfer_test.go @@ -13,21 +13,18 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/types/query" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/samber/lo" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/CoreumFoundation/coreum/v5/testutil/simapp" testcontracts "github.com/CoreumFoundation/coreum/v5/x/asset/ft/keeper/test-contracts" "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" + wibctransfertypes "github.com/CoreumFoundation/coreum/v5/x/wibctransfer/types" ) var ( AmountDisallowedTrigger = sdkmath.NewInt(7) - AmountIgnoreWhitelistingTrigger = sdkmath.NewInt(49) - AmountIgnoreFreezingTrigger = sdkmath.NewInt(79) AmountBurningTrigger = sdkmath.NewInt(101) AmountMintingTrigger = sdkmath.NewInt(105) AmountIgnoreBurnRateTrigger = sdkmath.NewInt(108) @@ -120,7 +117,7 @@ func TestKeeper_Extension_Issue(t *testing.T) { requireT.EqualValues("2", balance.Amount.String()) } -func TestKeeper_Extension_Issue_WithIBCAndBlockSmartContract(t *testing.T) { +func TestKeeper_Extension_IBC(t *testing.T) { requireT := require.New(t) testApp := simapp.New() @@ -130,51 +127,78 @@ func TestKeeper_Extension_Issue_WithIBCAndBlockSmartContract(t *testing.T) { }) ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper - testCases := []struct { - features []types.Feature - }{ - { - features: []types.Feature{ - types.Feature_extension, - types.Feature_ibc, - }, + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + codeID, _, err := testApp.WasmPermissionedKeeper.Create( + ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, + ) + requireT.NoError(err) + + settingsWithoutIBC := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 1, + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{ + types.Feature_whitelisting, + types.Feature_extension, }, - { - features: []types.Feature{ - types.Feature_extension, - types.Feature_block_smart_contracts, - }, + ExtensionSettings: &types.ExtensionIssueSettings{ + CodeId: codeID, }, } - for _, tc := range testCases { - issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - codeID, _, err := testApp.WasmPermissionedKeeper.Create( - ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, - ) - requireT.NoError(err) - - settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC", - Description: "ABC Desc", - Subunit: "extensionabc", - Precision: 8, - InitialAmount: sdkmath.NewInt(777), - Features: tc.features, - ExtensionSettings: &types.ExtensionIssueSettings{ - CodeId: codeID, - }, - } - _, err = ftKeeper.Issue(ctx, settings) - requireT.ErrorIs(err, types.ErrInvalidInput) + denomWithoutIBC, err := ftKeeper.Issue(ctx, settingsWithoutIBC) + requireT.NoError(err) + + settingsWithIBC := types.IssueSettings{ + Issuer: issuer, + Symbol: "ABC", + Subunit: "abc", + Precision: 1, + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{ + types.Feature_whitelisting, + types.Feature_extension, + types.Feature_ibc, + }, + ExtensionSettings: &types.ExtensionIssueSettings{ + CodeId: codeID, + }, } + + denomWithIBC, err := ftKeeper.Issue(ctx, settingsWithIBC) + requireT.NoError(err) + + // Trick the ctx to look like an outgoing IBC, + // so we may use regular bank send to test the logic. + ctx = sdk.UnwrapSDKContext(wibctransfertypes.WithPurpose(ctx, wibctransfertypes.PurposeOut)) + + // transferring denom with disabled IBC should fail + err = bankKeeper.SendCoins( + ctx, + issuer, + recipient, + sdk.NewCoins(sdk.NewCoin(denomWithoutIBC, sdkmath.NewInt(100))), + ) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) + + // transferring denom with enabled IBC should succeed + err = bankKeeper.SendCoins( + ctx, + issuer, + recipient, + sdk.NewCoins(sdk.NewCoin(denomWithIBC, sdkmath.NewInt(100))), + ) + requireT.NoError(err) } func TestKeeper_Extension_Whitelist(t *testing.T) { requireT := require.New(t) - assertT := assert.New(t) testApp := simapp.New() ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{ @@ -186,6 +210,7 @@ func TestKeeper_Extension_Whitelist(t *testing.T) { bankKeeper := testApp.BankKeeper issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) codeID, _, err := testApp.WasmPermissionedKeeper.Create( ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, @@ -211,122 +236,42 @@ func TestKeeper_Extension_Whitelist(t *testing.T) { denom, err := ftKeeper.Issue(ctx, settings) requireT.NoError(err) - token, err := ftKeeper.GetToken(ctx, denom) - requireT.NoError(err) - - extensionCWAddress, err := sdk.AccAddressFromBech32(token.ExtensionCWAddress) - requireT.NoError(err) - - unwhitelistableSettings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC", - Subunit: "abc", - Precision: 1, - Description: "ABC Desc", - InitialAmount: sdkmath.NewInt(666), - Features: []types.Feature{ - types.Feature_extension, - }, - ExtensionSettings: &types.ExtensionIssueSettings{ - CodeId: codeID, - }, - } - - recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - - unwhitelistableDenom, err := ftKeeper.Issue(ctx, unwhitelistableSettings) - requireT.NoError(err) - _, err = ftKeeper.GetToken(ctx, unwhitelistableDenom) - requireT.NoError(err) - - // set whitelisted balance to 0 - requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, sdk.NewCoin(denom, sdkmath.NewInt(0)))) - whitelistedBalance := ftKeeper.GetWhitelistedBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(0)).String(), whitelistedBalance.String()) - - coinsToSend := sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(100))) + coinToSend := sdk.NewCoin(denom, sdkmath.NewInt(100)) + coinsToSend := sdk.NewCoins(coinToSend) // send - err = bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, issuer, coinsToSend) - requireT.NoError(err) + requireT.ErrorIs(bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend), types.ErrWhitelistedLimitExceeded) // multi-send - err = bankKeeper.InputOutputCoins(ctx, + requireT.ErrorIs(bankKeeper.InputOutputCoins(ctx, banktypes.Input{Address: issuer.String(), Coins: coinsToSend}, - []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}}) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, issuer, coinsToSend) - requireT.NoError(err) + []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}}), types.ErrWhitelistedLimitExceeded) - // set whitelisted balance to 100 - requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, sdk.NewCoin(denom, sdkmath.NewInt(100)))) - whitelistedBalance = ftKeeper.GetWhitelistedBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(100)).String(), whitelistedBalance.String()) + // set whitelisted balance (for 2 sends) + requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, coinToSend.Add(coinToSend))) - // test query all whitelisted balances - allBalances, pageRes, err := ftKeeper.GetAccountsWhitelistedBalances(ctx, &query.PageRequest{}) - requireT.NoError(err) - assertT.Len(allBalances, 1) - assertT.EqualValues(1, pageRes.GetTotal()) - assertT.EqualValues(recipient.String(), allBalances[0].Address) - requireT.Equal(sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(100))).String(), allBalances[0].Coins.String()) - - coinsToSend = sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(50)), - sdk.NewCoin(unwhitelistableDenom, sdkmath.NewInt(50)), + // check that expected to fail extension call fails + err = bankKeeper.SendCoins(ctx, settings.Issuer, recipient, sdk.NewCoins( + sdk.NewCoin(denom, AmountDisallowedTrigger)), ) - // send - err = bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend) - requireT.NoError(err) + requireT.ErrorIs(err, types.ErrExtensionCallFailed) + requireT.ErrorContains(err, "7 is not allowed") + + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend)) // multi-send - err = bankKeeper.InputOutputCoins(ctx, + requireT.NoError(bankKeeper.InputOutputCoins(ctx, banktypes.Input{Address: issuer.String(), Coins: coinsToSend}, - []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}}) - requireT.NoError(err) + []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}})) bankBalance := bankKeeper.GetBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(100)).String(), bankBalance.String()) - - whitelistedBalance = ftKeeper.GetWhitelistedBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(100)).String(), whitelistedBalance.String()) + requireT.Equal(coinToSend.Add(coinToSend).String(), bankBalance.String()) // try to send more coinsToSend = sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(1))) // send - err = bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, issuer, coinsToSend) - requireT.NoError(err) + requireT.ErrorIs(bankKeeper.SendCoins(ctx, issuer, recipient, coinsToSend), types.ErrWhitelistedLimitExceeded) // multi-send - err = bankKeeper.InputOutputCoins(ctx, + requireT.ErrorIs(bankKeeper.InputOutputCoins(ctx, banktypes.Input{Address: issuer.String(), Coins: coinsToSend}, - []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}}) - requireT.ErrorContains(err, "Whitelisted limit exceeded.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, issuer, coinsToSend) - requireT.NoError(err) - - // sending trigger amount will be transferred despite whitelisted amount being exceeded - err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins( - sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger)), - ) - requireT.NoError(err) - - bankBalance = bankKeeper.GetBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(149)).String(), bankBalance.String()) - - whitelistedBalance = ftKeeper.GetWhitelistedBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(100)).String(), whitelistedBalance.String()) - - // reduce whitelisting limit below the current balance - err = ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, sdk.NewCoin(denom, sdkmath.NewInt(80))) - requireT.NoError(err) - - bankBalance = bankKeeper.GetBalance(ctx, issuer, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(517)).String(), bankBalance.String()) + []banktypes.Output{{Address: recipient.String(), Coins: coinsToSend}}), types.ErrWhitelistedLimitExceeded) } func TestKeeper_Extension_FreezeUnfreeze(t *testing.T) { @@ -342,6 +287,8 @@ func TestKeeper_Extension_FreezeUnfreeze(t *testing.T) { bankKeeper := testApp.BankKeeper issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + recipient1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + recipient2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) codeID, _, err := testApp.WasmPermissionedKeeper.Create( ctx, issuer, testcontracts.AssetExtensionWasm, &wasmtypes.AllowEverybody, @@ -367,77 +314,49 @@ func TestKeeper_Extension_FreezeUnfreeze(t *testing.T) { denom, err := ftKeeper.Issue(ctx, settings) requireT.NoError(err) - token, err := ftKeeper.GetToken(ctx, denom) - requireT.NoError(err) - - extensionCWAddress, err := sdk.AccAddressFromBech32(token.ExtensionCWAddress) - requireT.NoError(err) - - unfreezableSettings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC", - Subunit: "abc", - Precision: 1, - Description: "ABC Desc", - InitialAmount: sdkmath.NewInt(666), - Features: []types.Feature{types.Feature_extension}, - ExtensionSettings: &types.ExtensionIssueSettings{ - CodeId: codeID, - }, - } - - unfreezableDenom, err := ftKeeper.Issue(ctx, unfreezableSettings) - requireT.NoError(err) - _, err = ftKeeper.GetToken(ctx, unfreezableDenom) - requireT.NoError(err) + // check that expected to fail extension call fails + err = bankKeeper.SendCoins(ctx, settings.Issuer, recipient1, sdk.NewCoins( + sdk.NewCoin(denom, AmountDisallowedTrigger)), + ) + requireT.ErrorIs(err, types.ErrExtensionCallFailed) + requireT.ErrorContains(err, "7 is not allowed") - recipient := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins( + // send coins to recipient1 + err = bankKeeper.SendCoins(ctx, issuer, recipient1, sdk.NewCoins( sdk.NewCoin(denom, sdkmath.NewInt(100)), - sdk.NewCoin(unfreezableDenom, sdkmath.NewInt(100)), )) requireT.NoError(err) - // freeze, query frozen - err = ftKeeper.Freeze(ctx, issuer, recipient, sdk.NewCoin(denom, sdkmath.NewInt(120))) - requireT.NoError(err) - frozenBalance, err := ftKeeper.GetFrozenBalance(ctx, recipient, denom) - requireT.NoError(err) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(120)), frozenBalance) + // freeze + requireT.NoError(ftKeeper.Freeze(ctx, issuer, recipient1, sdk.NewCoin(denom, sdkmath.NewInt(120)))) + // try to send more than available coinsToSend := sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(80))) // send - err = bankKeeper.SendCoins(ctx, recipient, issuer, coinsToSend) - requireT.ErrorContains(err, "Requested transfer token is frozen.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, recipient, coinsToSend) - requireT.NoError(err) + requireT.ErrorIs(bankKeeper.SendCoins(ctx, recipient1, recipient2, coinsToSend), cosmoserrors.ErrInsufficientFunds) // multi-send - err = bankKeeper.InputOutputCoins(ctx, - banktypes.Input{Address: recipient.String(), Coins: coinsToSend}, - []banktypes.Output{{Address: issuer.String(), Coins: coinsToSend}}) - requireT.ErrorContains(err, "Requested transfer token is frozen.") - // return attached fund of failed transaction - err = bankKeeper.SendCoins(ctx, extensionCWAddress, recipient, coinsToSend) - requireT.NoError(err) + requireT.ErrorIs(bankKeeper.InputOutputCoins( + ctx, + banktypes.Input{Address: recipient1.String(), Coins: coinsToSend}, + []banktypes.Output{{Address: recipient2.String(), Coins: coinsToSend}}), + cosmoserrors.ErrInsufficientFunds, + ) - bankBalance := bankKeeper.GetBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(100)).String(), bankBalance.String()) - frozenBalance, err = ftKeeper.GetFrozenBalance(ctx, recipient, denom) - requireT.NoError(err) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(120)).String(), frozenBalance.String()) + // unfreeze + requireT.NoError(ftKeeper.Unfreeze(ctx, issuer, recipient1, sdk.NewCoin(denom, sdkmath.NewInt(120)))) - // send trigger amount to transfer despite freezing - err = bankKeeper.SendCoins(ctx, recipient, issuer, sdk.NewCoins( - sdk.NewCoin(denom, AmountIgnoreFreezingTrigger)), - ) - requireT.NoError(err) + // send + requireT.NoError(bankKeeper.SendCoins(ctx, recipient1, recipient2, coinsToSend)) - bankBalance = bankKeeper.GetBalance(ctx, recipient, denom) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(21)).String(), bankBalance.String()) - frozenBalance, err = ftKeeper.GetFrozenBalance(ctx, recipient, denom) - requireT.NoError(err) - requireT.Equal(sdk.NewCoin(denom, sdkmath.NewInt(120)).String(), frozenBalance.String()) + // freeze globally + requireT.NoError(ftKeeper.SetGlobalFreeze(ctx, denom, true)) + + // try to send more than available + requireT.ErrorIs( + bankKeeper.SendCoins( + ctx, recipient1, recipient2, sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(1))), + ), types.ErrGloballyFrozen, + ) } func TestKeeper_Extension_Burn(t *testing.T) { @@ -629,7 +548,7 @@ func TestKeeper_Extension_Burn(t *testing.T) { err = bankKeeper.SendCoins(ctx, recipient, issuer, sdk.NewCoins( sdk.NewCoin(burnableDenom, AmountBurningTrigger)), ) - requireT.ErrorContains(err, "Requested transfer token is frozen.") + requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) } func TestKeeper_Extension_Mint(t *testing.T) { diff --git a/x/asset/ft/keeper/test-contracts/asset-extension/src/contract.rs b/x/asset/ft/keeper/test-contracts/asset-extension/src/contract.rs index 21b5755ab..259df3d48 100644 --- a/x/asset/ft/keeper/test-contracts/asset-extension/src/contract.rs +++ b/x/asset/ft/keeper/test-contracts/asset-extension/src/contract.rs @@ -4,11 +4,10 @@ use crate::msg::{ TransferContext, }; use crate::state::{DENOM, EXTRA_DATA}; -use coreum_wasm_sdk::deprecated::assetft::{FREEZING, WHITELISTING}; use coreum_wasm_sdk::deprecated::core::{CoreumMsg, CoreumResult}; use coreum_wasm_sdk::types::coreum::asset::ft::v1::{ MsgBurn, MsgMint, QueryFrozenBalanceRequest, QueryFrozenBalanceResponse, QueryTokenRequest, - QueryTokenResponse, QueryWhitelistedBalanceRequest, QueryWhitelistedBalanceResponse, Token, + QueryTokenResponse, Token, }; use coreum_wasm_sdk::types::cosmos::bank::v1beta1::{ MsgSend, QueryBalanceRequest, QueryBalanceResponse, @@ -25,8 +24,6 @@ const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const AMOUNT_DISALLOWED_TRIGGER: Uint128 = Uint128::new(7); -const AMOUNT_IGNORE_WHITELISTING_TRIGGER: Uint128 = Uint128::new(49); -const AMOUNT_IGNORE_FREEZING_TRIGGER: Uint128 = Uint128::new(79); const AMOUNT_BURNING_TRIGGER: Uint128 = Uint128::new(101); const AMOUNT_MINTING_TRIGGER: Uint128 = Uint128::new(105); const AMOUNT_IGNORE_BURN_RATE_TRIGGER: Uint128 = Uint128::new(108); @@ -125,14 +122,6 @@ pub fn sudo_extension_transfer( let token = query_token(deps.as_ref(), &denom)?; if !&token.features.is_empty() { - if token.features.contains(&(FREEZING as i32)) { - assert_freezing(&context, deps.as_ref(), sender.as_ref(), &token, amount)?; - } - - if token.features.contains(&(WHITELISTING as i32)) { - assert_whitelisting(&context, deps.as_ref(), &recipient, &token, amount)?; - } - assert_block_smart_contracts(&context, &recipient, &token, amount)?; assert_ibc(&context, &recipient, &token, amount)?; @@ -159,7 +148,7 @@ pub fn sudo_extension_transfer( denom: token.denom.to_string(), amount: amount.to_string(), }] - .to_vec(), + .to_vec(), }; let mut response = rsp.add_message(CosmosMsg::Any(transfer_msg.to_any())); @@ -216,80 +205,6 @@ fn query_issuance_msg(deps: Deps) -> StdResult { to_json_binary(&resp) } -fn assert_freezing( - context: &TransferContext, - deps: Deps, - account: &str, - token: &Token, - amount: Uint128, -) -> Result<(), ContractError> { - // Allow any amount if recipient is admin - if token.admin == account { - return Ok(()); - } - - // Ignore freezing if the transfer is an IBC transfer in. In case of IBC transfer coming into the chain - // source account is the escrow account and since we don't want to allow freeze of every - // escrow address we ignore freezing for incoming ibc transfers. - if context.ibc_purpose == IBCPurpose::In { - return Ok(()); - } - - if amount == AMOUNT_IGNORE_FREEZING_TRIGGER { - return Ok(()); - } - - if token.globally_frozen { - return Err(ContractError::FreezingError {}); - } - - let bank_balance = query_bank_balance(deps, account, &token.denom)?; - let frozen_balance = query_frozen_balance(deps, account, &token.denom)?; - - // the amount is already deducted from the balance, so you can omit it from both sides - if frozen_balance.amount.parse::().unwrap() > bank_balance.amount.parse::().unwrap() - { - return Err(ContractError::FreezingError {}); - } - - Ok(()) -} - -fn assert_whitelisting( - context: &TransferContext, - deps: Deps, - account: &str, - token: &Token, - amount: Uint128, -) -> Result<(), ContractError> { - // Allow any amount if recipient is admin - if token.admin == account { - return Ok(()); - } - - // Ignore whitelising if the transfer is an IBC transfer. In case of IBC transfer - // destination account is the escrow account and since we don't want to whitelist every - // escrow address we ignore whitelisting for outgoing ibc transfers. - if context.ibc_purpose == IBCPurpose::Out { - return Ok(()); - } - - if amount == AMOUNT_IGNORE_WHITELISTING_TRIGGER { - return Ok(()); - } - - let bank_balance = query_bank_balance(deps, account, &token.denom)?; - let whitelisted_balance = query_whitelisted_balance(deps, account, &token.denom)?; - - if amount + Uint128::from(bank_balance.amount.parse::().unwrap()) - > Uint128::from(whitelisted_balance.amount.parse::().unwrap()) - { - return Err(ContractError::WhitelistingError {}); - } - - Ok(()) -} - fn assert_burning(contract: &str, amount: Uint128, token: &Token) -> CoreumResult { let burn_message = MsgBurn { sender: contract.to_string(), @@ -327,7 +242,7 @@ fn assert_minting( denom: token.denom.to_string(), amount: amount.to_string(), }] - .to_vec(), + .to_vec(), }; Ok(Response::new() @@ -386,7 +301,7 @@ fn assert_send_commission_rate( denom: token.denom.to_string(), amount: commission_amount.to_string(), }] - .to_vec(), + .to_vec(), }; return Ok(response @@ -409,7 +324,7 @@ fn assert_send_commission_rate( denom: token.denom.to_string(), amount: admin_commission_amount.to_string(), }] - .to_vec(), + .to_vec(), }; return Ok(response @@ -440,7 +355,7 @@ fn assert_burn_rate( denom: token.denom.to_string(), amount: burn_amount.to_string(), }] - .to_vec(), + .to_vec(), }; return Ok(response @@ -470,15 +385,6 @@ fn query_frozen_balance(deps: Deps, account: &str, denom: &str) -> StdResult StdResult { - let request = QueryWhitelistedBalanceRequest { - account: account.to_string(), - denom: denom.to_string(), - }; - let whitelisted_balance: QueryWhitelistedBalanceResponse = request.query(&deps.querier)?; - Ok(whitelisted_balance.balance.unwrap_or_default()) -} - fn query_bank_balance(deps: Deps, account: &str, denom: &str) -> StdResult { let request = QueryBalanceRequest { address: account.to_string(), diff --git a/x/asset/ft/keeper/test-contracts/asset-extension/src/error.rs b/x/asset/ft/keeper/test-contracts/asset-extension/src/error.rs index 10a2818c8..774b9d887 100644 --- a/x/asset/ft/keeper/test-contracts/asset-extension/src/error.rs +++ b/x/asset/ft/keeper/test-contracts/asset-extension/src/error.rs @@ -6,12 +6,6 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - #[error("Requested transfer token is frozen.")] - FreezingError {}, - - #[error("Whitelisted limit exceeded.")] - WhitelistingError {}, - #[error("Unauthorized.")] Unauthorized {}, diff --git a/x/asset/ft/types/token.go b/x/asset/ft/types/token.go index 1191418d6..1260bc7ba 100644 --- a/x/asset/ft/types/token.go +++ b/x/asset/ft/types/token.go @@ -228,11 +228,17 @@ func ValidateFeatures(features []Feature) error { present[f] = struct{}{} } for _, feature := range features { - if hasExtension && (feature == Feature_ibc || feature == Feature_block_smart_contracts) { + if hasExtension && feature == Feature_block_smart_contracts { return sdkerrors.Wrapf(ErrInvalidInput, "extension is not allowed in combination with %s", feature.String()) } - if hasDEXBlock && (feature == Feature_dex_whitelisted_denoms || feature == Feature_dex_order_cancellation) { - return sdkerrors.Wrapf(ErrInvalidInput, "DEX block is not allowed in combination with %s", feature.String()) + // if dex is blocked those features make not sense + if hasDEXBlock && (feature == Feature_dex_whitelisted_denoms || + feature == Feature_dex_order_cancellation || + feature == Feature_dex_unified_ref_amount_change) { + return sdkerrors.Wrapf( + ErrInvalidInput, + "%s is not allowed in combination with %s", Feature_dex_block.String(), feature.String(), + ) } } diff --git a/x/asset/ft/types/token_test.go b/x/asset/ft/types/token_test.go index 0ced69991..2e936dbbd 100644 --- a/x/asset/ft/types/token_test.go +++ b/x/asset/ft/types/token_test.go @@ -244,7 +244,7 @@ func TestValidateFeatures(t *testing.T) { types.Feature_ibc, types.Feature_extension, }, - Ok: false, + Ok: true, }, { Name: "all", @@ -316,6 +316,14 @@ func TestValidateFeatures(t *testing.T) { }, Ok: false, }, + { + Name: "dex_block_unified_ref_amount_change", + Features: []types.Feature{ + types.Feature_dex_block, + types.Feature_dex_unified_ref_amount_change, + }, + Ok: false, + }, { Name: "all_dex_features_except_block", Features: []types.Feature{