diff --git a/integration-tests/modules/dex_test.go b/integration-tests/modules/dex_test.go index 982066786..ceb306354 100644 --- a/integration-tests/modules/dex_test.go +++ b/integration-tests/modules/dex_test.go @@ -4,10 +4,13 @@ package modules import ( "context" + "encoding/json" + "fmt" "testing" "time" sdkmath "cosmossdk.io/math" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" sdk "github.com/cosmos/cosmos-sdk/types" cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -24,6 +27,7 @@ import ( "github.com/CoreumFoundation/coreum-tools/pkg/retry" integrationtests "github.com/CoreumFoundation/coreum/v5/integration-tests" + moduleswasm "github.com/CoreumFoundation/coreum/v5/integration-tests/contracts/modules" "github.com/CoreumFoundation/coreum/v5/pkg/client" "github.com/CoreumFoundation/coreum/v5/testutil/integration" assetfttypes "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" @@ -1400,7 +1404,7 @@ func TestLimitOrdersMatchingWithStaking(t *testing.T) { requireT.NoError(err) customStakingParams, err := customParamsClient.StakingParams(ctx, &customparamstypes.QueryStakingParamsRequest{}) - require.NoError(t, err) + requireT.NoError(err) delegateAmount := sdkmath.NewInt(1_000_000) @@ -1419,7 +1423,7 @@ func TestLimitOrdersMatchingWithStaking(t *testing.T) { _, validator1Address, deactivateValidator, err := chain.CreateValidator( ctx, t, customStakingParams.Params.MinSelfDelegation, customStakingParams.Params.MinSelfDelegation, ) - require.NoError(t, err) + requireT.NoError(err) defer deactivateValidator() balanceRes, err := assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{ @@ -1532,7 +1536,7 @@ func TestLimitOrdersMatchingWithBurnRate(t *testing.T) { chain.TxFactory().WithGas(chain.GasLimitByMsgs(issueMsg)), issueMsg, ) - require.NoError(t, err) + requireT.NoError(err) denom1 := assetfttypes.BuildDenom(issueMsg.Subunit, acc1) denom2 := issueFT(ctx, t, chain, acc2, sdkmath.NewIntWithDecimal(1, 6)) @@ -1681,7 +1685,7 @@ func TestLimitOrdersMatchingWithCommissionRate(t *testing.T) { chain.TxFactory().WithGas(chain.GasLimitByMsgs(issueMsg)), issueMsg, ) - require.NoError(t, err) + requireT.NoError(err) denom1 := assetfttypes.BuildDenom(issueMsg.Subunit, acc1) denom2 := issueFT(ctx, t, chain, acc2, sdkmath.NewIntWithDecimal(1, 6)) @@ -2160,6 +2164,215 @@ func issueFT( return assetfttypes.BuildDenom(issueMsg.Subunit, issuer) } +// TestAssetFTBlockSmartContractsFeatureWithDEX tests the dex module integration with the asset ft +// block_smart_contracts features. +func TestAssetFTBlockSmartContractsFeatureWithDEX(t *testing.T) { + t.Parallel() + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + dexClient := dextypes.NewQueryClient(chain.ClientContext) + + dexParamsRes, err := dexClient.Params(ctx, &dextypes.QueryParamsRequest{}) + requireT.NoError(err) + + issuer := chain.GenAccount() + chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetfttypes.MsgIssue{}, + &banktypes.MsgSend{}, + &banktypes.MsgSend{}, + &banktypes.MsgSend{}, + &banktypes.MsgSend{}, + }, + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount.MulRaw(2), + }) + + acc := chain.GenAccount() + chain.FundAccountWithOptions(ctx, t, acc, integration.BalancesOptions{ + // 2 to place directly and 1 through the smart contract + Amount: dexParamsRes.Params.OrderReserve.Amount.MulRaw(3). + Add(sdkmath.NewInt(100_000).MulRaw(3)). + Add(chain.QueryDEXParams(ctx, t).OrderReserve.Amount), + }) + + issue1Msg := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "BLK" + uuid.NewString()[:4], + Subunit: "blk" + uuid.NewString()[:4], + Precision: 5, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []assetfttypes.Feature{assetfttypes.Feature_block_smart_contracts}, + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(issue1Msg)), + issue1Msg, + ) + requireT.NoError(err) + denom1WithBlockSmartContract := assetfttypes.BuildDenom(issue1Msg.Subunit, issuer) + + // issue 2nd denom without block_smart_contracts + denom2 := issueFT(ctx, t, chain, issuer, sdkmath.NewIntWithDecimal(1, 6)) + + sendMsg1 := &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: acc.String(), + Amount: sdk.NewCoins(sdk.NewCoin(denom1WithBlockSmartContract, sdkmath.NewInt(100))), + } + sendMsg2 := &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: acc.String(), + Amount: sdk.NewCoins(sdk.NewCoin(denom2, sdkmath.NewInt(100))), + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactoryAuto(), + sendMsg1, sendMsg2, + ) + requireT.NoError(err) + + // we expect to receive denom with block_smart_contracts feature + placeBuyOrderMsg := &dextypes.MsgPlaceOrder{ + Sender: acc.String(), + Type: dextypes.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denom1WithBlockSmartContract, + QuoteDenom: denom2, + Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")), + Quantity: sdkmath.NewInt(100), + Side: dextypes.SIDE_BUY, + TimeInForce: dextypes.TIME_IN_FORCE_GTC, + } + // send it to chain directly + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc), + chain.TxFactoryAuto(), + placeBuyOrderMsg, + ) + requireT.NoError(err) + + // we expect to spend denom with block_smart_contracts feature + placeSellOrderMsg := &dextypes.MsgPlaceOrder{ + Sender: acc.String(), + Type: dextypes.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: denom2, + QuoteDenom: denom1WithBlockSmartContract, + Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")), + Quantity: sdkmath.NewInt(100), + Side: dextypes.SIDE_BUY, + TimeInForce: dextypes.TIME_IN_FORCE_GTC, + } + // send it to chain directly + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(acc), + chain.TxFactoryAuto(), + placeSellOrderMsg, + ) + requireT.NoError(err) + + // now same tokens but for the DEX smart contract + + // fund the DEX smart contract + issuanceReq := issueFTRequest{ + Symbol: "CTR", + Subunit: "ctr", + Precision: 6, + InitialAmount: sdkmath.NewInt(100).String(), + } + issuerFTInstantiatePayload, err := json.Marshal(issuanceReq) + requireT.NoError(err) + + // instantiate new contract from the acc (the contract issues a token, but we don't use it for the test) + contractAddr, _, err := chain.Wasm.DeployAndInstantiateWASMContract( + ctx, + chain.TxFactoryAuto(), + acc, + moduleswasm.DEXWASM, + integration.InstantiateConfig{ + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee, + AccessType: wasmtypes.AccessTypeUnspecified, + Payload: issuerFTInstantiatePayload, + Label: "dex", + }, + ) + requireT.NoError(err) + + // it's prohibited to send tokens to the DEX smart contract with the denom with block_smart_contracts feature, + // that's why we can't place and order with it + sendMsg1 = &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: contractAddr, + Amount: sdk.NewCoins(sdk.NewCoin(denom1WithBlockSmartContract, sdkmath.NewInt(100))), + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg1)), + sendMsg1, + ) + requireT.Error(err) + requireT.True(cosmoserrors.ErrUnauthorized.Is(err)) + requireT.ErrorContains(err, "transfers to smart contracts are disabled") + + // send tokens to place and order from the smart contract + sendMsg2 = &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: contractAddr, + Amount: sdk.NewCoins(sdk.NewCoin(denom2, sdkmath.NewInt(100))), + } + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg2)), + sendMsg2, + ) + requireT.NoError(err) + + placeOrderPayload, err := json.Marshal(map[dexMethod]placeOrderBodyDEXRequest{ + dexMethodPlaceOrder: { + Order: dextypes.Order{ + Creator: contractAddr, + Type: dextypes.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denom1WithBlockSmartContract, + QuoteDenom: denom2, + Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")), + Quantity: sdkmath.NewInt(100), + Side: dextypes.SIDE_BUY, + TimeInForce: dextypes.TIME_IN_FORCE_GTC, + // next attributes are required by smart contract, but not used + RemainingQuantity: sdkmath.ZeroInt(), + RemainingBalance: sdkmath.ZeroInt(), + GoodTil: nil, + Reserve: sdk.NewCoin("denom1", sdkmath.ZeroInt()), + }, + }, + }) + requireT.NoError(err) + + // however the contract has the coins to place such and order, the placement is failed because the order expects + // to receive the asset ft with block_smart_contracts feature + _, err = chain.Wasm.ExecuteWASMContract( + ctx, + chain.TxFactoryAuto(), + acc, + contractAddr, + placeOrderPayload, + chain.NewCoin(chain.QueryDEXParams(ctx, t).OrderReserve.Amount), + ) + requireT.Error(err) + requireT.ErrorContains( + err, + fmt.Sprintf("usage of %s is not supported for DEX in smart contract", denom1WithBlockSmartContract), + ) +} + func ordersToPlaceMsgs(orders []dextypes.Order) []sdk.Msg { return lo.Map(orders, func(order dextypes.Order, _ int) sdk.Msg { return &dextypes.MsgPlaceOrder{ diff --git a/integration-tests/modules/wasm_test.go b/integration-tests/modules/wasm_test.go index cff47670c..276b48389 100644 --- a/integration-tests/modules/wasm_test.go +++ b/integration-tests/modules/wasm_test.go @@ -3694,7 +3694,7 @@ func TestWASMDEXInContract(t *testing.T) { admin, moduleswasm.DEXWASM, integration.InstantiateConfig{ - Amount: chain.QueryDEXParams(ctx, t).OrderReserve, + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee, AccessType: wasmtypes.AccessTypeUnspecified, Payload: issuerFTInstantiatePayload, Label: "dex", diff --git a/x/asset/ft/keeper/keeper.go b/x/asset/ft/keeper/keeper.go index 8b876fa1d..bcf222726 100644 --- a/x/asset/ft/keeper/keeper.go +++ b/x/asset/ft/keeper/keeper.go @@ -708,7 +708,7 @@ func (k Keeper) SetWhitelistedBalances(ctx sdk.Context, addr sdk.AccAddress, coi func (k Keeper) DEXIncreaseLimits( ctx sdk.Context, addr sdk.AccAddress, lockCoin, reserveWhitelistingCoin sdk.Coin, ) error { - if err := k.dexChecksForDenoms(ctx, []string{lockCoin.Denom, reserveWhitelistingCoin.Denom}); err != nil { + if err := k.dexChecksForDenoms(ctx, addr, lockCoin.Denom, reserveWhitelistingCoin.Denom); err != nil { return err } @@ -754,7 +754,7 @@ func (k Keeper) DEXDecreaseLimitsAndSend( func (k Keeper) DEXChecksLimitsAndSend( ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, sendCoin, checkReserveWhitelistingCoin sdk.Coin, ) error { - if err := k.dexChecksForDenoms(ctx, []string{sendCoin.Denom, checkReserveWhitelistingCoin.Denom}); err != nil { + if err := k.dexChecksForDenoms(ctx, fromAddr, sendCoin.Denom, checkReserveWhitelistingCoin.Denom); err != nil { return err } @@ -1631,7 +1631,10 @@ func (k Keeper) dexLockingChecks(ctx sdk.Context, addr sdk.AccAddress, coin sdk. return nil } -func (k Keeper) dexChecksForDenoms(ctx sdk.Context, denoms []string) error { +func (k Keeper) dexChecksForDenoms( + ctx sdk.Context, acc sdk.AccAddress, spendDenom, receiveDenom string, +) error { + denoms := []string{spendDenom, receiveDenom} for _, denom := range denoms { def, err := k.getDefinitionOrNil(ctx, denom) if err != nil { @@ -1642,15 +1645,25 @@ func (k Keeper) dexChecksForDenoms(ctx sdk.Context, denoms []string) error { if def.ExtensionCWAddress != "" { return sdkerrors.Wrapf( types.ErrInvalidInput, - "failed to DEX lock %s, not supported for the tokens with extensions", + "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, - "locking coins for DEX disabled for %s", - def.Denom, + "usage of %s is not supported for DEX, the token has %s feature enabled", + def.Denom, types.Feature_dex_block.String(), + ) + } + // don't allow the smart contract to use the denom with Feature_block_smart_contracts if not admin + if def.IsFeatureEnabled(types.Feature_block_smart_contracts) && + !def.HasAdminPrivileges(acc) && + cwasmtypes.IsTriggeredBySmartContract(ctx) { + return sdkerrors.Wrapf( + cosmoserrors.ErrUnauthorized, + "usage of %s is not supported for DEX in smart contract, the token has %s feature enabled", + def.Denom, types.Feature_block_smart_contracts.String(), ) } } diff --git a/x/asset/ft/keeper/keeper_test.go b/x/asset/ft/keeper/keeper_test.go index bf7dae210..08665c727 100644 --- a/x/asset/ft/keeper/keeper_test.go +++ b/x/asset/ft/keeper/keeper_test.go @@ -31,6 +31,7 @@ import ( "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" + cwasmtypes "github.com/CoreumFoundation/coreum/v5/x/wasm/types" wbankkeeper "github.com/CoreumFoundation/coreum/v5/x/wbank/keeper" wibctransfertypes "github.com/CoreumFoundation/coreum/v5/x/wibctransfer/types" ) @@ -1952,7 +1953,71 @@ func TestKeeper_DEXLockAndUnlock(t *testing.T) { requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(extensionCoin))) requireT.ErrorContains( ftKeeper.DEXIncreaseLimits(ctx, acc, extensionCoin, sdk.NewInt64Coin(denom1, 1)), - "not supported for the tokens with extensions", + "the token has extensions", + ) +} + +func TestKeeper_DEXBlockSmartContracts(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()) + + settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEFBLK", + Subunit: "defblk", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_block_smart_contracts, + }, + } + denom, err := ftKeeper.Issue(ctx, settings) + requireT.NoError(err) + blockSmartContractCoin := sdk.NewInt64Coin(denom, 50) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(blockSmartContractCoin))) + // triggered from native call + requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, blockSmartContractCoin, sdk.NewInt64Coin(denom1, 1))) + + ctxFromSmartContract := cwasmtypes.WithSmartContractSender(ctx, acc.String()) + blockingErr := fmt.Sprintf("usage of %s is not supported for DEX in smart contract", denom) + requireT.ErrorContains( + ftKeeper.DEXIncreaseLimits(ctxFromSmartContract, acc, blockSmartContractCoin, sdk.NewInt64Coin(denom1, 1)), + blockingErr, + ) + requireT.ErrorContains( + ftKeeper.DEXIncreaseLimits(ctxFromSmartContract, acc, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin), + blockingErr, + ) + // same check for DEXChecksLimitsAndSend + requireT.ErrorContains( + ftKeeper.DEXChecksLimitsAndSend(ctxFromSmartContract, acc, acc, blockSmartContractCoin, sdk.NewInt64Coin(denom1, 1)), + blockingErr, + ) + requireT.ErrorContains( + ftKeeper.DEXChecksLimitsAndSend(ctxFromSmartContract, acc, acc, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin), + blockingErr, + ) + + // but still allowed to lock by admin + testApp.MintAndSendCoin(t, ctxFromSmartContract, issuer, sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))) + requireT.NoError( + ftKeeper.DEXChecksLimitsAndSend( + ctxFromSmartContract, issuer, issuer, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin, + ), + ) + requireT.NoError( + ftKeeper.DEXIncreaseLimits(ctxFromSmartContract, issuer, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin), ) } @@ -1993,7 +2058,7 @@ func TestKeeper_DEXSettings_BlockDEX(t *testing.T) { ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) requireT.NoError(err) - errStr := fmt.Sprintf("locking coins for DEX disabled for %s", ft1Denom) + errStr := fmt.Sprintf("usage of %s is not supported for DEX, the token has dex_block", ft1Denom) requireT.ErrorContains(ftKeeper.DEXIncreaseLimits( ctx, acc, sdk.NewInt64Coin(ft1Denom, 50), sdk.NewInt64Coin(denom1, 1), ), errStr) diff --git a/x/dex/keeper/keeper_ft_test.go b/x/dex/keeper/keeper_ft_test.go index 5560377b6..d00ebee47 100644 --- a/x/dex/keeper/keeper_ft_test.go +++ b/x/dex/keeper/keeper_ft_test.go @@ -72,7 +72,9 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { 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), "not supported for the tokens with extensions") + require.ErrorContains( + t, testApp.DEXKeeper.PlaceOrder(sdkCtx, order), "is not supported for DEX, the token has extensions", + ) } func TestKeeper_PlaceOrderWithDEXBlockFeature(t *testing.T) { @@ -116,7 +118,7 @@ func TestKeeper_PlaceOrderWithDEXBlockFeature(t *testing.T) { require.NoError(t, err) require.NoError(t, testApp.BankKeeper.SendCoins(sdkCtx, issuer, acc, sdk.NewCoins(lockedBalance))) fundOrderReserve(t, testApp, sdkCtx, acc) - errStr := fmt.Sprintf("locking coins for DEX disabled for %s", denomWithExtension) + errStr := fmt.Sprintf("usage of %s is not supported for DEX, the token has dex_block", denomWithExtension) require.ErrorContains(t, testApp.DEXKeeper.PlaceOrder(sdkCtx, order), errStr) // use the denomWithExtension as quote