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

Integrate asset FT extensions with the DEX. #1029

Merged
merged 9 commits into from
Nov 26, 2024
2 changes: 1 addition & 1 deletion build/coreum/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 0 additions & 3 deletions build/coreum/generate-proto-breaking.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//nolint:unused
package coreum

import (
Expand All @@ -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)

Expand Down
178 changes: 177 additions & 1 deletion integration-tests/modules/assetft_extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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 (
Expand All @@ -49,7 +50,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
})

Expand Down Expand Up @@ -1806,3 +1807,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: sdkmath.NewInt(104), // 104 is prohibited by extensions smart contract
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: sdkmath.NewInt(103), // 103 is prohibited by extensions smart contract
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())
}
16 changes: 7 additions & 9 deletions x/asset/ft/keeper/before_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@ 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"
cwasmtypes "github.com/CoreumFoundation/coreum/v5/x/wasm/types"
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.
//
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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
}
Expand Down
27 changes: 19 additions & 8 deletions x/asset/ft/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,25 +706,36 @@ func (k Keeper) GetSpendableBalance(
ctx sdk.Context,
addr sdk.AccAddress,
denom string,
) sdk.Coin {
) (sdk.Coin, error) {
balance := k.bankKeeper.GetBalance(ctx, addr, denom)
if balance.Amount.IsZero() {
return balance
return balance, nil
}

notLockedAmt := balance.Amount.
Sub(k.GetDEXLockedBalance(ctx, addr, denom).Amount).
Sub(k.bankKeeper.LockedCoins(ctx, addr).AmountOf(denom))
if notLockedAmt.IsNegative() {
return sdk.NewCoin(denom, sdkmath.ZeroInt())
return sdk.NewCoin(denom, sdkmath.ZeroInt()), nil
}
notFrozenAmt := balance.Amount.Sub(k.GetFrozenBalance(ctx, addr, denom).Amount)
if notFrozenAmt.IsNegative() {
return sdk.NewCoin(denom, sdkmath.ZeroInt())

def, err := k.getDefinitionOrNil(ctx, denom)
if err != nil {
return sdk.Coin{}, err
}
// the spendable balance counts the frozen balance, but if extensions are not enabled
if def != nil &&
def.IsFeatureEnabled(types.Feature_freezing) &&
!def.IsFeatureEnabled(types.Feature_extension) {
notFrozenAmt := balance.Amount.Sub(k.GetFrozenBalance(ctx, addr, denom).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)
return sdk.NewCoin(denom, notLockedAmt), nil
}

// TransferAdmin changes admin of a fungible token.
Expand Down
Loading
Loading