Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block DEX for tokens with block_smart_contracts feature calling from the smart contracts. #1015

Merged
merged 7 commits into from
Nov 5, 2024
221 changes: 217 additions & 4 deletions integration-tests/modules/dex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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{
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/modules/wasm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 19 additions & 6 deletions x/asset/ft/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
)
}
}
Expand Down
Loading