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 1af7e7947..b1f816002 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 @@ -6,11 +6,10 @@ use crate::msg::{ use crate::state::{DENOM, EXTRA_DATA}; use coreum_wasm_sdk::deprecated::core::{CoreumMsg, CoreumResult}; use coreum_wasm_sdk::types::coreum::asset::ft::v1::{ - MsgBurn, MsgMint, QueryFrozenBalanceRequest, QueryFrozenBalanceResponse, QueryTokenRequest, - QueryTokenResponse, Token, + MsgBurn, MsgMint, QueryTokenRequest, QueryTokenResponse, Token, }; use coreum_wasm_sdk::types::cosmos::bank::v1beta1::{ - MsgSend, QueryBalanceRequest, QueryBalanceResponse, + MsgSend, }; use coreum_wasm_sdk::types::cosmos::base::v1beta1::Coin; use cosmwasm_std::{entry_point, to_json_binary, CosmosMsg, StdError}; @@ -18,6 +17,7 @@ use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, use cw2::set_contract_version; use std::ops::Div; use std::string::ToString; +use cosmwasm_schema::schemars::_serde_json::to_string; // version info for migration info const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -189,7 +189,15 @@ pub fn sudo_extension_place_order( { return Err(ContractError::DEXOrderPlacementError {}); } - Ok(Response::new().add_attribute("method", "extension_place_order")) + + let order_data = to_string(&order). + map_err(|_| ContractError::Std(StdError::generic_err("failed to serialize order to json string")))?; + + Ok( + Response::new() + .add_attribute("method", "extension_place_order") + .add_attribute("order_data", order_data) + ) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -376,24 +384,6 @@ fn assert_burn_rate( .add_message(CosmosMsg::Any(burn_message.to_any()))) } -fn query_frozen_balance(deps: Deps, account: &str, denom: &str) -> StdResult { - let request = QueryFrozenBalanceRequest { - account: account.to_string(), - denom: denom.to_string(), - }; - let frozen_balance: QueryFrozenBalanceResponse = request.query(&deps.querier)?; - Ok(frozen_balance.balance.unwrap_or_default()) -} - -fn query_bank_balance(deps: Deps, account: &str, denom: &str) -> StdResult { - let request = QueryBalanceRequest { - address: account.to_string(), - denom: denom.to_string(), - }; - let bank_balance: QueryBalanceResponse = request.query(&deps.querier)?; - Ok(bank_balance.balance.unwrap_or_default()) -} - fn query_token(deps: Deps, denom: &str) -> StdResult { let request = QueryTokenRequest { denom: denom.to_string(), 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 b89121160..1d036648f 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 @@ -22,6 +22,7 @@ pub struct DEXOrder { #[serde(rename = "type")] pub order_type: String, pub id: String, + pub sequence: u64, pub base_denom: String, pub quote_denom: String, pub price: Option, diff --git a/x/asset/ft/spec/README.md b/x/asset/ft/spec/README.md index 65d8715dc..e7a445424 100644 --- a/x/asset/ft/spec/README.md +++ b/x/asset/ft/spec/README.md @@ -193,8 +193,6 @@ Same rules apply to receiving tokens over IBC transfer protocol if IBC is enable When token is created, admin decides if users may send and receive it over IBC transfer protocol. If IBC feature is disabled token can never leave the Coreum chain. -The IBC feature is incompatible with the extension feature. The reason for it is that extension can override -the full functionality of ibc, so allowing the two features together can lead to confusion. ### Clawback @@ -265,13 +263,16 @@ The fields are: - `ibc_purpose`: if it is an ibc transfer, indicates whether it's an outgoing, incoming, acknowledged or timed-out transfer -_**Note**: The extension feature is not compatible with ibc and block smart contract feature. It will error out if you -try -to enable those features at the same time._ +_**Note**: The extension feature is not compatible with block smart contract feature. It will error out if you +try to enable those features at the same time._ There is a sample implementation of extension in `x/asset/ft/keeper/test-contracts/asset-extension` which can be used to take inspiration from, when implementing other extensions. +#### DEX extension + +The `extension` is also integrate with the DEX check [DEX spec](../../../dex/spec/README.md#Extension) for more details. + ### DEX unified ref amount. The `unified_ref_amount` DEX setting can be updated by the token admin or gov. @@ -300,7 +301,7 @@ the `dex_whitelisted_denoms` feature is not enabled or not the `whitelisted_deno Features - Extensions + Extension Default diff --git a/x/asset/ft/types/dex.go b/x/asset/ft/types/dex.go index b73ab0f3b..497d7aa91 100644 --- a/x/asset/ft/types/dex.go +++ b/x/asset/ft/types/dex.go @@ -25,6 +25,7 @@ type DEXOrder struct { Creator sdk.AccAddress `json:"creator"` Type string `json:"type"` ID string `json:"id"` + Sequence uint64 `json:"sequence"` BaseDenom string `json:"base_denom"` QuoteDenom string `json:"quote_denom"` Price *string `json:"price,omitempty"` // might be nil diff --git a/x/dex/keeper/keeper_ft_test.go b/x/dex/keeper/keeper_ft_test.go index e3fba1017..ccab0e01c 100644 --- a/x/dex/keeper/keeper_ft_test.go +++ b/x/dex/keeper/keeper_ft_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "encoding/json" "fmt" "testing" "time" @@ -18,14 +19,19 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/require" + "github.com/CoreumFoundation/coreum/v5/testutil/event" "github.com/CoreumFoundation/coreum/v5/testutil/simapp" testcontracts "github.com/CoreumFoundation/coreum/v5/x/asset/ft/keeper/test-contracts" assetfttypes "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" "github.com/CoreumFoundation/coreum/v5/x/dex/types" ) -var ( +const ( + ExtensionOrderDataWASMAttribute = "order_data" IDDEXOrderSuffixTrigger = "blocked" +) + +var ( AmountDEXExpectToSpendTrigger = sdkmath.NewInt(103) AmountDEXExpectToReceiveTrigger = sdkmath.NewInt(104) ) @@ -84,7 +90,7 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { wantDEXErr: false, }, { - name: "sell_dex_error_spend_amount", + name: "sell_dex_error", order: types.Order{ Creator: func() string { creator, _ := testApp.GenAccount(sdkCtx) @@ -101,24 +107,6 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { }, wantDEXErr: true, }, - { - name: "sell_dex_error_order_id", - order: types.Order{ - Creator: func() string { - creator, _ := testApp.GenAccount(sdkCtx) - return creator.String() - }(), - Type: types.ORDER_TYPE_LIMIT, - ID: uuid.Generate().String()[:10] + IDDEXOrderSuffixTrigger, - 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: true, - }, { name: "buy_positive", order: types.Order{ @@ -138,7 +126,7 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { wantDEXErr: false, }, { - name: "buy_dex_error_receive_amount", + name: "buy_dex_error", order: types.Order{ Creator: func() string { creator, _ := testApp.GenAccount(sdkCtx) @@ -164,13 +152,38 @@ func TestKeeper_PlaceOrderWithExtension(t *testing.T) { testApp.MintAndSendCoin(t, sdkCtx, creator, sdk.NewCoins(lockedBalance)) fundOrderReserve(t, testApp, sdkCtx, creator) if !tt.wantDEXErr { + sdkCtx = sdkCtx.WithEventManager(sdk.NewEventManager()) require.NoError(t, testApp.DEXKeeper.PlaceOrder(sdkCtx, tt.order)) + + // decode wasm events + orderStr, err := event.FindStringEventAttribute( + sdkCtx.EventManager().Events().ToABCIEvents(), + wasmtypes.WasmModuleEventType, + ExtensionOrderDataWASMAttribute, + ) + require.NoError(t, err) + + extensionOrderData := assetfttypes.DEXOrder{} + require.NoError(t, json.Unmarshal([]byte(orderStr), &extensionOrderData)) + + order, err := testApp.DEXKeeper.GetOrderByAddressAndID(sdkCtx, creator, tt.order.ID) + require.NoError(t, err) + + require.Equal(t, assetfttypes.DEXOrder{ + Creator: sdk.MustAccAddressFromBech32(order.Creator), + Type: order.Type.String(), + ID: order.ID, + Sequence: order.Sequence, + BaseDenom: order.BaseDenom, + QuoteDenom: order.QuoteDenom, + Price: lo.ToPtr(order.Price.String()), + Quantity: order.Quantity, + Side: order.Side.String(), + }, extensionOrderData) } else { - err := testApp.DEXKeeper.PlaceOrder(sdkCtx, tt.order) - require.ErrorIs(t, err, assetfttypes.ErrExtensionCallFailed) require.ErrorContains( t, - err, + testApp.DEXKeeper.PlaceOrder(simapp.CopyContextWithMultiStore(sdkCtx), tt.order), "wasm error: DEX order placement is failed", ) } diff --git a/x/dex/keeper/keeper_matching_result.go b/x/dex/keeper/keeper_matching_result.go index 2e5b9b722..9b476f506 100644 --- a/x/dex/keeper/keeper_matching_result.go +++ b/x/dex/keeper/keeper_matching_result.go @@ -46,6 +46,7 @@ func NewMatchingResult(order types.Order) (*MatchingResult, error) { Creator: takerAddress, Type: order.Type.String(), ID: order.ID, + Sequence: order.Sequence, BaseDenom: order.BaseDenom, QuoteDenom: order.QuoteDenom, Price: orderStrPrice, diff --git a/x/dex/spec/README.md b/x/dex/spec/README.md index 3c2eb32ff..47326c342 100644 --- a/x/dex/spec/README.md +++ b/x/dex/spec/README.md @@ -280,7 +280,98 @@ This feature introduces an enhancement to the asset FT (`restrict_dex` feature), This adds a layer of control over trading pairs, ensuring that denom(asset FT) can only be exchanged with certain denoms/currencies or assets, as specified by the admin. -### Extensions +### Extension + +The `extension` feature integrates the DEX with the smart contract extension capability of asset FT. This integration +enables asset FT extension contracts to define custom extension functions that the DEX invokes before executing an +order. The extension functions can leverage order details, including the amounts expected to be spent and received +post-execution, to implement custom rules. For example, they can validate an order against specific business logic or +constraints. + +#### Asset FT features handling + +If the extension feature is enabled for a token but no contract extension function is implemented, all order +placements will fail. When multiple asset FT features are enabled, the rules of the asset FT features are validated +first, followed by invoking the extension. + +#### Example of WASM Call Implementation in an Extension Smart Contract + +Below is an example showcasing how to implement the extension functionality in a smart contract: + +##### Code Example + +```rust +#[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, +} + +pub enum SudoMsg { + ExtensionTransfer { + recipient: String, + sender: String, + transfer_amount: Uint128, + commission_amount: Uint128, + burn_amount: Uint128, + context: TransferContext, + }, + ExtensionPlaceOrder { + order: DEXOrder, + expected_to_spend: Coin, + expected_to_receive: Coin, + }, +} + +#[entry_point] +pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> CoreumResult { + match msg { + SudoMsg::ExtensionTransfer { + recipient, + sender, + transfer_amount, + commission_amount, + burn_amount, + context, + } => sudo_extension_transfer( + deps, + env, + recipient, + sender, + transfer_amount, + commission_amount, + burn_amount, + context, + ), + SudoMsg::ExtensionPlaceOrder { + order, + expected_to_spend, + expected_to_receive, + } => sudo_extension_place_order( + deps, + env, + order, + expected_to_spend, + expected_to_receive, + ), + } +} +``` + +**`SudoMsg` Enum**: + +- Defines the types of messages the extension supports, such as `ExtensionTransfer` and `ExtensionPlaceOrder`. +- Each variant carries the necessary information to execute specific logic. + +**`sudo` Entry Point**: + +- Acts as the main function that handles incoming messages. +- Delegates to specific helper functions based on the message type. -The current version of the DEX doesn't support the extensions. It means if a user places an order with the asset FT -extension token, such an order will be rejected. diff --git a/x/wasm/handler/query_grpc.go b/x/wasm/handler/query_grpc.go index 7b91515fe..4785456e6 100644 --- a/x/wasm/handler/query_grpc.go +++ b/x/wasm/handler/query_grpc.go @@ -2,7 +2,6 @@ package handler import ( "fmt" - "sync" msgv1 "cosmossdk.io/api/cosmos/msg/v1" queryv1 "cosmossdk.io/api/cosmos/query/v1" @@ -25,7 +24,6 @@ type GRPCQuerier struct { codec codec.Codec // map[query proto URL]proto response type acceptedQueries map[string]func() gogoproto.Message - mu sync.Mutex } // NewGRPCQuerier returns a new instance of GRPCQuerier. @@ -40,7 +38,6 @@ func NewGRPCQuerier(gRPCQueryRouter *baseapp.GRPCQueryRouter, codec codec.Codec) gRPCQueryRouter: gRPCQueryRouter, codec: codec, acceptedQueries: acceptedQueries, - mu: sync.Mutex{}, } } @@ -59,12 +56,10 @@ func (q *GRPCQuerier) Query(ctx sdk.Context, request *wasmvmtypes.GrpcQuery) (go return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)} } - q.mu.Lock() res, err := handler(ctx, &abci.RequestQuery{ Data: request.Data, Path: request.Path, }) - q.mu.Unlock() if err != nil { return nil, err } diff --git a/x/wasm/handler/query_grpc_test.go b/x/wasm/handler/query_grpc_test.go index 514a34379..3106b952f 100644 --- a/x/wasm/handler/query_grpc_test.go +++ b/x/wasm/handler/query_grpc_test.go @@ -19,7 +19,7 @@ import ( "github.com/CoreumFoundation/coreum/v5/x/wasm/handler" ) -func TestGRPCQuerier_Query(t *testing.T) { +func TestGRPCQuerier(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -49,13 +49,15 @@ func TestGRPCQuerier_Query(t *testing.T) { eg, _ := errgroup.WithContext(ctx) for range 1000 { + // rebuild the ctx + routineSDKCtx := testApp.BaseApp.NewContext(false) eg.Go(func() error { wasmGrpcReq := &wasmvmtypes.GrpcQuery{ Data: wasmGrpcData, // url which corresponds query token Path: "/coreum.asset.ft.v1.Query/Token", } - wasmGrpcRes, err := q.Query(sdkCtx, wasmGrpcReq) + wasmGrpcRes, err := q.Query(routineSDKCtx, wasmGrpcReq) if err != nil { return err }