diff --git a/build/coreum/build.go b/build/coreum/build.go index 10aae734d..e34493ddf 100644 --- a/build/coreum/build.go +++ b/build/coreum/build.go @@ -195,7 +195,7 @@ func Lint(ctx context.Context, deps types.DepsFunc) error { CompileAllSmartContracts, formatProto, lintProto, - // breakingProto, // FIXME: uncomment in next PR + breakingProto, ) return golang.Lint(ctx, deps) } diff --git a/build/coreum/generate-proto-breaking.go b/build/coreum/generate-proto-breaking.go index 4a517f8cd..0c821e16e 100644 --- a/build/coreum/generate-proto-breaking.go +++ b/build/coreum/generate-proto-breaking.go @@ -1,4 +1,3 @@ -//nolint:unused package coreum import ( @@ -22,10 +21,8 @@ import ( ) //go:embed proto-breaking.tmpl.json -//nolint:unused // FIXME: uncomment in next PR var configBreakingTmpl string -//nolint:deadcode func breakingProto(ctx context.Context, deps types.DepsFunc) error { deps(golang.Tidy, tools.EnsureProtoc, tools.EnsureProtocGenBufBreaking) diff --git a/integration-tests/modules/assetft_extension_test.go b/integration-tests/modules/assetft_extension_test.go index bfb0e2c8a..6cc650637 100644 --- a/integration-tests/modules/assetft_extension_test.go +++ b/integration-tests/modules/assetft_extension_test.go @@ -24,17 +24,20 @@ import ( "github.com/CoreumFoundation/coreum/v5/testutil/integration" testcontracts "github.com/CoreumFoundation/coreum/v5/x/asset/ft/keeper/test-contracts" assetfttypes "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" + dextypes "github.com/CoreumFoundation/coreum/v5/x/dex/types" ) -const ( - AmountDisallowedTrigger = 7 - AmountIgnoreWhitelistingTrigger = 49 - AmountIgnoreFreezingTrigger = 79 - AmountBurningTrigger = 101 - AmountMintingTrigger = 105 - AmountIgnoreBurnRateTrigger = 108 - AmountIgnoreSendCommissionRateTrigger = 109 - AmountBlockSmartContractTrigger = 111 +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) + AmountIgnoreSendCommissionRateTrigger = sdkmath.NewInt(109) + AmountBlockSmartContractTrigger = sdkmath.NewInt(111) + AmountDEXExpectToSpendTrigger = sdkmath.NewInt(103) + AmountDEXExpectToReceiveTrigger = sdkmath.NewInt(104) ) // TestAssetFTExtensionIssue tests extension issue functionality of fungible tokens. @@ -49,7 +52,7 @@ func TestAssetFTExtensionIssue(t *testing.T) { issuer := chain.GenAccount() chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount. - Add(sdkmath.NewInt(1_000_000)). // one million added for uploading wasm code + Add(sdkmath.NewInt(1_000_000)). Add(sdkmath.NewInt(3 * 500_000)), // give 500k gas for each message since extensions are nondeterministic }) @@ -141,7 +144,7 @@ func TestAssetFTExtensionIssue(t *testing.T) { requireT.EqualValues("12", balance.Balance.Amount.String()) // sending 7 will fail - sendMsg.Amount = sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(AmountDisallowedTrigger))) + sendMsg.Amount = sdk.NewCoins(sdk.NewCoin(denom, AmountDisallowedTrigger)) _, err = client.BroadcastTx( ctx, chain.ClientContext.WithFromAddress(issuer), @@ -405,7 +408,7 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { sendMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: recipient.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreWhitelistingTrigger))), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger)), } _, err = client.BroadcastTx( ctx, @@ -419,12 +422,12 @@ func TestAssetFTExtensionWhitelist(t *testing.T) { // try to send trigger amount via Multisend multiSendMsg = &banktypes.MsgMultiSend{ Inputs: []banktypes.Input{{Address: issuer.String(), Coins: sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreWhitelistingTrigger)), + 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, sdkmath.NewInt(AmountIgnoreWhitelistingTrigger)), + sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger), sdk.NewCoin(denomWithoutExtension, sdkmath.NewInt(10)), chain.NewCoin(sdkmath.NewInt(10)), )}}, @@ -589,7 +592,7 @@ func TestAssetFTExtensionFreeze(t *testing.T) { sendMsg = &banktypes.MsgSend{ FromAddress: recipient.String(), ToAddress: recipient2.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreFreezingTrigger))), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreFreezingTrigger)), } res, err = client.BroadcastTx( ctx, @@ -702,7 +705,7 @@ func TestAssetFTExtensionBurn(t *testing.T) { ToAddress: issuer.String(), Amount: sdk.NewCoins(sdk.Coin{ Denom: unburnable, - Amount: sdkmath.NewInt(AmountBurningTrigger), + Amount: AmountBurningTrigger, }), } @@ -763,7 +766,7 @@ func TestAssetFTExtensionBurn(t *testing.T) { // burn tokens and check balance and total supply oldSupply, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: burnableDenom}) requireT.NoError(err) - burnCoin := sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger)) + burnCoin := sdk.NewCoin(burnableDenom, AmountBurningTrigger) burnMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), @@ -859,7 +862,7 @@ func TestAssetFTExtensionMint(t *testing.T) { ToAddress: issuer.String(), Amount: sdk.NewCoins(sdk.Coin{ Denom: unmintableDenom, - Amount: sdkmath.NewInt(AmountMintingTrigger), + Amount: AmountMintingTrigger, }), } @@ -919,7 +922,7 @@ func TestAssetFTExtensionMint(t *testing.T) { // mint tokens and check balance and total supply oldSupply, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: mintableDenom}) requireT.NoError(err) - mintCoin := sdk.NewCoin(mintableDenom, sdkmath.NewInt(AmountMintingTrigger)) + mintCoin := sdk.NewCoin(mintableDenom, AmountMintingTrigger) mintMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: issuer.String(), @@ -945,7 +948,7 @@ func TestAssetFTExtensionMint(t *testing.T) { assertT.EqualValues(mintCoin, newSupply.GetAmount().Sub(oldSupply.GetAmount())) // mint tokens to recipient - mintCoin = sdk.NewCoin(mintableDenom, sdkmath.NewInt(AmountMintingTrigger)) + mintCoin = sdk.NewCoin(mintableDenom, AmountMintingTrigger) mintMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: recipient.String(), @@ -987,7 +990,7 @@ func TestAssetFTExtensionMint(t *testing.T) { mintMsg = &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: contractAddr, - Amount: sdk.NewCoins(sdk.NewCoin(mintableDenom, sdkmath.NewInt(AmountBlockSmartContractTrigger))), + Amount: sdk.NewCoins(sdk.NewCoin(mintableDenom, AmountBlockSmartContractTrigger)), } _, err = client.BroadcastTx( ctx, @@ -1072,7 +1075,7 @@ func TestAssetFTExtensionSendingToSmartContractIsDenied(t *testing.T) { sendMsg := &banktypes.MsgSend{ FromAddress: issuer.String(), ToAddress: contractAddr, - Amount: sdk.NewCoins(sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger)), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountBlockSmartContractTrigger)), } _, err = client.BroadcastTx( ctx, @@ -1086,13 +1089,13 @@ func TestAssetFTExtensionSendingToSmartContractIsDenied(t *testing.T) { Inputs: []banktypes.Input{ { Address: issuer.String(), - Coins: sdk.NewCoins(sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger)), + Coins: sdk.NewCoins(sdk.NewCoin(denom, AmountBlockSmartContractTrigger)), }, }, Outputs: []banktypes.Output{ { Address: contractAddr, - Coins: sdk.NewCoins(sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger)), + Coins: sdk.NewCoins(sdk.NewCoin(denom, AmountBlockSmartContractTrigger)), }, }, } @@ -1180,7 +1183,7 @@ func TestAssetFTExtensionAttachingToSmartContractCallIsDenied(t *testing.T) { incrementPayload, err := moduleswasm.MethodToEmptyBodyPayload(moduleswasm.SimpleIncrement) requireT.NoError(err) _, err = chain.Wasm.ExecuteWASMContract( - ctx, txf, issuer, contractAddr, incrementPayload, sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger), + ctx, txf, issuer, contractAddr, incrementPayload, sdk.NewCoin(denom, AmountBlockSmartContractTrigger), ) requireT.ErrorContains(err, "Transferring to or from smart contracts are prohibited.") } @@ -1273,7 +1276,7 @@ func TestAssetFTExtensionIssuingSmartContractIsAllowedToSendAndReceive(t *testin requireT.NoError(err) // mint to someone else - amountToMint = sdkmath.NewInt(AmountBlockSmartContractTrigger) + amountToMint = AmountBlockSmartContractTrigger mintPayload, err = json.Marshal(map[ftMethod]amountRecipientBodyFTRequest{ ftMethodMint: { Amount: amountToMint.String(), @@ -1365,7 +1368,7 @@ func TestAssetFTExtensionAttachingToSmartContractInstantiationIsDenied(t *testin integration.InstantiateConfig{ AccessType: wasmtypes.AccessTypeUnspecified, Payload: initialPayload, - Amount: sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger), + Amount: sdk.NewCoin(denom, AmountBlockSmartContractTrigger), Label: "simple_state", }, ) @@ -1487,12 +1490,12 @@ func TestAssetFTExtensionMintingAndSendingOnBehalfOfIssuingSmartContractIsPossib &banktypes.MsgSend{ FromAddress: contractAddr, ToAddress: recipient.String(), - Amount: sdk.NewCoins(sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger)), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountBlockSmartContractTrigger)), }, &assetfttypes.MsgMint{ Sender: contractAddr, Recipient: recipient.String(), - Coin: sdk.NewInt64Coin(denom, AmountBlockSmartContractTrigger), + Coin: sdk.NewCoin(denom, AmountBlockSmartContractTrigger), }, }) @@ -1600,7 +1603,7 @@ func TestAssetFTExtensionBurnRate(t *testing.T) { sendMsg = &banktypes.MsgSend{ FromAddress: recipient1.String(), ToAddress: recipient2.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreBurnRateTrigger))), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreBurnRateTrigger)), } _, err = client.BroadcastTx( @@ -1747,7 +1750,7 @@ func TestAssetFTExtensionSendCommissionRate(t *testing.T) { sendMsg = &banktypes.MsgSend{ FromAddress: recipient1.String(), ToAddress: recipient2.String(), - Amount: sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreSendCommissionRateTrigger))), + Amount: sdk.NewCoins(sdk.NewCoin(denom, AmountIgnoreSendCommissionRateTrigger)), } _, err = client.BroadcastTx( @@ -1806,3 +1809,178 @@ func TestAssetFTExtensionSendCommissionRate(t *testing.T) { &extension: 30, // 25 + 5 (50% of the commission to the extension) }) } + +// TestAssetFTExtensionDEX checks extension works with the DEX. +func TestAssetFTExtensionDEX(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + + assetFTClint := assetfttypes.NewQueryClient(chain.ClientContext) + dexClient := dextypes.NewQueryClient(chain.ClientContext) + + dexParamsRes, err := dexClient.Params(ctx, &dextypes.QueryParamsRequest{}) + requireT.NoError(err) + dexReserver := dexParamsRes.Params.OrderReserve + + admin := chain.GenAccount() + acc1 := chain.GenAccount() + acc2 := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, admin, integration.BalancesOptions{ + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount. + AddRaw(1_000_000). // added 1 million for smart contract upload + AddRaw(2 * 500_000), + }) + chain.FundAccountWithOptions(ctx, t, acc1, integration.BalancesOptions{ + Amount: sdkmath.NewInt(500_000). // message + order reserve + Add(dexReserver.Amount), + }) + chain.FundAccountWithOptions(ctx, t, acc2, integration.BalancesOptions{ + Amount: sdkmath.NewInt(500_000). + AddRaw(100). + Add(dexReserver.Amount), // message + balance to place an order + order reserve + }) + + codeID, err := chain.Wasm.DeployWASMContract( + ctx, chain.TxFactory().WithSimulateAndExecute(true), admin, testcontracts.AssetExtensionWasm, + ) + requireT.NoError(err) + + issueMsg := &assetfttypes.MsgIssue{ + Issuer: admin.String(), + Symbol: "EXABC", + Subunit: "extabc", + Precision: 6, + InitialAmount: sdkmath.NewInt(1000), + Features: []assetfttypes.Feature{ + assetfttypes.Feature_extension, + }, + ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ + CodeId: codeID, + Label: "testing-dex", + }, + } + + res, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(admin), + chain.TxFactoryAuto(), + issueMsg, + ) + requireT.NoError(err) + tokenIssuedEvents, err := event.FindTypedEvents[*assetfttypes.EventIssued](res.Events) + requireT.NoError(err) + denomWithExtension := tokenIssuedEvents[0].Denom + + // send from admin to acc1 to place an order + sendMsg := &banktypes.MsgSend{ + FromAddress: admin.String(), + ToAddress: acc1.String(), + Amount: sdk.NewCoins(sdk.NewCoin(denomWithExtension, sdkmath.NewInt(400))), + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(admin), + chain.TxFactoryAuto(), + sendMsg, + ) + requireT.NoError(err) + + placeSellOrderMsg := &dextypes.MsgPlaceOrder{ + Sender: acc1.String(), + Type: dextypes.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denomWithExtension, + QuoteDenom: chain.ChainSettings.Denom, + Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")), + Quantity: AmountDEXExpectToReceiveTrigger, + Side: dextypes.SIDE_SELL, + TimeInForce: dextypes.TIME_IN_FORCE_GTC, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc1), + chain.TxFactoryAuto(), + placeSellOrderMsg, + ) + requireT.ErrorContains(err, "wasm error: DEX order placement is failed") + + // update to allowed + placeSellOrderMsg.Quantity = sdkmath.NewInt(100) + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc1), + chain.TxFactoryAuto(), + placeSellOrderMsg, + ) + requireT.NoError(err) + + acc1BalanceRes, err := assetFTClint.Balance(ctx, &assetfttypes.QueryBalanceRequest{ + Account: acc1.String(), + Denom: denomWithExtension, + }) + requireT.NoError(err) + requireT.True(acc1BalanceRes.ExpectedToReceiveInDEX.IsZero()) + requireT.Equal(sdkmath.NewInt(100).String(), acc1BalanceRes.LockedInDEX.String()) + + // place buy order from acc2 + + acc2BalanceRes, err := assetFTClint.Balance(ctx, &assetfttypes.QueryBalanceRequest{ + Account: acc2.String(), + Denom: denomWithExtension, + }) + requireT.NoError(err) + // no coins of the denomWithExtension + requireT.True(acc2BalanceRes.Balance.IsZero()) + + placeBuyOrderMsg := &dextypes.MsgPlaceOrder{ + Sender: acc2.String(), + Type: dextypes.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denomWithExtension, + QuoteDenom: chain.ChainSettings.Denom, + Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")), + Quantity: AmountDEXExpectToSpendTrigger, + Side: dextypes.SIDE_BUY, + TimeInForce: dextypes.TIME_IN_FORCE_GTC, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc2), + chain.TxFactoryAuto(), + placeBuyOrderMsg, + ) + requireT.ErrorContains(err, "wasm error: DEX order placement is failed") + + // update to allowed + placeBuyOrderMsg.Quantity = sdkmath.NewInt(100) + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc2), + chain.TxFactoryAuto(), + placeBuyOrderMsg, + ) + requireT.NoError(err) + + // both order are executed and closed + acc2BalanceRes, err = assetFTClint.Balance(ctx, &assetfttypes.QueryBalanceRequest{ + Account: acc2.String(), + Denom: denomWithExtension, + }) + requireT.NoError(err) + // bought expected quantity + requireT.Equal(sdkmath.NewInt(100).String(), acc2BalanceRes.Balance.String()) + requireT.True(acc2BalanceRes.LockedInDEX.IsZero()) + requireT.True(acc2BalanceRes.ExpectedToReceiveInDEX.IsZero()) + + acc1BalanceRes, err = assetFTClint.Balance(ctx, &assetfttypes.QueryBalanceRequest{ + Account: acc1.String(), + Denom: denomWithExtension, + }) + requireT.NoError(err) + requireT.True(acc1BalanceRes.LockedInDEX.IsZero()) + requireT.True(acc1BalanceRes.ExpectedToReceiveInDEX.IsZero()) +} diff --git a/x/asset/ft/keeper/before_send.go b/x/asset/ft/keeper/before_send.go index f2ec5bae8..67f5853f3 100644 --- a/x/asset/ft/keeper/before_send.go +++ b/x/asset/ft/keeper/before_send.go @@ -7,6 +7,7 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/pkg/errors" "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" "github.com/CoreumFoundation/coreum/v5/x/wasm" @@ -14,12 +15,9 @@ import ( wibctransfertypes "github.com/CoreumFoundation/coreum/v5/x/wibctransfer/types" ) -// extension method calls. -const ( - // the function name of the extension smart contract, which will be invoked - // when doing the transfer. - ExtenstionTransferMethod = "extension_transfer" -) +// ExtensionTransferMethod the function name of the extension smart contract, which will be invoked +// when doing the transfer. +const ExtensionTransferMethod = "extension_transfer" // sudoExtensionTransferMsg contains the fields passed to extension method call. // @@ -201,7 +199,7 @@ func (k Keeper) invokeAssetExtension( wasm.IsSmartContract(ctx, recipient, k.wasmKeeper) contractMsg := map[string]interface{}{ - ExtenstionTransferMethod: sudoExtensionTransferMsg{ + ExtensionTransferMethod: sudoExtensionTransferMsg{ Sender: sender.String(), Recipient: recipient.String(), TransferAmount: sendAmount.Amount, @@ -216,7 +214,7 @@ func (k Keeper) invokeAssetExtension( } contractMsgBytes, err := json.Marshal(contractMsg) if err != nil { - return err + return errors.Wrapf(err, "failed to marshal contract msg") } _, err = k.wasmPermissionedKeeper.Sudo( @@ -225,7 +223,7 @@ func (k Keeper) invokeAssetExtension( contractMsgBytes, ) if err != nil { - return types.ErrExtensionCallFailed.Wrapf("was error: %s", err) + return types.ErrExtensionCallFailed.Wrapf("wasm error: %s", err) } return nil } diff --git a/x/asset/ft/keeper/keeper.go b/x/asset/ft/keeper/keeper.go index 91dd6d971..2dd58f506 100644 --- a/x/asset/ft/keeper/keeper.go +++ b/x/asset/ft/keeper/keeper.go @@ -741,17 +741,28 @@ func (k Keeper) GetSpendableBalance( if notLockedAmt.IsNegative() { return sdk.NewCoin(denom, sdkmath.ZeroInt()), nil } - frozenBalance, err := k.GetFrozenBalance(ctx, addr, denom) + + def, err := k.getDefinitionOrNil(ctx, denom) if err != nil { return sdk.Coin{}, err } - notFrozenAmt := balance.Amount.Sub(frozenBalance.Amount) - if notFrozenAmt.IsNegative() { - return sdk.NewCoin(denom, sdkmath.ZeroInt()), nil + // 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) { + frozenBalance, err := k.GetFrozenBalance(ctx, addr, denom) + if err != nil { + return sdk.Coin{}, err + } + notFrozenAmt := balance.Amount.Sub(frozenBalance.Amount) + if notFrozenAmt.IsNegative() { + return sdk.NewCoin(denom, sdkmath.ZeroInt()), nil + } + spendableAmount := sdkmath.MinInt(notLockedAmt, notFrozenAmt) + return sdk.NewCoin(denom, spendableAmount), nil } - spendableAmount := sdkmath.MinInt(notLockedAmt, notFrozenAmt) - return sdk.NewCoin(denom, spendableAmount), nil + return sdk.NewCoin(denom, notLockedAmt), nil } // TransferAdmin changes admin of a fungible token. @@ -911,14 +922,14 @@ func (k Keeper) validateCoinSpendable( return nil } - isGloballyFrozen, err := k.isGloballyFrozen(ctx, def.Denom) - if err != nil { - return err - } - if def.IsFeatureEnabled(types.Feature_freezing) && - isGloballyFrozen && - !def.HasAdminPrivileges(addr) { - return sdkerrors.Wrapf(types.ErrGloballyFrozen, "%s is globally frozen", def.Denom) + if def.IsFeatureEnabled(types.Feature_freezing) { + isGloballyFrozen, err := k.isGloballyFrozen(ctx, def.Denom) + if err != nil { + return err + } + if isGloballyFrozen && !def.HasAdminPrivileges(addr) { + return sdkerrors.Wrapf(types.ErrGloballyFrozen, "%s is globally frozen", def.Denom) + } } // Checking for IBC-received transfer is done here (after call to k.isGloballyFrozen), because those transfers diff --git a/x/asset/ft/keeper/keeper_dex.go b/x/asset/ft/keeper/keeper_dex.go index 73cc8531b..2ed0e7e50 100644 --- a/x/asset/ft/keeper/keeper_dex.go +++ b/x/asset/ft/keeper/keeper_dex.go @@ -1,6 +1,8 @@ package keeper import ( + "encoding/json" + sdkerrors "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" "cosmossdk.io/store/prefix" @@ -15,6 +17,19 @@ import ( cwasmtypes "github.com/CoreumFoundation/coreum/v5/x/wasm/types" ) +// ExtensionPlaceOrderMethod is the function name of the extension smart contract, which will be invoked +// when place and DEX order. +const ExtensionPlaceOrderMethod = "extension_place_order" + +// sudoExtensionPlaceOrderMsg contains the fields passed to extension method call. +// +//nolint:tagliatelle // these will be exposed to rust and must be snake case. +type sudoExtensionPlaceOrderMsg struct { + Order types.DEXOrder `json:"order"` + ExpectedToSpend sdk.Coin `json:"expected_to_spend"` + ExpectedToReceive sdk.Coin `json:"expected_to_receive"` +} + // DEXExecuteActions executes a series of DEX actions which include checking order amounts, // adjusting locked balances, and updating expected to receive balances. It performs necessary // validations and updates the state accordingly based on the provided actions. @@ -88,26 +103,11 @@ func (k Keeper) DEXCheckOrderAmounts( order types.DEXOrder, expectedToSpend, expectedToReceive sdk.Coin, ) error { - spendDef, err := k.getDefinitionOrNil(ctx, expectedToSpend.Denom) - if err != nil { - return err - } - if err := k.dexChecksForDenom(ctx, order.Creator, spendDef, expectedToReceive.Denom); err != nil { - return err - } - if err := k.dexExpectedToSpendChecks(ctx, order.Creator, spendDef, expectedToSpend); err != nil { + if err := k.dexCheckExpectedToSpend(ctx, order, expectedToSpend, expectedToReceive); err != nil { return err } - receiveDef, err := k.getDefinitionOrNil(ctx, expectedToReceive.Denom) - if err != nil { - return err - } - if err := k.dexChecksForDenom(ctx, order.Creator, receiveDef, expectedToSpend.Denom); err != nil { - return err - } - - return k.dexExpectedToReceiveChecks(ctx, order.Creator, receiveDef, expectedToReceive) + return k.dexCheckExpectedToReceive(ctx, order, expectedToSpend, expectedToReceive) } // SetDEXSettings sets the DEX settings of a specified denom. @@ -389,18 +389,90 @@ func (k Keeper) ValidateDEXCancelOrdersByDenomIsAllowed(ctx sdk.Context, addr sd return nil } -func (k Keeper) dexExpectedToReceiveChecks( +func (k Keeper) dexCheckExpectedToSpend( ctx sdk.Context, - addr sdk.AccAddress, - def *types.Definition, - coin sdk.Coin, + order types.DEXOrder, + 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 { + return sdkerrors.Wrapf(types.ErrDEXInsufficientSpendableBalance, "%s", err) + } + + spendDef, err := k.getDefinitionOrNil(ctx, expectedToSpend.Denom) + if err != nil { + return err + } + + if spendDef == nil { + 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 != 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 nil +} + +func (k Keeper) dexCheckExpectedToReceive( + ctx sdk.Context, + order types.DEXOrder, + expectedToSpend, expectedToReceive sdk.Coin, ) error { - if coin.IsZero() || def == nil { + receiveDef, err := k.getDefinitionOrNil(ctx, expectedToReceive.Denom) + if err != nil { + return err + } + if receiveDef == nil { return nil } - if def.IsFeatureEnabled(types.Feature_whitelisting) && !def.HasAdminPrivileges(addr) { - if err := k.validateWhitelistedBalance(ctx, addr, coin); err != 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 receiveDef.IsFeatureEnabled(types.Feature_whitelisting) && !receiveDef.HasAdminPrivileges(order.Creator) { + if err := k.validateWhitelistedBalance(ctx, order.Creator, expectedToReceive); err != nil { return err } } @@ -408,6 +480,36 @@ func (k Keeper) dexExpectedToReceiveChecks( return nil } +func (k Keeper) dexCallExtensionPlaceOrder( + ctx sdk.Context, + extensionContract sdk.AccAddress, + order types.DEXOrder, + expectedToSpend, expectedToReceive sdk.Coin, +) error { + contractMsg := map[string]interface{}{ + ExtensionPlaceOrderMethod: sudoExtensionPlaceOrderMsg{ + Order: order, + ExpectedToSpend: expectedToSpend, + ExpectedToReceive: expectedToReceive, + }, + } + contractMsgBytes, err := json.Marshal(contractMsg) + if err != nil { + return errors.Wrapf(err, "failed to marshal contract msg") + } + + _, err = k.wasmPermissionedKeeper.Sudo( + ctx, + extensionContract, + contractMsgBytes, + ) + if err != nil { + return types.ErrExtensionCallFailed.Wrapf("wasm error: %s", err) + } + + return nil +} + func (k Keeper) dexChecksForDenom( ctx sdk.Context, acc sdk.AccAddress, @@ -445,14 +547,6 @@ func (k Keeper) dexChecksForDenom( } func (k Keeper) dexChecksForDefinition(ctx sdk.Context, acc sdk.AccAddress, def types.Definition) error { - if def.ExtensionCWAddress != "" { - return sdkerrors.Wrapf( - types.ErrInvalidInput, - "usage of %s is not supported for DEX, the token has extensions", - def.Denom, - ) - } - if def.IsFeatureEnabled(types.Feature_dex_block) { return sdkerrors.Wrapf( cosmoserrors.ErrUnauthorized, @@ -491,47 +585,6 @@ func (k Keeper) dexChecksForDefinition(ctx sdk.Context, acc sdk.AccAddress, def return nil } -func (k Keeper) dexExpectedToSpendChecks( - ctx sdk.Context, - addr sdk.AccAddress, - def *types.Definition, - coin sdk.Coin, -) error { - if coin.IsZero() { - return nil - } - - // validate locked balance for any token - balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, balance, coin); err != nil { - return sdkerrors.Wrapf(types.ErrDEXInsufficientSpendableBalance, "%s", err) - } - - if def == nil { - return nil - } - - if def.IsFeatureEnabled(types.Feature_freezing) && !def.HasAdminPrivileges(addr) { - frozenBalance, err := k.GetFrozenBalance(ctx, addr, coin.Denom) - if err != nil { - return err - } - frozenAmt := frozenBalance.Amount - notFrozenTotalAmt := balance.Amount.Sub(frozenAmt) - if notFrozenTotalAmt.LT(coin.Amount) { - return sdkerrors.Wrapf( - types.ErrDEXInsufficientSpendableBalance, - "failed to DEX lock %s available %s%s", - coin.String(), - notFrozenTotalAmt, - coin.Denom, - ) - } - } - - return nil -} - func (k Keeper) updateDEXSettings( ctx sdk.Context, sender sdk.AccAddress, @@ -632,7 +685,7 @@ func (k Keeper) shouldRecordExpectedToReceiveBalance(ctx sdk.Context, denom stri return false, err } // increase for FT with the whitelisting enabled only - if def != nil && def.IsFeatureEnabled(types.Feature_whitelisting) { + if def != nil && (def.IsFeatureEnabled(types.Feature_whitelisting) || def.IsFeatureEnabled(types.Feature_extension)) { return true, nil } diff --git a/x/asset/ft/keeper/keeper_dex_test.go b/x/asset/ft/keeper/keeper_dex_test.go index 5b796782c..d11d694ad 100644 --- a/x/asset/ft/keeper/keeper_dex_test.go +++ b/x/asset/ft/keeper/keeper_dex_test.go @@ -30,7 +30,10 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { requireT := require.New(t) testApp := simapp.New() - ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) + ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{ + Time: time.Now(), + AppHash: []byte("some-hash"), + }) ftKeeper := testApp.AssetFTKeeper bankKeeper := testApp.BankKeeper @@ -39,20 +42,7 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { sender := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) recipient := 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_whitelisting}, - } - - denom, err := ftKeeper.Issue(ctx, settings) - requireT.NoError(err) - - unwhitelistableSettings := types.IssueSettings{ + noFeaturesIssuanceSettings := types.IssueSettings{ Issuer: issuer, Symbol: "ABC", Subunit: "abc", @@ -62,16 +52,16 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { Features: []types.Feature{}, } - unwhitelistableDenom, err := ftKeeper.Issue(ctx, unwhitelistableSettings) + noFeaturesDenom, err := ftKeeper.Issue(ctx, noFeaturesIssuanceSettings) requireT.NoError(err) - _, err = ftKeeper.GetToken(ctx, unwhitelistableDenom) + _, err = ftKeeper.GetToken(ctx, noFeaturesDenom) requireT.NoError(err) // function passed but nothing is reserved requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( - ctx, recipient, sdk.NewCoin(unwhitelistableDenom, sdkmath.NewInt(1)), + ctx, recipient, sdk.NewCoin(noFeaturesDenom, sdkmath.NewInt(1)), )) - requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, unwhitelistableDenom).IsZero()) + requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, noFeaturesDenom).IsZero()) // increase for not asset FT denom, passes but nothing is reserved notFTDenom := types.BuildDenom("nonexist", issuer) @@ -82,9 +72,22 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, "nonexist").IsZero(), ) + whitelistingIssuanceSettings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 1, + Description: "DEF Desc", + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{types.Feature_whitelisting}, + } + + whitelistingDenom, err := ftKeeper.Issue(ctx, whitelistingIssuanceSettings) + requireT.NoError(err) + // set whitelisted balance - coinToSend := sdk.NewCoin(denom, sdkmath.NewInt(100)) - // whitelist sender and fund + coinToSend := sdk.NewCoin(whitelistingDenom, sdkmath.NewInt(100)) + // whitelist sender and whitelistingDenom requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, sender, coinToSend)) requireT.NoError(bankKeeper.SendCoins(ctx, issuer, sender, sdk.NewCoins(coinToSend))) // send without the expected to received balance @@ -93,11 +96,11 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { // return coin requireT.NoError(bankKeeper.SendCoins(ctx, recipient, sender, sdk.NewCoins(coinToSend))) // increase expected to received balance - coinToIncreaseExpectedToReceive := sdk.NewCoin(denom, sdkmath.NewInt(1)) + coinToIncreaseExpectedToReceive := sdk.NewCoin(whitelistingDenom, sdkmath.NewInt(1)) requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) requireT.Equal( coinToIncreaseExpectedToReceive.String(), - ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).String(), + ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, whitelistingDenom).String(), ) // try to send with the increased part requireT.ErrorIs( @@ -114,9 +117,39 @@ func TestKeeper_DEXExpectedToReceive(t *testing.T) { ) requireT.NoError(ftKeeper.DEXDecreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) - requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).IsZero()) + 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) { @@ -294,6 +327,8 @@ func TestKeeper_DEXLocked(t *testing.T) { // 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, @@ -305,25 +340,25 @@ func TestKeeper_DEXLocked(t *testing.T) { Subunit: "defext", Precision: 6, InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{types.Feature_extension}, - + Features: []types.Feature{ + types.Feature_freezing, + types.Feature_extension, + }, ExtensionSettings: &types.ExtensionIssueSettings{ CodeId: codeID, }, } - denomWithExtension, err := ftKeeper.Issue(ctx, settingsWithExtension) + extensionDenom, err := ftKeeper.Issue(ctx, settingsWithExtension) requireT.NoError(err) - extensionCoin := sdk.NewInt64Coin(denomWithExtension, 50) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(extensionCoin))) - requireT.ErrorContains( - ftKeeper.DEXCheckOrderAmounts( - ctx, - types.DEXOrder{Creator: acc}, - extensionCoin, - sdk.NewInt64Coin(denom1, 0), - ), - "the token has extensions", - ) + 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) { @@ -417,6 +452,7 @@ func TestKeeper_DEXSettings_BlockDEX(t *testing.T) { }) ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) @@ -443,19 +479,21 @@ func TestKeeper_DEXSettings_BlockDEX(t *testing.T) { ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) requireT.NoError(err) + dexBlockedCoin := sdk.NewInt64Coin(ft1Denom, 50) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(dexBlockedCoin))) errStr := fmt.Sprintf("usage of %s is not supported for DEX, the token has dex_block", ft1Denom) requireT.ErrorContains(ftKeeper.DEXCheckOrderAmounts( ctx, types.DEXOrder{Creator: acc}, - sdk.NewInt64Coin(ft1Denom, 50), + dexBlockedCoin, sdk.NewInt64Coin(denom1, 0), ), errStr) requireT.ErrorContains(ftKeeper.DEXCheckOrderAmounts( ctx, types.DEXOrder{Creator: acc}, sdk.NewInt64Coin(denom1, 0), - sdk.NewInt64Coin(ft1Denom, 50), + dexBlockedCoin, ), errStr) } @@ -672,7 +710,7 @@ func TestKeeper_DEXLimitsWithGlobalFreeze(t *testing.T) { // admin still can increase the limits requireT.NoError( ftKeeper.DEXCheckOrderAmounts( - simapp.CopyContextWithMultiStore(ctx), + ctx, types.DEXOrder{Creator: issuer}, ft1CoinToSend, ft2CoinToSend, @@ -985,3 +1023,119 @@ func TestKeeper_UpdateDEXWhitelistedDenoms(t *testing.T) { WhitelistedDenoms: whitelistedDenoms, }, dexSettings) } + +func TestKeeper_DEXExtensions(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{ + Time: time.Now(), + AppHash: []byte("some-hash"), + }) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + // 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, + types.Feature_whitelisting, + }, + ExtensionSettings: &types.ExtensionIssueSettings{ + CodeId: codeID, + }, + } + extensionDenom, err := ftKeeper.Issue(ctx, settingsWithExtension) + requireT.NoError(err) + coinWithExtension := sdk.NewCoin(extensionDenom, sdkmath.NewInt(200)) + requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, acc, coinWithExtension)) + coinWithoutExtension := sdk.NewInt64Coin(denom1, 123) + testApp.MintAndSendCoin(t, ctx, acc, sdk.NewCoins(coinWithoutExtension)) + + // the extension controls all features, but to locked, amount + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + coinWithExtension, + coinWithoutExtension, + ), + types.ErrDEXInsufficientSpendableBalance, + ) + // 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 + requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, coinWithExtension)) + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinWithExtension, + coinWithoutExtension, + ), + ) + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinWithExtension)) + + // try with locked balance + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + coinWithExtension, + coinWithoutExtension, + ), + types.ErrDEXInsufficientSpendableBalance, + ) + + // check expected to send and receive with prohibited amounts + requireT.NoError(ftKeeper.DEXDecreaseLocked(ctx, acc, coinWithExtension)) + + // try with prohibited balances + coinWithExtensionProhibitedToSpend := sdk.NewCoin(extensionDenom, AmountDEXExpectToSpendTrigger) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + coinWithExtensionProhibitedToSpend, + coinWithoutExtension, + ), + "wasm error: DEX order placement is failed", + ) + + coinWithExtensionProhibitedToReceive := sdk.NewCoin(extensionDenom, AmountDEXExpectToReceiveTrigger) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + coinWithoutExtension, + coinWithExtensionProhibitedToReceive, + ), + "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( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinWithoutExtension, + coinWithExtension, + ), + ) +} diff --git a/x/asset/ft/keeper/keeper_extension_test.go b/x/asset/ft/keeper/keeper_extension_test.go index d8194f708..45e63bed6 100644 --- a/x/asset/ft/keeper/keeper_extension_test.go +++ b/x/asset/ft/keeper/keeper_extension_test.go @@ -24,14 +24,16 @@ import ( "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" ) -const ( - AmountDisallowedTrigger = 7 - AmountIgnoreWhitelistingTrigger = 49 - AmountIgnoreFreezingTrigger = 79 - AmountBurningTrigger = 101 - AmountMintingTrigger = 105 - AmountIgnoreBurnRateTrigger = 108 - AmountIgnoreSendCommissionRateTrigger = 109 +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) + AmountIgnoreSendCommissionRateTrigger = sdkmath.NewInt(109) + AmountDEXExpectToSpendTrigger = sdkmath.NewInt(103) + AmountDEXExpectToReceiveTrigger = sdkmath.NewInt(104) ) func TestKeeper_Extension_Issue(t *testing.T) { @@ -111,7 +113,7 @@ func TestKeeper_Extension_Issue(t *testing.T) { // send 7 coin will fail. // the test contract is written as such that sending 7 will fail. err = bankKeeper.SendCoins(ctx, settings.Issuer, receiver, sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountDisallowedTrigger))), + sdk.NewCoin(denom, AmountDisallowedTrigger)), ) requireT.ErrorIs(err, types.ErrExtensionCallFailed) balance = bankKeeper.GetBalance(ctx, receiver, denom) @@ -309,7 +311,7 @@ func TestKeeper_Extension_Whitelist(t *testing.T) { // sending trigger amount will be transferred despite whitelisted amount being exceeded err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreWhitelistingTrigger))), + sdk.NewCoin(denom, AmountIgnoreWhitelistingTrigger)), ) requireT.NoError(err) @@ -427,7 +429,7 @@ func TestKeeper_Extension_FreezeUnfreeze(t *testing.T) { // send trigger amount to transfer despite freezing err = bankKeeper.SendCoins(ctx, recipient, issuer, sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreFreezingTrigger))), + sdk.NewCoin(denom, AmountIgnoreFreezingTrigger)), ) requireT.NoError(err) @@ -489,7 +491,7 @@ func TestKeeper_Extension_Burn(t *testing.T) { err = bankKeeper.SendCoins(ctx, issuer, recipient, sdk.NewCoins(sdk.NewCoin(unburnableDenom, sdkmath.NewInt(102)))) requireT.NoError(err) - coinsToBurn := sdk.NewCoins(sdk.NewCoin(unburnableDenom, sdkmath.NewInt(AmountBurningTrigger))) + coinsToBurn := sdk.NewCoins(sdk.NewCoin(unburnableDenom, AmountBurningTrigger)) // try to burn unburnable token from the recipient account and make sure that extension can do it err = bankKeeper.SendCoins(ctx, recipient, issuer, coinsToBurn) @@ -514,12 +516,12 @@ func TestKeeper_Extension_Burn(t *testing.T) { // the amount should be burnt requireT.Equal( issuerBalanceBefore.String(), - issuerBalanceAfter.Add(sdk.NewCoin(unburnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + issuerBalanceAfter.Add(sdk.NewCoin(unburnableDenom, AmountBurningTrigger)).String(), ) requireT.Equal(cwExtensionBalanceBefore.String(), cwExtensionBalanceAfter.String()) requireT.Equal( totalSupplyBefore.Supply.String(), - totalSupplyAfter.Supply.Add(sdk.NewCoin(unburnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + totalSupplyAfter.Supply.Add(sdk.NewCoin(unburnableDenom, AmountBurningTrigger)).String(), ) // Issue a burnable fungible token @@ -560,7 +562,7 @@ func TestKeeper_Extension_Burn(t *testing.T) { // try to burn as non-issuer err = bankKeeper.SendCoins(ctx, recipient, issuer, sdk.NewCoins( - sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))), + sdk.NewCoin(burnableDenom, AmountBurningTrigger)), ) requireT.NoError(err) @@ -573,12 +575,12 @@ func TestKeeper_Extension_Burn(t *testing.T) { // the amount should be burnt requireT.Equal( recipientBalanceBefore.String(), - recipientBalanceAfter.Add(sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + recipientBalanceAfter.Add(sdk.NewCoin(burnableDenom, AmountBurningTrigger)).String(), ) requireT.Equal(cwExtensionBalanceBefore.String(), cwExtensionBalanceAfter.String()) requireT.Equal( totalSupplyBefore.Supply.String(), - totalSupplyAfter.Supply.Add(sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + totalSupplyAfter.Supply.Add(sdk.NewCoin(burnableDenom, AmountBurningTrigger)).String(), ) issuerBalanceBefore = bankKeeper.GetBalance(ctx, issuer, burnableDenom) @@ -589,7 +591,7 @@ func TestKeeper_Extension_Burn(t *testing.T) { // burn tokens and check balance and total supply err = bankKeeper.SendCoins(ctx, issuer, issuer, sdk.NewCoins( - sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))), + sdk.NewCoin(burnableDenom, AmountBurningTrigger)), ) requireT.NoError(err) @@ -602,12 +604,12 @@ func TestKeeper_Extension_Burn(t *testing.T) { // the amount should be burnt requireT.Equal( issuerBalanceBefore.String(), - issuerBalanceAfter.Add(sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + issuerBalanceAfter.Add(sdk.NewCoin(burnableDenom, AmountBurningTrigger)).String(), ) requireT.Equal(cwExtensionBalanceBefore.String(), cwExtensionBalanceAfter.String()) requireT.Equal( totalSupplyBefore.Supply.String(), - totalSupplyAfter.Supply.Add(sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))).String(), + totalSupplyAfter.Supply.Add(sdk.NewCoin(burnableDenom, AmountBurningTrigger)).String(), ) balance := bankKeeper.GetBalance(ctx, issuer, burnableDenom) @@ -622,10 +624,10 @@ func TestKeeper_Extension_Burn(t *testing.T) { requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) // try to burn non-issuer frozen coins - err = ftKeeper.Freeze(ctx, issuer, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))) + err = ftKeeper.Freeze(ctx, issuer, recipient, sdk.NewCoin(burnableDenom, AmountBurningTrigger)) requireT.NoError(err) err = bankKeeper.SendCoins(ctx, recipient, issuer, sdk.NewCoins( - sdk.NewCoin(burnableDenom, sdkmath.NewInt(AmountBurningTrigger))), + sdk.NewCoin(burnableDenom, AmountBurningTrigger)), ) requireT.ErrorContains(err, "Requested transfer token is frozen.") } @@ -672,7 +674,7 @@ func TestKeeper_Extension_Mint(t *testing.T) { // try to mint unmintable token err = bankKeeper.SendCoins(ctx, addr, addr, sdk.NewCoins( - sdk.NewCoin(unmintableDenom, sdkmath.NewInt(AmountMintingTrigger))), + sdk.NewCoin(unmintableDenom, AmountMintingTrigger)), ) requireT.ErrorContains(err, "feature minting is disabled") @@ -695,7 +697,7 @@ func TestKeeper_Extension_Mint(t *testing.T) { mintableDenom, err := ftKeeper.Issue(ctx, settings) requireT.NoError(err) - coinsToMint := sdk.NewCoins(sdk.NewCoin(mintableDenom, sdkmath.NewInt(AmountMintingTrigger))) + coinsToMint := sdk.NewCoins(sdk.NewCoin(mintableDenom, AmountMintingTrigger)) randomAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) @@ -783,7 +785,7 @@ func TestKeeper_Extension_BurnRate_BankSend(t *testing.T) { // send trigger amount from recipient1 to recipient2 (burn must not apply if the extension decides) recipient2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) err = bankKeeper.SendCoins(ctx, recipient, recipient2, sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreBurnRateTrigger)), + sdk.NewCoin(denom, AmountIgnoreBurnRateTrigger), )) requireT.NoError(err) @@ -1074,7 +1076,7 @@ func TestKeeper_Extension_SendCommissionRate_BankSend(t *testing.T) { // send trigger amount from recipient1 to recipient2 (send commission rate must not apply) recipient2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) err = bankKeeper.SendCoins(ctx, recipient, recipient2, sdk.NewCoins( - sdk.NewCoin(denom, sdkmath.NewInt(AmountIgnoreSendCommissionRateTrigger)), + sdk.NewCoin(denom, AmountIgnoreSendCommissionRateTrigger), )) requireT.NoError(err) 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 eb4bea6f6..21b5755ab 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 @@ -1,6 +1,6 @@ use crate::error::ContractError; use crate::msg::{ - ExecuteMsg, IBCPurpose, InstantiateMsg, QueryIssuanceMsgResponse, QueryMsg, SudoMsg, + DEXOrder, ExecuteMsg, IBCPurpose, InstantiateMsg, QueryIssuanceMsgResponse, QueryMsg, SudoMsg, TransferContext, }; use crate::state::{DENOM, EXTRA_DATA}; @@ -18,6 +18,7 @@ use cosmwasm_std::{entry_point, to_json_binary, CosmosMsg, StdError}; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128}; use cw2::set_contract_version; use std::ops::Div; +use std::string::ToString; // version info for migration info const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -32,6 +33,9 @@ const AMOUNT_IGNORE_BURN_RATE_TRIGGER: Uint128 = Uint128::new(108); const AMOUNT_IGNORE_SEND_COMMISSION_RATE_TRIGGER: Uint128 = Uint128::new(109); const AMOUNT_BLOCK_IBC_TRIGGER: Uint128 = Uint128::new(110); const AMOUNT_BLOCK_SMART_CONTRACT_TRIGGER: Uint128 = Uint128::new(111); +const ID_DEX_ORDER_TRIGGER: &str = "id-blocked"; +const AMOUNT_DEX_EXPECT_TO_SPEND_TRIGGER: Uint128 = Uint128::new(103); +const AMOUNT_DEX_EXPECT_TO_RECEIVE_TRIGGER: Uint128 = Uint128::new(104); #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -83,6 +87,11 @@ pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> CoreumResult sudo_extension_place_order(order, expected_to_spend, expected_to_receive), } } @@ -180,6 +189,20 @@ pub fn sudo_extension_transfer( Ok(response) } +pub fn sudo_extension_place_order( + order: DEXOrder, + expected_to_spend: Coin, + expected_to_receive: Coin, +) -> CoreumResult { + if order.id == ID_DEX_ORDER_TRIGGER + || expected_to_spend.amount == AMOUNT_DEX_EXPECT_TO_SPEND_TRIGGER.to_string() + || expected_to_receive.amount == AMOUNT_DEX_EXPECT_TO_RECEIVE_TRIGGER.to_string() + { + return Err(ContractError::DEXOrderPlacementError {}); + } + Ok(Response::new().add_attribute("method", "extension_place_order")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { 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 312f42007..10a2818c8 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 @@ -26,4 +26,7 @@ pub enum ContractError { #[error("Invalid amount.")] InvalidAmountError {}, + + #[error("DEX order placement is failed.")] + DEXOrderPlacementError {}, } diff --git a/x/asset/ft/keeper/test-contracts/asset-extension/src/msg.rs b/x/asset/ft/keeper/test-contracts/asset-extension/src/msg.rs index 3b1479bee..b89121160 100644 --- a/x/asset/ft/keeper/test-contracts/asset-extension/src/msg.rs +++ b/x/asset/ft/keeper/test-contracts/asset-extension/src/msg.rs @@ -1,3 +1,4 @@ +use coreum_wasm_sdk::types::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; @@ -15,6 +16,19 @@ pub struct IssuanceMsg { #[cw_serde] pub enum ExecuteMsg {} +#[cw_serde] +pub struct DEXOrder { + pub creator: String, + #[serde(rename = "type")] + pub order_type: String, + pub id: String, + pub base_denom: String, + pub quote_denom: String, + pub price: Option, + pub quantity: Uint128, + pub side: String, +} + #[cw_serde] pub enum SudoMsg { ExtensionTransfer { @@ -25,6 +39,11 @@ pub enum SudoMsg { burn_amount: Uint128, context: TransferContext, }, + ExtensionPlaceOrder { + order: DEXOrder, + expected_to_spend: Coin, + expected_to_receive: Coin, + }, } #[cw_serde] diff --git a/x/dex/keeper/keeper_ft_test.go b/x/dex/keeper/keeper_ft_test.go index 8ab7fec40..f41feddf9 100644 --- a/x/dex/keeper/keeper_ft_test.go +++ b/x/dex/keeper/keeper_ft_test.go @@ -24,6 +24,11 @@ import ( "github.com/CoreumFoundation/coreum/v5/x/dex/types" ) +var ( + AmountDEXExpectToSpendTrigger = sdkmath.NewInt(103) + AmountDEXExpectToReceiveTrigger = sdkmath.NewInt(104) +) + func TestKeeper_PlaceOrderWithExtension(t *testing.T) { testApp := simapp.New() sdkCtx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{ @@ -31,7 +36,6 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { AppHash: []byte("some-hash"), }) - acc, _ := testApp.GenAccount(sdkCtx) issuer, _ := testApp.GenAccount(sdkCtx) // extension @@ -45,7 +49,9 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { Subunit: "defext", Precision: 6, InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []assetfttypes.Feature{assetfttypes.Feature_extension}, + Features: []assetfttypes.Feature{ + assetfttypes.Feature_extension, + }, ExtensionSettings: &assetfttypes.ExtensionIssueSettings{ CodeId: codeID, }, @@ -53,28 +59,102 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { denomWithExtension, err := testApp.AssetFTKeeper.Issue(sdkCtx, settingsWithExtension) require.NoError(t, err) - order := types.Order{ - Creator: acc.String(), - Type: types.ORDER_TYPE_LIMIT, - ID: uuid.Generate().String(), - BaseDenom: denomWithExtension, - QuoteDenom: denom2, - Price: lo.ToPtr(types.MustNewPriceFromString("12e-1")), - Quantity: sdkmath.NewInt(10), - Side: types.SIDE_SELL, - GoodTil: &types.GoodTil{ - GoodTilBlockHeight: 390, + tests := []struct { + name string + order types.Order + wantDEXErr bool + }{ + { + name: "sell_positive", + order: types.Order{ + Creator: func() string { + creator, _ := testApp.GenAccount(sdkCtx) + return creator.String() + }(), + Type: types.ORDER_TYPE_LIMIT, + ID: uuid.Generate().String(), + BaseDenom: denomWithExtension, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("1")), + Quantity: sdkmath.NewInt(10), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + wantDEXErr: false, + }, + { + name: "sell_dex_error", + order: types.Order{ + Creator: func() string { + creator, _ := testApp.GenAccount(sdkCtx) + return creator.String() + }(), + Type: types.ORDER_TYPE_LIMIT, + ID: uuid.Generate().String(), + BaseDenom: denomWithExtension, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("1")), + Quantity: AmountDEXExpectToSpendTrigger, + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + wantDEXErr: true, + }, + { + name: "buy_positive", + order: types.Order{ + Creator: func() string { + creator, _ := testApp.GenAccount(sdkCtx) + return creator.String() + }(), + Type: types.ORDER_TYPE_LIMIT, + ID: uuid.Generate().String(), + BaseDenom: denom2, + QuoteDenom: denomWithExtension, + Price: lo.ToPtr(types.MustNewPriceFromString("1")), + Quantity: sdkmath.NewInt(10), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + wantDEXErr: false, + }, + { + name: "buy_dex_error", + order: types.Order{ + Creator: func() string { + creator, _ := testApp.GenAccount(sdkCtx) + return creator.String() + }(), + Type: types.ORDER_TYPE_LIMIT, + ID: uuid.Generate().String(), + BaseDenom: denom2, + QuoteDenom: denomWithExtension, + Price: lo.ToPtr(types.MustNewPriceFromString("1")), + Quantity: AmountDEXExpectToReceiveTrigger, + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + wantDEXErr: true, }, - TimeInForce: types.TIME_IN_FORCE_GTC, } - lockedBalance, err := order.ComputeLimitOrderLockedBalance() - require.NoError(t, err) - require.NoError(t, testApp.BankKeeper.SendCoins(sdkCtx, issuer, acc, sdk.NewCoins(lockedBalance))) - fundOrderReserve(t, testApp, sdkCtx, acc) - - require.ErrorContains( - t, testApp.DEXKeeper.PlaceOrder(sdkCtx, order), "is not supported for DEX, the token has extensions", - ) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creator := sdk.MustAccAddressFromBech32(tt.order.Creator) + lockedBalance, err := tt.order.ComputeLimitOrderLockedBalance() + require.NoError(t, err) + testApp.MintAndSendCoin(t, sdkCtx, creator, sdk.NewCoins(lockedBalance)) + fundOrderReserve(t, testApp, sdkCtx, creator) + if !tt.wantDEXErr { + require.NoError(t, testApp.DEXKeeper.PlaceOrder(sdkCtx, tt.order)) + } else { + require.ErrorContains( + t, + testApp.DEXKeeper.PlaceOrder(sdkCtx, tt.order), + "wasm error: DEX order placement is failed", + ) + } + }) + } } func TestKeeper_PlaceOrderWithDEXBlockFeature(t *testing.T) { diff --git a/x/wbank/keeper/keeper.go b/x/wbank/keeper/keeper.go index d7335a637..670889746 100644 --- a/x/wbank/keeper/keeper.go +++ b/x/wbank/keeper/keeper.go @@ -6,7 +6,6 @@ import ( sdkstore "cosmossdk.io/core/store" sdkerrors "cosmossdk.io/errors" "cosmossdk.io/log" - sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -192,15 +191,13 @@ func (k BaseKeeperWrapper) SpendableBalances( return nil, err } - bankLockedCoins := k.BaseKeeper.LockedCoins(ctx, addr) - balances := balancesRes.Balances for i := range balances { - bankLockedCoin := sdk.NewCoin(balances[i].Denom, bankLockedCoins.AmountOf(balances[i].Denom)) - balances[i], err = k.getSpendableCoin(sdk.UnwrapSDKContext(ctx), addr, balances[i], bankLockedCoin) + spendableCoin, err := k.ftProvider.GetSpendableBalance(sdk.UnwrapSDKContext(ctx), addr, balances[i].Denom) if err != nil { return nil, err } + balances[i] = spendableCoin } return &banktypes.QuerySpendableBalancesResponse{ @@ -231,9 +228,7 @@ func (k BaseKeeperWrapper) SpendableBalanceByDenom( return &banktypes.QuerySpendableBalanceByDenomResponse{}, nil } - bankLockedCoins := k.BaseKeeper.LockedCoins(ctx, addr) - bankLockedCoin := sdk.NewCoin(req.Denom, bankLockedCoins.AmountOf(req.Denom)) - spendableCoin, err := k.getSpendableCoin(sdk.UnwrapSDKContext(ctx), addr, *balanceRes.Balance, bankLockedCoin) + spendableCoin, err := k.ftProvider.GetSpendableBalance(sdk.UnwrapSDKContext(ctx), addr, balanceRes.Balance.Denom) if err != nil { return nil, err } @@ -243,30 +238,6 @@ func (k BaseKeeperWrapper) SpendableBalanceByDenom( }, nil } -func (k BaseKeeperWrapper) getSpendableCoin( - ctx sdk.Context, - addr sdk.AccAddress, - balance, bankLocked sdk.Coin, -) (sdk.Coin, error) { - denom := balance.Denom - notLockedAmt := balance.Amount. - Sub(bankLocked.Amount). - Sub(k.ftProvider.GetDEXLockedBalance(ctx, addr, denom).Amount) - - frozenBalance, err := k.ftProvider.GetFrozenBalance(ctx, addr, denom) - if err != nil { - return sdk.Coin{}, err - } - notFrozenAmt := balance.Amount.Sub(frozenBalance.Amount) - - spendableAmount := sdkmath.MinInt(notLockedAmt, notFrozenAmt) - if !spendableAmount.IsPositive() { - return sdk.NewCoin(denom, sdkmath.ZeroInt()), nil - } - - return sdk.NewCoin(denom, spendableAmount), nil -} - func (k BaseKeeperWrapper) isSmartContract(ctx sdk.Context, addr sdk.AccAddress) bool { return wasm.IsSmartContract(ctx, addr, k.wasmKeeper) } diff --git a/x/wbank/types/expected_keepers.go b/x/wbank/types/expected_keepers.go index 8bdf34708..0c4acc2ad 100644 --- a/x/wbank/types/expected_keepers.go +++ b/x/wbank/types/expected_keepers.go @@ -9,6 +9,6 @@ import ( type FungibleTokenProvider interface { BeforeSendCoins(ctx sdk.Context, fromAddress, toAddress sdk.AccAddress, coins sdk.Coins) error BeforeInputOutputCoins(ctx sdk.Context, input banktypes.Input, outputs []banktypes.Output) error - GetFrozenBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) + GetSpendableBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) GetDEXLockedBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin }