From adc5f4d51760cb59e6cfdcefb35a4329102f685e Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Fri, 15 Nov 2024 15:30:57 +0300 Subject: [PATCH] Refactor integration of asset FT with the DEX to accept all `DEXActions` by asset FT required to be applied. --- integration-tests/modules/dex_test.go | 4 +- x/asset/ft/keeper/keeper.go | 591 ------------- x/asset/ft/keeper/keeper_dex.go | 645 ++++++++++++++ x/asset/ft/keeper/keeper_dex_test.go | 979 ++++++++++++++++++++++ x/asset/ft/keeper/keeper_test.go | 863 +------------------ x/asset/ft/types/dex.go | 125 +++ x/asset/ft/types/errors.go | 6 +- x/dex/genesis_test.go | 4 +- x/dex/keeper/keeper.go | 29 +- x/dex/keeper/keeper_ft_lock.go | 128 --- x/dex/keeper/keeper_matching.go | 128 ++- x/dex/keeper/keeper_matching_fuzz_test.go | 2 +- x/dex/keeper/keeper_matching_result.go | 343 +++----- x/dex/keeper/keeper_matching_test.go | 387 ++++++++- x/dex/types/expected_keepers.go | 16 +- x/wbank/keeper/keeper_test.go | 2 +- 16 files changed, 2354 insertions(+), 1898 deletions(-) create mode 100644 x/asset/ft/keeper/keeper_dex.go create mode 100644 x/asset/ft/keeper/keeper_dex_test.go create mode 100644 x/asset/ft/types/dex.go delete mode 100644 x/dex/keeper/keeper_ft_lock.go diff --git a/integration-tests/modules/dex_test.go b/integration-tests/modules/dex_test.go index a20782906..a88d2fe1b 100644 --- a/integration-tests/modules/dex_test.go +++ b/integration-tests/modules/dex_test.go @@ -843,7 +843,7 @@ func TestLimitOrdersMatchingWithAssetFTFreeze(t *testing.T) { chain.TxFactoryAuto(), placeSellOrderMsg, ) - requireT.ErrorContains(err, assetfttypes.ErrDEXLockFailed.Error()) + requireT.ErrorContains(err, assetfttypes.ErrDEXInsufficientSpendableBalance.Error()) balanceRes, err = assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{ Account: acc1.String(), @@ -1487,7 +1487,7 @@ func TestLimitOrdersMatchingWithStaking(t *testing.T) { chain.TxFactoryAuto(), placeSellOrderMsg, ) - requireT.ErrorContains(err, assetfttypes.ErrDEXLockFailed.Error()) + requireT.ErrorContains(err, assetfttypes.ErrDEXInsufficientSpendableBalance.Error()) chain.FundAccountWithOptions(ctx, t, acc, integration.BalancesOptions{ Amount: delegateAmount.Add(sdkmath.NewInt(100_000)), diff --git a/x/asset/ft/keeper/keeper.go b/x/asset/ft/keeper/keeper.go index f67cdb223..4f37f7485 100644 --- a/x/asset/ft/keeper/keeper.go +++ b/x/asset/ft/keeper/keeper.go @@ -18,8 +18,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/pkg/errors" - "github.com/samber/lo" "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" "github.com/CoreumFoundation/coreum/v5/x/wasm" @@ -704,287 +702,6 @@ func (k Keeper) SetWhitelistedBalances(ctx sdk.Context, addr sdk.AccAddress, coi } } -// DEXIncreaseLimits increases the DEX limits. -func (k Keeper) DEXIncreaseLimits( - ctx sdk.Context, addr sdk.AccAddress, lockedCoin, expectedToReceiveCoin sdk.Coin, -) error { - if err := k.dexChecksForDenoms(ctx, addr, lockedCoin.Denom, expectedToReceiveCoin.Denom); err != nil { - return err - } - - if err := k.DEXLock(ctx, addr, lockedCoin); err != nil { - return err - } - - return k.DEXIncreaseExpectedToReceive(ctx, addr, expectedToReceiveCoin) -} - -// DEXDecreaseLimits decreases the DEX limits. -func (k Keeper) DEXDecreaseLimits( - ctx sdk.Context, - addr sdk.AccAddress, - lockedCoin, expectedToReceiveCoin sdk.Coin, -) error { - if err := k.DEXUnlock(ctx, addr, lockedCoin); err != nil { - return err - } - - return k.DEXDecreaseExpectedToReceive(ctx, addr, expectedToReceiveCoin) -} - -// DEXDecreaseLimitsAndSend decreases the DEX limits and sends the coin. -func (k Keeper) DEXDecreaseLimitsAndSend( - ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, unlockAndSendCoin, decreaseExpectedToReceiveCoin sdk.Coin, -) error { - if err := k.DEXUnlock(ctx, fromAddr, unlockAndSendCoin); err != nil { - return err - } - if err := k.DEXDecreaseExpectedToReceive(ctx, fromAddr, decreaseExpectedToReceiveCoin); err != nil { - return err - } - // send using the native bank - if err := k.bankKeeper.SendCoins(ctx, fromAddr, toAddr, sdk.NewCoins(unlockAndSendCoin)); err != nil { - return sdkerrors.Wrap(err, "failed to send DEX coins") - } - - return nil -} - -// DEXCheckLimitsAndSend checks DEX limits and sends the coin. -func (k Keeper) DEXCheckLimitsAndSend( - ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, sendCoin, checkExpectedToReceiveCoin sdk.Coin, -) error { - if err := k.dexChecksForDenoms(ctx, fromAddr, sendCoin.Denom, checkExpectedToReceiveCoin.Denom); err != nil { - return err - } - - if err := k.dexLockingChecks(ctx, fromAddr, sendCoin); err != nil { - return err - } - - // check the whitelisted balance only if we record the expected to receive amount - shouldCheckWhitelistedBalance, err := k.shouldRecordExpectedToReceiveBalance( - ctx, fromAddr, checkExpectedToReceiveCoin.Denom, - ) - if err != nil { - return err - } - if shouldCheckWhitelistedBalance { - if err := k.validateWhitelistedBalance(ctx, fromAddr, checkExpectedToReceiveCoin); err != nil { - return err - } - } - - if err := k.bankKeeper.SendCoins(ctx, fromAddr, toAddr, sdk.NewCoins(sendCoin)); err != nil { - return sdkerrors.Wrap(err, "failed to DEX send coins") - } - - return nil -} - -// DEXIncreaseExpectedToReceive increases the expected to receive amount for the specified account. -func (k Keeper) DEXIncreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { - if !coin.IsPositive() { - return sdkerrors.Wrap( - cosmoserrors.ErrInvalidCoins, "amount to increase DEX expected to receive must be positive", - ) - } - - shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, addr, coin.Denom) - if err != nil { - return err - } - if !shouldRecord { - return nil - } - - if err := k.validateWhitelistedBalance(ctx, addr, coin); err != nil { - return err - } - - dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) - prevExpectedToReceiveBalance, newExpectedToReceiveBalance := dexExpectedToReceiveStore.AddBalance(coin) - - if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXExpectedToReceiveAmountChanged{ - Account: addr.String(), - Denom: coin.Denom, - PreviousAmount: prevExpectedToReceiveBalance.Amount, - CurrentAmount: newExpectedToReceiveBalance.Amount, - }); err != nil { - return sdkerrors.Wrapf( - types.ErrInvalidState, "failed to emit EventDEXExpectedToReceiveAmountChanged event: %s", err, - ) - } - - return nil -} - -// DEXDecreaseExpectedToReceive decreases the expected to receive amount for the specified account. -func (k Keeper) DEXDecreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { - if !coin.IsPositive() { - return sdkerrors.Wrap( - cosmoserrors.ErrInvalidCoins, "amount to decrease DEX expected to receive must be positive", - ) - } - - shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, addr, coin.Denom) - if err != nil { - return err - } - if !shouldRecord { - return nil - } - - dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) - prevExpectedToReceiveBalance, newExpectedToReceiveBalance, err := dexExpectedToReceiveStore.SubBalance(coin) - if err != nil { - return sdkerrors.Wrap(err, "failed to cancel DEX whitelisted") - } - - if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXExpectedToReceiveAmountChanged{ - Account: addr.String(), - Denom: coin.Denom, - PreviousAmount: prevExpectedToReceiveBalance.Amount, - CurrentAmount: newExpectedToReceiveBalance.Amount, - }); err != nil { - return sdkerrors.Wrapf( - types.ErrInvalidState, "failed to emit EventDEXExpectedToReceiveAmountChanged event: %s", err, - ) - } - - return nil -} - -// GetDEXExpectedToReceivedBalance returns the DEX expected to receive balance. -func (k Keeper) GetDEXExpectedToReceivedBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { - return k.dexExpectedToReceiveAccountBalanceStore(ctx, addr).Balance(denom) -} - -// GetDEXExpectedToReceiveBalances returns the DEX expected to receive balances of an account. -func (k Keeper) GetDEXExpectedToReceiveBalances( - ctx sdk.Context, - addr sdk.AccAddress, - pagination *query.PageRequest, -) (sdk.Coins, *query.PageResponse, error) { - return k.dexExpectedToReceiveAccountBalanceStore(ctx, addr).Balances(pagination) -} - -// GetAccountsDEXExpectedToReceiveBalances returns the DEX expected to receive balance on all the account. -func (k Keeper) GetAccountsDEXExpectedToReceiveBalances( - ctx sdk.Context, - pagination *query.PageRequest, -) ([]types.Balance, *query.PageResponse, error) { - return collectBalances(k.cdc, k.dexExpectedToReceiveBalancesStore(ctx), pagination) -} - -// SetDEXExpectedToReceiveBalances sets the DEX expected to receive balances of a specified account. -// Pay attention that the sdk.NewCoins() sanitizes/removes the empty coins, hence if you -// need set zero amount use the slice []sdk.Coins. -func (k Keeper) SetDEXExpectedToReceiveBalances(ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) { - dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) - for _, coin := range coins { - dexExpectedToReceiveStore.SetBalance(coin) - } -} - -// DEXLock locks specified token for the specified account. -func (k Keeper) DEXLock(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { - if err := k.dexLockingChecks(ctx, addr, coin); err != nil { - return err - } - - dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) - prevLockedBalance, newLockedBalance := dexLockedStore.AddBalance(coin) - - if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXLockedAmountChanged{ - Account: addr.String(), - Denom: coin.Denom, - PreviousAmount: prevLockedBalance.Amount, - CurrentAmount: newLockedBalance.Amount, - }); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXLockedAmountChanged event: %s", err) - } - - return nil -} - -// DEXUnlock unlocks specified tokens from the specified account. -func (k Keeper) DEXUnlock(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { - if !coin.IsPositive() { - return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "amount to unlock DEX tokens must be positive") - } - - dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) - prevLockedBalance, newLockedBalance, err := dexLockedStore.SubBalance(coin) - if err != nil { - return sdkerrors.Wrap(err, "failed to unlock DEX") - } - - if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXLockedAmountChanged{ - Account: addr.String(), - Denom: coin.Denom, - PreviousAmount: prevLockedBalance.Amount, - CurrentAmount: newLockedBalance.Amount, - }); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXLockedAmountChanged event: %s", err) - } - - return nil -} - -// GetDEXLockedBalance returns the DEX locked balance. -func (k Keeper) GetDEXLockedBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { - return k.dexLockedAccountBalanceStore(ctx, addr).Balance(denom) -} - -// GetDEXLockedBalances returns the DEX locked balances of an account. -func (k Keeper) GetDEXLockedBalances( - ctx sdk.Context, - addr sdk.AccAddress, - pagination *query.PageRequest, -) (sdk.Coins, *query.PageResponse, error) { - return k.dexLockedAccountBalanceStore(ctx, addr).Balances(pagination) -} - -// GetAccountsDEXLockedBalances returns the DEX locked balance on all the account. -func (k Keeper) GetAccountsDEXLockedBalances( - ctx sdk.Context, - pagination *query.PageRequest, -) ([]types.Balance, *query.PageResponse, error) { - return collectBalances(k.cdc, k.dexLockedBalancesStore(ctx), pagination) -} - -// SetDEXLockedBalances sets the DEX locked balances of a specified account. -// Pay attention that the sdk.NewCoins() sanitizes/removes the empty coins, hence if you -// need set zero amount use the slice []sdk.Coins. -func (k Keeper) SetDEXLockedBalances(ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) { - dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) - for _, coin := range coins { - dexLockedStore.SetBalance(coin) - } -} - -// ValidateDEXCancelOrdersByDenomIsAllowed validates whether the cancellation of orders by denom is allowed. -func (k Keeper) ValidateDEXCancelOrdersByDenomIsAllowed(ctx sdk.Context, addr sdk.AccAddress, denom string) error { - def, err := k.GetDefinition(ctx, denom) - if err != nil { - return err - } - - if !def.HasAdminPrivileges(addr) { - return sdkerrors.Wrapf(cosmoserrors.ErrUnauthorized, "only admin is able to cancel orders by denom %s", denom) - } - if !def.IsFeatureEnabled(types.Feature_dex_order_cancellation) { - return sdkerrors.Wrapf( - cosmoserrors.ErrUnauthorized, - "order cancellation is not allowed by denom %s, feature %s is disabled", - denom, types.Feature_dex_order_cancellation, - ) - } - - return nil -} - // GetSpendableBalance returns balance allowed to be spent. func (k Keeper) GetSpendableBalance( ctx sdk.Context, @@ -1002,7 +719,6 @@ func (k Keeper) GetSpendableBalance( if notLockedAmt.IsNegative() { return sdk.NewCoin(denom, sdkmath.ZeroInt()) } - notFrozenAmt := balance.Amount.Sub(k.GetFrozenBalance(ctx, addr, denom).Amount) if notFrozenAmt.IsNegative() { return sdk.NewCoin(denom, sdkmath.ZeroInt()) @@ -1081,130 +797,6 @@ func (k Keeper) ClearAdmin(ctx sdk.Context, sender sdk.AccAddress, denom string) return nil } -// SetDEXSettings sets the DEX settings of a specified denom. -func (k Keeper) SetDEXSettings(ctx sdk.Context, denom string, settings types.DEXSettings) { - ctx.KVStore(k.storeKey).Set(types.CreateDEXSettingsKey(denom), k.cdc.MustMarshal(&settings)) -} - -// GetDEXSettings gets the DEX settings of a specified denom. -func (k Keeper) GetDEXSettings(ctx sdk.Context, denom string) (types.DEXSettings, error) { - bz := ctx.KVStore(k.storeKey).Get(types.CreateDEXSettingsKey(denom)) - if bz == nil { - return types.DEXSettings{}, sdkerrors.Wrapf(types.ErrDEXSettingsNotFound, "denom: %s", denom) - } - var settings types.DEXSettings - k.cdc.MustUnmarshal(bz, &settings) - - return settings, nil -} - -// GetDEXSettingsWithDenoms returns all DEX settings with the corresponding denoms. -func (k Keeper) GetDEXSettingsWithDenoms( - ctx sdk.Context, - pagination *query.PageRequest, -) ([]types.DEXSettingsWithDenom, *query.PageResponse, error) { - dexSettings := make([]types.DEXSettingsWithDenom, 0) - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXSettingsKeyPrefix) - pageRes, err := query.Paginate(store, pagination, func(key, value []byte) error { - denom, err := types.DecodeDenomFromKey(key) - if err != nil { - return err - } - var settings types.DEXSettings - k.cdc.MustUnmarshal(value, &settings) - - dexSettings = append(dexSettings, types.DEXSettingsWithDenom{ - Denom: denom, - DEXSettings: settings, - }) - - return nil - }) - - return dexSettings, pageRes, err -} - -// UpdateDEXUnifiedRefAmount updates the DEX unified ref amount . -func (k Keeper) UpdateDEXUnifiedRefAmount( - ctx sdk.Context, - sender sdk.AccAddress, - denom string, - unifiedRefAmount sdkmath.LegacyDec, -) error { - return k.updateDEXSettings(ctx, sender, denom, types.DEXSettings{UnifiedRefAmount: &unifiedRefAmount}) -} - -// UpdateDEXWhitelistedDenoms updates the DEX whitelisted of a specified denoms. -func (k Keeper) UpdateDEXWhitelistedDenoms( - ctx sdk.Context, - sender sdk.AccAddress, - denom string, - whitelistedDenoms []string, -) error { - if whitelistedDenoms == nil { - // check to prevent mistakes using the `updateDEXSettings` method, set to empty slice if the input is nil - whitelistedDenoms = make([]string, 0) - } - return k.updateDEXSettings(ctx, sender, denom, types.DEXSettings{WhitelistedDenoms: whitelistedDenoms}) -} - -func (k Keeper) updateDEXSettings( - ctx sdk.Context, - sender sdk.AccAddress, - denom string, - settings types.DEXSettings, -) error { - prevSettings, err := k.getDEXSettingsOrNil(ctx, denom) - if err != nil { - return err - } - if prevSettings == nil { - prevSettings = &types.DEXSettings{} - } - - newSettings := *prevSettings - // update not nil settings - if settings.WhitelistedDenoms != nil { - newSettings.WhitelistedDenoms = settings.WhitelistedDenoms - } - if settings.UnifiedRefAmount != nil { - newSettings.UnifiedRefAmount = settings.UnifiedRefAmount - } - - if err := types.ValidateDEXSettings(settings); err != nil { - return err - } - - def, err := k.getDefinitionOrNil(ctx, denom) - if err != nil { - return err - } - // the gov can update any DEX setting even if the features are disabled - if k.authority != sender.String() { //nolint:nestif // the ifs are for the error checks mostly - if def != nil { - if !def.IsAdmin(sender) { - return sdkerrors.Wrap(cosmoserrors.ErrUnauthorized, "only admin and gov can update DEX settings") - } - if err := types.ValidateDEXSettingsAccess(newSettings, *def); err != nil { - return err - } - } else { - return sdkerrors.Wrap(cosmoserrors.ErrUnauthorized, "only admin or gov can update DEX settings") - } - } - - k.SetDEXSettings(ctx, denom, newSettings) - - if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXSettingsChanged{ - PreviousSettings: prevSettings, - NewSettings: newSettings, - }); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXSettingsChanged event: %s", err) - } - - return nil -} - func (k Keeper) mintIfReceivable( ctx sdk.Context, def types.Definition, @@ -1565,32 +1157,6 @@ func (k Keeper) whitelistedAccountBalanceStore(ctx sdk.Context, addr sdk.AccAddr return newBalanceStore(k.cdc, ctx.KVStore(k.storeKey), types.CreateWhitelistedBalancesKey(addr)) } -// dexExpectedToReceiveBalancesStore get the store for the DEX expected to receive balances of all accounts. -func (k Keeper) dexExpectedToReceiveBalancesStore(ctx sdk.Context) prefix.Store { - return prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXExpectedToReceiveBalancesKeyPrefix) -} - -// dexExpectedToReceiveAccountBalanceStore gets the store for the DEX expected to receive balances of an account. -func (k Keeper) dexExpectedToReceiveAccountBalanceStore(ctx sdk.Context, addr sdk.AccAddress) balanceStore { - return newBalanceStore(k.cdc, ctx.KVStore(k.storeKey), types.CreateDEXExpectedToReceiveBalancesKey(addr)) -} - -func (k Keeper) shouldRecordExpectedToReceiveBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) (bool, error) { - def, err := k.getDefinitionOrNil(ctx, denom) - if err != nil { - return false, err - } - // increase for FT with the whitelisting enabled - if def == nil || - !def.IsFeatureEnabled(types.Feature_whitelisting) || - // it's prohibited to whitelist the admin - def.IsAdmin(addr) { - return false, nil - } - - return true, nil -} - func (k Keeper) validateWhitelistedBalance(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) whitelistedBalance := k.GetWhitelistedBalance(ctx, addr, coin.Denom) @@ -1609,163 +1175,6 @@ func (k Keeper) validateWhitelistedBalance(ctx sdk.Context, addr sdk.AccAddress, return nil } -// dexLockedBalancesStore get the store for the DEX locked balances of all accounts. -func (k Keeper) dexLockedBalancesStore(ctx sdk.Context) prefix.Store { - return prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXLockedBalancesKeyPrefix) -} - -// dexLockedAccountBalanceStore gets the store for the DEX locked balances of an account. -func (k Keeper) dexLockedAccountBalanceStore(ctx sdk.Context, addr sdk.AccAddress) balanceStore { - return newBalanceStore(k.cdc, ctx.KVStore(k.storeKey), types.CreateDEXLockedBalancesKey(addr)) -} - -func (k Keeper) dexLockingChecks(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { - if !coin.IsPositive() { - return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "amount to lock DEX tokens must be positive") - } - - balance := k.bankKeeper.GetBalance(ctx, addr, coin.Denom) - if err := k.validateCoinIsNotLockedByDEXAndBank(ctx, addr, balance, coin); err != nil { - return sdkerrors.Wrapf(types.ErrDEXLockFailed, "%s", err) - } - - frozenAmt := k.GetFrozenBalance(ctx, addr, coin.Denom).Amount - notFrozenTotalAmt := balance.Amount.Sub(frozenAmt) - if notFrozenTotalAmt.LT(coin.Amount) { - return sdkerrors.Wrapf( - types.ErrDEXLockFailed, - "failed to DEX lock %s available %s%s", - coin.String(), - notFrozenTotalAmt, - coin.Denom, - ) - } - - return nil -} - -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 { - return err - } - - if err := k.dexChecksForDefinition(ctx, acc, def); err != nil { - return err - } - - // settings specific validation - settings, err := k.getDEXSettingsOrNil(ctx, denom) - if err != nil { - return err - } - - if settings != nil { - // validate whitelisted denoms - for _, tradeDenom := range denoms { - if denom == tradeDenom || len(settings.WhitelistedDenoms) == 0 { - continue - } - if !lo.Contains(settings.WhitelistedDenoms, tradeDenom) { - return sdkerrors.Wrapf( - cosmoserrors.ErrUnauthorized, - "locking coins for DEX is prohibited, denom %s not whitelisted for %s", - tradeDenom, denom, - ) - } - } - } - } - - return nil -} - -func (k Keeper) dexChecksForDefinition(ctx sdk.Context, acc sdk.AccAddress, def *types.Definition) error { - if def == nil { - return nil - } - - 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, - "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(), - ) - } - - if def.IsFeatureEnabled(types.Feature_freezing) { - if k.isGloballyFrozen(ctx, def.Denom) && - // sill allow the admin to do the trade, to follow same logic as we have in the sending - !def.HasAdminPrivileges(acc) { - return sdkerrors.Wrapf( - cosmoserrors.ErrUnauthorized, - "usage of %s for DEX is blocked because the token is globally frozen", - def.Denom, - ) - } - } - - return nil -} - -func (k Keeper) validateCoinIsNotLockedByDEXAndBank( - ctx sdk.Context, - addr sdk.AccAddress, - balance, coin sdk.Coin, -) error { - dexLockedAmt := k.GetDEXLockedBalance(ctx, addr, coin.Denom).Amount - availableAmt := balance.Amount.Sub(dexLockedAmt) - if availableAmt.LT(coin.Amount) { - return sdkerrors.Wrapf(cosmoserrors.ErrInsufficientFunds, "%s is not available, available %s%s", - coin.String(), availableAmt.String(), coin.Denom) - } - - bankLockedAmt := k.bankKeeper.LockedCoins(ctx, addr).AmountOf(coin.Denom) - // validate that we don't use the coins locked by bank - availableAmt = availableAmt.Sub(bankLockedAmt) - if availableAmt.LT(coin.Amount) { - return sdkerrors.Wrapf(cosmoserrors.ErrInsufficientFunds, "%s is not available, available %s%s", - coin.String(), availableAmt.String(), coin.Denom) - } - - return nil -} - -func (k Keeper) getDEXSettingsOrNil(ctx sdk.Context, denom string) (*types.DEXSettings, error) { - dexSettings, err := k.GetDEXSettings(ctx, denom) - if err != nil { - if errors.Is(err, types.ErrDEXSettingsNotFound) { - return nil, nil //nolint:nilnil //returns nil if data not found - } - return nil, err - } - - return &dexSettings, nil -} - // logger returns the Keeper logger. func (k Keeper) logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) diff --git a/x/asset/ft/keeper/keeper_dex.go b/x/asset/ft/keeper/keeper_dex.go new file mode 100644 index 000000000..73f6a0cf9 --- /dev/null +++ b/x/asset/ft/keeper/keeper_dex.go @@ -0,0 +1,645 @@ +package keeper + +import ( + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/pkg/errors" + "github.com/samber/lo" + + "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" + cwasmtypes "github.com/CoreumFoundation/coreum/v5/x/wasm/types" +) + +// 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. +func (k Keeper) DEXExecuteActions(ctx sdk.Context, actions types.DEXActions) error { + if err := k.DEXCheckOrderAmounts( + ctx, + actions.Order, + actions.CreatorExpectedToSpend, + actions.CreatorExpectedToReceive, + ); err != nil { + return err + } + + for _, lock := range actions.IncreaseLocked { + if err := k.DEXIncreaseLocked(ctx, lock.Address, lock.Coin); err != nil { + return err + } + } + + for _, unlock := range actions.DecreaseLocked { + if err := k.DEXDecreaseLocked(ctx, unlock.Address, unlock.Coin); err != nil { + return err + } + } + + for _, increase := range actions.IncreaseExpectedToReceive { + if err := k.DEXIncreaseExpectedToReceive(ctx, increase.Address, increase.Coin); err != nil { + return err + } + } + + for _, decrease := range actions.DecreaseExpectedToReceive { + if err := k.DEXDecreaseExpectedToReceive(ctx, decrease.Address, decrease.Coin); err != nil { + return err + } + } + + for _, send := range actions.Send { + k.logger(ctx).Debug( + "DEX sending coin", + "from", send.FromAddress.String(), + "to", send.ToAddress.String(), + "coin", send.Coin.String(), + ) + if err := k.bankKeeper.SendCoins(ctx, send.FromAddress, send.ToAddress, sdk.NewCoins(send.Coin)); err != nil { + return sdkerrors.Wrap(err, "failed to DEX send coins") + } + } + + return nil +} + +// DEXDecreaseLimits decreases the DEX limits. +func (k Keeper) DEXDecreaseLimits( + ctx sdk.Context, + addr sdk.AccAddress, + lockedCoins sdk.Coins, expectedToReceiveCoin sdk.Coin, +) error { + for _, coin := range lockedCoins { + if err := k.DEXDecreaseLocked(ctx, addr, coin); err != nil { + return err + } + } + + return k.DEXDecreaseExpectedToReceive(ctx, addr, expectedToReceiveCoin) +} + +// DEXCheckOrderAmounts validates that the order's creator is allowed to place and order with the provided amounts. +func (k Keeper) DEXCheckOrderAmounts( + ctx sdk.Context, + 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 { + 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) +} + +// SetDEXSettings sets the DEX settings of a specified denom. +func (k Keeper) SetDEXSettings(ctx sdk.Context, denom string, settings types.DEXSettings) { + ctx.KVStore(k.storeKey).Set(types.CreateDEXSettingsKey(denom), k.cdc.MustMarshal(&settings)) +} + +// GetDEXSettings gets the DEX settings of a specified denom. +func (k Keeper) GetDEXSettings(ctx sdk.Context, denom string) (types.DEXSettings, error) { + bz := ctx.KVStore(k.storeKey).Get(types.CreateDEXSettingsKey(denom)) + if bz == nil { + return types.DEXSettings{}, sdkerrors.Wrapf(types.ErrDEXSettingsNotFound, "denom: %s", denom) + } + var settings types.DEXSettings + k.cdc.MustUnmarshal(bz, &settings) + + return settings, nil +} + +// GetDEXSettingsWithDenoms returns all DEX settings with the corresponding denoms. +func (k Keeper) GetDEXSettingsWithDenoms( + ctx sdk.Context, + pagination *query.PageRequest, +) ([]types.DEXSettingsWithDenom, *query.PageResponse, error) { + dexSettings := make([]types.DEXSettingsWithDenom, 0) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXSettingsKeyPrefix) + pageRes, err := query.Paginate(store, pagination, func(key, value []byte) error { + denom, err := types.DecodeDenomFromKey(key) + if err != nil { + return err + } + var settings types.DEXSettings + k.cdc.MustUnmarshal(value, &settings) + + dexSettings = append(dexSettings, types.DEXSettingsWithDenom{ + Denom: denom, + DEXSettings: settings, + }) + + return nil + }) + + return dexSettings, pageRes, err +} + +// UpdateDEXUnifiedRefAmount updates the DEX unified ref amount . +func (k Keeper) UpdateDEXUnifiedRefAmount( + ctx sdk.Context, + sender sdk.AccAddress, + denom string, + unifiedRefAmount sdkmath.LegacyDec, +) error { + return k.updateDEXSettings(ctx, sender, denom, types.DEXSettings{UnifiedRefAmount: &unifiedRefAmount}) +} + +// UpdateDEXWhitelistedDenoms updates the DEX whitelisted denoms of a specified denom. +func (k Keeper) UpdateDEXWhitelistedDenoms( + ctx sdk.Context, + sender sdk.AccAddress, + denom string, + whitelistedDenoms []string, +) error { + if whitelistedDenoms == nil { + // check to prevent mistakes using the `updateDEXSettings` method, set to empty slice if the input is nil + whitelistedDenoms = make([]string, 0) + } + return k.updateDEXSettings(ctx, sender, denom, types.DEXSettings{WhitelistedDenoms: whitelistedDenoms}) +} + +// DEXIncreaseExpectedToReceive increases the expected to receive amount for the specified account. +func (k Keeper) DEXIncreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { + k.logger(ctx).Debug("DEX increasing expected to receive coin", "address", addr.String(), "coin", coin.String()) + if !coin.IsPositive() { + return sdkerrors.Wrap( + cosmoserrors.ErrInvalidCoins, "amount to increase DEX expected to receive must be positive", + ) + } + + shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, coin.Denom) + if err != nil { + return err + } + if !shouldRecord { + return nil + } + + dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) + prevExpectedToReceiveBalance, newExpectedToReceiveBalance := dexExpectedToReceiveStore.AddBalance(coin) + + if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXExpectedToReceiveAmountChanged{ + Account: addr.String(), + Denom: coin.Denom, + PreviousAmount: prevExpectedToReceiveBalance.Amount, + CurrentAmount: newExpectedToReceiveBalance.Amount, + }); err != nil { + return sdkerrors.Wrapf( + types.ErrInvalidState, "failed to emit EventDEXExpectedToReceiveAmountChanged event: %s", err, + ) + } + + return nil +} + +// DEXDecreaseExpectedToReceive decreases the expected to receive amount for the specified account. +func (k Keeper) DEXDecreaseExpectedToReceive(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { + k.logger(ctx).Debug("DEX decreasing expected to receive coin", "address", addr.String(), "coin", coin.String()) + if !coin.IsPositive() { + return sdkerrors.Wrap( + cosmoserrors.ErrInvalidCoins, "amount to decrease DEX expected to receive must be positive", + ) + } + + shouldRecord, err := k.shouldRecordExpectedToReceiveBalance(ctx, coin.Denom) + if err != nil { + return err + } + if !shouldRecord { + return nil + } + + dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) + prevExpectedToReceiveBalance, newExpectedToReceiveBalance, err := dexExpectedToReceiveStore.SubBalance(coin) + if err != nil { + return sdkerrors.Wrap(err, "failed to cancel DEX whitelisted") + } + + if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXExpectedToReceiveAmountChanged{ + Account: addr.String(), + Denom: coin.Denom, + PreviousAmount: prevExpectedToReceiveBalance.Amount, + CurrentAmount: newExpectedToReceiveBalance.Amount, + }); err != nil { + return sdkerrors.Wrapf( + types.ErrInvalidState, "failed to emit EventDEXExpectedToReceiveAmountChanged event: %s", err, + ) + } + + return nil +} + +// GetDEXExpectedToReceivedBalance returns the DEX expected to receive balance. +func (k Keeper) GetDEXExpectedToReceivedBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { + return k.dexExpectedToReceiveAccountBalanceStore(ctx, addr).Balance(denom) +} + +// GetDEXExpectedToReceiveBalances returns the DEX expected to receive balances of an account. +func (k Keeper) GetDEXExpectedToReceiveBalances( + ctx sdk.Context, + addr sdk.AccAddress, + pagination *query.PageRequest, +) (sdk.Coins, *query.PageResponse, error) { + return k.dexExpectedToReceiveAccountBalanceStore(ctx, addr).Balances(pagination) +} + +// GetAccountsDEXExpectedToReceiveBalances returns the DEX expected to receive balance on all the account. +func (k Keeper) GetAccountsDEXExpectedToReceiveBalances( + ctx sdk.Context, + pagination *query.PageRequest, +) ([]types.Balance, *query.PageResponse, error) { + return collectBalances(k.cdc, k.dexExpectedToReceiveBalancesStore(ctx), pagination) +} + +// SetDEXExpectedToReceiveBalances sets the DEX expected to receive balances of a specified account. +// Pay attention that the sdk.NewCoins() sanitizes/removes the empty coins, hence if you +// need set zero amount use the slice []sdk.Coins. +func (k Keeper) SetDEXExpectedToReceiveBalances(ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) { + dexExpectedToReceiveStore := k.dexExpectedToReceiveAccountBalanceStore(ctx, addr) + for _, coin := range coins { + dexExpectedToReceiveStore.SetBalance(coin) + } +} + +// DEXIncreaseLocked locks specified token for the specified account. +func (k Keeper) DEXIncreaseLocked(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { + k.logger(ctx).Debug("DEX increasing locked coin", "addr", addr.String(), "coin", coin.String()) + if !coin.IsPositive() { + return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "amount to lock DEX tokens must be positive") + } + + 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) + } + + dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) + prevLockedBalance, newLockedBalance := dexLockedStore.AddBalance(coin) + + if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXLockedAmountChanged{ + Account: addr.String(), + Denom: coin.Denom, + PreviousAmount: prevLockedBalance.Amount, + CurrentAmount: newLockedBalance.Amount, + }); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXLockedAmountChanged event: %s", err) + } + + return nil +} + +// DEXDecreaseLocked unlocks specified tokens from the specified account. +func (k Keeper) DEXDecreaseLocked(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { + k.logger(ctx).Debug("DEX decrease locked coin", "address", addr.String(), "coin", coin.String()) + if !coin.IsPositive() { + return sdkerrors.Wrap(cosmoserrors.ErrInvalidCoins, "amount to unlock DEX tokens must be positive") + } + + dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) + prevLockedBalance, newLockedBalance, err := dexLockedStore.SubBalance(coin) + if err != nil { + return sdkerrors.Wrap(err, "failed to unlock DEX") + } + + if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXLockedAmountChanged{ + Account: addr.String(), + Denom: coin.Denom, + PreviousAmount: prevLockedBalance.Amount, + CurrentAmount: newLockedBalance.Amount, + }); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXLockedAmountChanged event: %s", err) + } + + return nil +} + +// GetDEXLockedBalance returns the DEX locked balance. +func (k Keeper) GetDEXLockedBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { + return k.dexLockedAccountBalanceStore(ctx, addr).Balance(denom) +} + +// GetDEXLockedBalances returns the DEX locked balances of an account. +func (k Keeper) GetDEXLockedBalances( + ctx sdk.Context, + addr sdk.AccAddress, + pagination *query.PageRequest, +) (sdk.Coins, *query.PageResponse, error) { + return k.dexLockedAccountBalanceStore(ctx, addr).Balances(pagination) +} + +// GetAccountsDEXLockedBalances returns the DEX locked balance on all the account. +func (k Keeper) GetAccountsDEXLockedBalances( + ctx sdk.Context, + pagination *query.PageRequest, +) ([]types.Balance, *query.PageResponse, error) { + return collectBalances(k.cdc, k.dexLockedBalancesStore(ctx), pagination) +} + +// SetDEXLockedBalances sets the DEX locked balances of a specified account. +// Pay attention that the sdk.NewCoins() sanitizes/removes the empty coins, hence if you +// need set zero amount use the slice []sdk.Coins. +func (k Keeper) SetDEXLockedBalances(ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) { + dexLockedStore := k.dexLockedAccountBalanceStore(ctx, addr) + for _, coin := range coins { + dexLockedStore.SetBalance(coin) + } +} + +// ValidateDEXCancelOrdersByDenomIsAllowed validates whether the cancellation of orders by denom is allowed. +func (k Keeper) ValidateDEXCancelOrdersByDenomIsAllowed(ctx sdk.Context, addr sdk.AccAddress, denom string) error { + def, err := k.GetDefinition(ctx, denom) + if err != nil { + return err + } + + if !def.HasAdminPrivileges(addr) { + return sdkerrors.Wrapf(cosmoserrors.ErrUnauthorized, "only admin is able to cancel orders by denom %s", denom) + } + if !def.IsFeatureEnabled(types.Feature_dex_order_cancellation) { + return sdkerrors.Wrapf( + cosmoserrors.ErrUnauthorized, + "order cancellation is not allowed by denom %s, feature %s is disabled", + denom, types.Feature_dex_order_cancellation, + ) + } + + return nil +} + +func (k Keeper) dexExpectedToReceiveChecks( + ctx sdk.Context, + addr sdk.AccAddress, + def *types.Definition, + coin sdk.Coin, +) error { + if coin.IsZero() || def == nil { + return nil + } + + if def.IsFeatureEnabled(types.Feature_whitelisting) && !def.HasAdminPrivileges(addr) { + if err := k.validateWhitelistedBalance(ctx, addr, coin); err != nil { + return err + } + } + + return nil +} + +func (k Keeper) dexChecksForDenom( + ctx sdk.Context, + acc sdk.AccAddress, + def *types.Definition, oppositeDenom string, +) error { + if def == nil { + return nil + } + + if err := k.dexChecksForDefinition(ctx, acc, *def); err != nil { + return err + } + + // settings specific validation + settings, err := k.getDEXSettingsOrNil(ctx, def.Denom) + if err != nil { + return err + } + + if settings != nil { + // validate whitelisted denoms + if len(settings.WhitelistedDenoms) == 0 { + return nil + } + if !lo.Contains(settings.WhitelistedDenoms, oppositeDenom) { + return sdkerrors.Wrapf( + cosmoserrors.ErrUnauthorized, + "locking coins for DEX is prohibited, denom %s not whitelisted for %s", + oppositeDenom, def.Denom, + ) + } + } + + return nil +} + +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, + "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(), + ) + } + + if def.IsFeatureEnabled(types.Feature_freezing) { + if k.isGloballyFrozen(ctx, def.Denom) && + // sill allow the admin to do the trade, to follow same logic as we have in the sending + !def.HasAdminPrivileges(acc) { + return sdkerrors.Wrapf( + cosmoserrors.ErrUnauthorized, + "usage of %s for DEX is blocked because the token is globally frozen", + def.Denom, + ) + } + } + + 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) { + frozenAmt := k.GetFrozenBalance(ctx, addr, coin.Denom).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, + denom string, + settings types.DEXSettings, +) error { + prevSettings, err := k.getDEXSettingsOrNil(ctx, denom) + if err != nil { + return err + } + if prevSettings == nil { + prevSettings = &types.DEXSettings{} + } + + newSettings := *prevSettings + // update not nil settings + if settings.WhitelistedDenoms != nil { + newSettings.WhitelistedDenoms = settings.WhitelistedDenoms + } + if settings.UnifiedRefAmount != nil { + newSettings.UnifiedRefAmount = settings.UnifiedRefAmount + } + + if err := types.ValidateDEXSettings(settings); err != nil { + return err + } + + def, err := k.getDefinitionOrNil(ctx, denom) + if err != nil { + return err + } + // the gov can update any DEX setting even if the features are disabled + if k.authority != sender.String() { //nolint:nestif // the ifs are for the error checks mostly + if def != nil { + if !def.IsAdmin(sender) { + return sdkerrors.Wrap(cosmoserrors.ErrUnauthorized, "only admin and gov can update DEX settings") + } + if err := types.ValidateDEXSettingsAccess(newSettings, *def); err != nil { + return err + } + } else { + return sdkerrors.Wrap(cosmoserrors.ErrUnauthorized, "only admin or gov can update DEX settings") + } + } + + k.SetDEXSettings(ctx, denom, newSettings) + + if err := ctx.EventManager().EmitTypedEvent(&types.EventDEXSettingsChanged{ + PreviousSettings: prevSettings, + NewSettings: newSettings, + }); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidState, "failed to emit EventDEXSettingsChanged event: %s", err) + } + + return nil +} + +func (k Keeper) validateCoinIsNotLockedByDEXAndBank( + ctx sdk.Context, + addr sdk.AccAddress, + balance, coin sdk.Coin, +) error { + dexLockedAmt := k.GetDEXLockedBalance(ctx, addr, coin.Denom).Amount + availableAmt := balance.Amount.Sub(dexLockedAmt) + if availableAmt.LT(coin.Amount) { + return sdkerrors.Wrapf(cosmoserrors.ErrInsufficientFunds, "%s is not available, available %s%s", + coin.String(), availableAmt.String(), coin.Denom) + } + + bankLockedAmt := k.bankKeeper.LockedCoins(ctx, addr).AmountOf(coin.Denom) + // validate that we don't use the coins locked by bank + availableAmt = availableAmt.Sub(bankLockedAmt) + if availableAmt.LT(coin.Amount) { + return sdkerrors.Wrapf(cosmoserrors.ErrInsufficientFunds, "%s is not available, available %s%s", + coin.String(), availableAmt.String(), coin.Denom) + } + + return nil +} + +// dexExpectedToReceiveBalancesStore get the store for the DEX expected to receive balances of all accounts. +func (k Keeper) dexExpectedToReceiveBalancesStore(ctx sdk.Context) prefix.Store { + return prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXExpectedToReceiveBalancesKeyPrefix) +} + +// dexExpectedToReceiveAccountBalanceStore gets the store for the DEX expected to receive balances of an account. +func (k Keeper) dexExpectedToReceiveAccountBalanceStore(ctx sdk.Context, addr sdk.AccAddress) balanceStore { + return newBalanceStore(k.cdc, ctx.KVStore(k.storeKey), types.CreateDEXExpectedToReceiveBalancesKey(addr)) +} + +func (k Keeper) shouldRecordExpectedToReceiveBalance(ctx sdk.Context, denom string) (bool, error) { + def, err := k.getDefinitionOrNil(ctx, denom) + if err != nil { + return false, err + } + // increase for FT with the whitelisting enabled only + if def != nil && def.IsFeatureEnabled(types.Feature_whitelisting) { + return true, nil + } + + return false, nil +} + +// dexLockedBalancesStore get the store for the DEX locked balances of all accounts. +func (k Keeper) dexLockedBalancesStore(ctx sdk.Context) prefix.Store { + return prefix.NewStore(ctx.KVStore(k.storeKey), types.DEXLockedBalancesKeyPrefix) +} + +// dexLockedAccountBalanceStore gets the store for the DEX locked balances of an account. +func (k Keeper) dexLockedAccountBalanceStore(ctx sdk.Context, addr sdk.AccAddress) balanceStore { + return newBalanceStore(k.cdc, ctx.KVStore(k.storeKey), types.CreateDEXLockedBalancesKey(addr)) +} + +func (k Keeper) getDEXSettingsOrNil(ctx sdk.Context, denom string) (*types.DEXSettings, error) { + dexSettings, err := k.GetDEXSettings(ctx, denom) + if err != nil { + if errors.Is(err, types.ErrDEXSettingsNotFound) { + return nil, nil //nolint:nilnil //returns nil if data not found + } + return nil, err + } + + return &dexSettings, nil +} diff --git a/x/asset/ft/keeper/keeper_dex_test.go b/x/asset/ft/keeper/keeper_dex_test.go new file mode 100644 index 000000000..950a6353d --- /dev/null +++ b/x/asset/ft/keeper/keeper_dex_test.go @@ -0,0 +1,979 @@ +package keeper_test + +import ( + "fmt" + "math" + "strings" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + "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" +) + +func TestKeeper_DEXExpectedToReceive(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + 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{ + Issuer: issuer, + Symbol: "ABC", + Subunit: "abc", + Precision: 1, + Description: "ABC Desc", + InitialAmount: sdkmath.NewInt(666), + Features: []types.Feature{}, + } + + unwhitelistableDenom, err := ftKeeper.Issue(ctx, unwhitelistableSettings) + requireT.NoError(err) + _, err = ftKeeper.GetToken(ctx, unwhitelistableDenom) + requireT.NoError(err) + + // function passed but nothing is reserved + requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( + ctx, recipient, sdk.NewCoin(unwhitelistableDenom, sdkmath.NewInt(1)), + )) + requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, unwhitelistableDenom).IsZero()) + + // increase for not asset FT denom, passes but nothing is reserved + notFTDenom := types.BuildDenom("nonexist", issuer) + requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( + ctx, recipient, sdk.NewCoin(notFTDenom, sdkmath.NewInt(10)), + )) + requireT.True( + ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, "nonexist").IsZero(), + ) + + // set whitelisted balance + coinToSend := sdk.NewCoin(denom, sdkmath.NewInt(100)) + // whitelist sender and fund + 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 + requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, coinToSend)) + requireT.NoError(bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend))) + // return coin + requireT.NoError(bankKeeper.SendCoins(ctx, recipient, sender, sdk.NewCoins(coinToSend))) + // increase expected to received balance + coinToIncreaseExpectedToReceive := sdk.NewCoin(denom, sdkmath.NewInt(1)) + requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) + requireT.Equal( + coinToIncreaseExpectedToReceive.String(), + ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).String(), + ) + // try to send with the increased part + requireT.ErrorIs( + bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend)), + types.ErrWhitelistedLimitExceeded, + ) + + // try to decrease more that the balance + requireT.ErrorIs( + cosmoserrors.ErrInsufficientFunds, + ftKeeper.DEXDecreaseExpectedToReceive( + ctx, recipient, coinToIncreaseExpectedToReceive.Add(coinToIncreaseExpectedToReceive), + ), + ) + + requireT.NoError(ftKeeper.DEXDecreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) + requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).IsZero()) + // send without decreased amount + requireT.NoError(bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend))) +} + +func TestKeeper_DEXLocked(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()) + + settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{types.Feature_freezing}, + } + denom, err := ftKeeper.Issue(ctx, settings) + requireT.NoError(err) + + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + // create acc with permanently vesting locked coins + vestingCoin := sdk.NewInt64Coin(denom, 50) + baseVestingAccount, err := vestingtypes.NewDelayedVestingAccount( + authtypes.NewBaseAccountWithAddress(acc), + sdk.NewCoins(vestingCoin), + math.MaxInt64, + ) + requireT.NoError(err) + account := testApp.App.AccountKeeper.NewAccount(ctx, baseVestingAccount) + testApp.AccountKeeper.SetAccount(ctx, account) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(vestingCoin))) + // check vesting locked amount + requireT.Equal(vestingCoin.Amount.String(), bankKeeper.LockedCoins(ctx, acc).AmountOf(denom).String()) + + coinToSend := sdk.NewInt64Coin(denom, 1000) + // try to DEX lock more than balance + requireT.ErrorIs(ftKeeper.DEXIncreaseLocked(ctx, acc, coinToSend), types.ErrDEXInsufficientSpendableBalance) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(coinToSend))) + + // try to send full balance with the vesting locked coins + requireT.ErrorIs( + bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend.Add(vestingCoin))), + cosmoserrors.ErrInsufficientFunds, + ) + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinToSend.Add(vestingCoin), + sdk.NewInt64Coin(denom1, 0), + ), + types.ErrDEXInsufficientSpendableBalance, + ) + // send max allowed amount + requireT.NoError(bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend))) + + // lock full allowed amount (but without the amount locked by vesting) + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinToSend)) + // try to send at least one coin + requireT.ErrorIs( + bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom, 1))), + cosmoserrors.ErrInsufficientFunds, + ) + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + sdk.NewInt64Coin(denom, 1), + sdk.NewInt64Coin(denom1, 0), + ), + types.ErrDEXInsufficientSpendableBalance, + ) + // DEX decrease locked full balance + requireT.NoError(ftKeeper.DEXDecreaseLocked(ctx, acc, coinToSend)) + // DEX lock one more time + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinToSend)) + + balance := bankKeeper.GetBalance(ctx, acc, denom) + requireT.Equal(coinToSend.Add(vestingCoin).String(), balance.String()) + + // try to DEX lock coins which are locked by the vesting + requireT.ErrorIs(ftKeeper.DEXIncreaseLocked(ctx, acc, vestingCoin), types.ErrDEXInsufficientSpendableBalance) + + // try lock decrease locked full balance + requireT.ErrorIs(ftKeeper.DEXDecreaseLocked(ctx, acc, balance), cosmoserrors.ErrInsufficientFunds) + requireT.ErrorIs( + ftKeeper.DEXDecreaseLocked(ctx, acc, balance), + cosmoserrors.ErrInsufficientFunds, + ) + + // decrease locked part + requireT.NoError(ftKeeper.DEXDecreaseLocked(ctx, acc, sdk.NewInt64Coin(denom, 400))) + requireT.Equal(sdk.NewInt64Coin(denom, 600).String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom).String()) + requireT.Equal(sdk.NewInt64Coin(denom, 400).String(), ftKeeper.GetSpendableBalance(ctx, acc, denom).String()) + + // freeze locked balance + requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, coinToSend)) + // 1050 - total, 600 locked by dex, 50 locked by bank, 1000 frozen + requireT.Equal(sdk.NewInt64Coin(denom, 50).String(), ftKeeper.GetSpendableBalance(ctx, acc, denom).String()) + + // decrease locked 2d part, even when it's frozen we allow it + requireT.NoError(ftKeeper.DEXDecreaseLocked(ctx, acc, sdk.NewInt64Coin(denom, 600))) + requireT.Equal(sdkmath.ZeroInt().String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom).Amount.String()) + + // check order amounts are spendable with frozen coins + requireT.ErrorIs( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + coinToSend, + sdk.NewInt64Coin(denom1, 0), + ), + types.ErrDEXInsufficientSpendableBalance, + ) + + // unfreeze part + requireT.NoError(ftKeeper.Unfreeze(ctx, issuer, acc, sdk.NewInt64Coin(denom, 300))) + requireT.Equal(sdk.NewInt64Coin(denom, 700).String(), ftKeeper.GetFrozenBalance(ctx, acc, denom).String()) + + // now 700 frozen, 50 locked by vesting, 1050 balance + // try to use more than allowed + err = ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + sdk.NewInt64Coin(denom, 351), + sdk.NewInt64Coin(denom1, 0), + ) + requireT.ErrorIs(err, types.ErrDEXInsufficientSpendableBalance) + requireT.ErrorContains(err, "available 350") + + // try to send more than allowed + err = bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom, 351))) + requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) + requireT.ErrorContains(err, "available 350") + + // try to use with global freezing + requireT.NoError(ftKeeper.GloballyFreeze(ctx, issuer, denom)) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + sdk.NewInt64Coin(denom, 350), + sdk.NewInt64Coin(denom1, 0), + ), + fmt.Sprintf("usage of %s for DEX is blocked because the token is globally frozen", denom), + ) + requireT.True(ftKeeper.GetSpendableBalance(ctx, acc, denom).IsZero()) + // globally unfreeze now and check that we can use the previously locked amount + requireT.NoError(ftKeeper.GloballyUnfreeze(ctx, issuer, denom)) + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + sdk.NewInt64Coin(denom, 350), + sdk.NewInt64Coin(denom1, 0), + ), + ) + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, sdk.NewInt64Coin(denom, 350))) + // freeze more than balance + requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, sdk.NewInt64Coin(denom, 1_000_000))) + + // 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, + }, + } + denomWithExtension, 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", + ) +} + +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.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: 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) + testApp.MintAndSendCoin(t, ctxFromSmartContract, acc, sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctxFromSmartContract, + types.DEXOrder{Creator: acc}, + blockSmartContractCoin, + sdk.NewInt64Coin(denom1, 1), + ), + blockingErr, + ) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctxFromSmartContract, + types.DEXOrder{Creator: 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.DEXCheckOrderAmounts( + ctxFromSmartContract, + types.DEXOrder{Creator: issuer}, + blockSmartContractCoin, + sdk.NewInt64Coin(denom1, 1), + ), + ) + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctxFromSmartContract, + types.DEXOrder{Creator: issuer}, + sdk.NewInt64Coin(denom1, 1), + blockSmartContractCoin, + ), + ) +} + +func TestKeeper_DEXSettings_BlockDEX(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 + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + ft1Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_freezing, + types.Feature_dex_block, + }, + } + + invalidFT1Settings := ft1Settings + invalidFT1Settings.DEXSettings = &types.DEXSettings{ + WhitelistedDenoms: []string{denom1}, + } + trialCtx := simapp.CopyContextWithMultiStore(ctx) + _, err := ftKeeper.Issue(trialCtx, invalidFT1Settings) + requireT.ErrorIs(err, types.ErrFeatureDisabled) + + ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) + requireT.NoError(err) + + 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), + sdk.NewInt64Coin(denom1, 0), + ), errStr) + requireT.ErrorContains(ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + sdk.NewInt64Coin(denom1, 0), + sdk.NewInt64Coin(ft1Denom, 50), + ), errStr) +} + +func TestKeeper_DEXSettings_WhitelistedDenom(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()) + + ft1Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF", + Subunit: "def", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_dex_whitelisted_denoms, + }, + DEXSettings: &types.DEXSettings{ + WhitelistedDenoms: []string{ + denom1, + }, + }, + } + ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) + requireT.NoError(err) + + ft2Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEF2", + Subunit: "def2", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_dex_whitelisted_denoms, + }, + DEXSettings: &types.DEXSettings{ + WhitelistedDenoms: []string{ + ft1Denom, + }, + }, + } + ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) + requireT.NoError(err) + + ft1CoinToLock := sdk.NewInt64Coin(ft1Denom, 10) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft1CoinToLock))) + errStr := fmt.Sprintf("denom %s not whitelisted for %s", denom2, ft1Denom) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + ft1CoinToLock, + sdk.NewInt64Coin(denom2, 1), + ), + errStr, + ) + + requireT.NoError(ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + ft1CoinToLock, + sdk.NewInt64Coin(denom1, 1), + )) + + denom2CoinToLock := sdk.NewInt64Coin(denom2, 10) + testApp.MintAndSendCoin(t, ctx, acc, sdk.NewCoins(denom2CoinToLock)) + // can't lock the receive denom + errStr = fmt.Sprintf("denom %s not whitelisted for %s", denom2, ft1Denom) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + denom2CoinToLock, + sdk.NewInt64Coin(ft1Denom, 1), + ), + errStr, + ) + + // both not ft + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + denom2CoinToLock, + sdk.NewInt64Coin(denom1, 1), + ), + ) + + // try to lock both not ft coins + ft2CoinToLock := sdk.NewInt64Coin(ft2Denom, 10) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft2CoinToLock))) + errStr = fmt.Sprintf("denom %s not whitelisted for %s", ft2Denom, ft1Denom) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + ft2CoinToLock, + sdk.NewInt64Coin(ft1Denom, 1), + ), + errStr, + ) + requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, []string{ft2Denom})) + // now we can lock + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + ft2CoinToLock, + sdk.NewInt64Coin(ft1Denom, 1), + ), + ) + // + // lock not ft denoms without settings + denom3CoinToLock := sdk.NewInt64Coin(denom3, 10) + testApp.MintAndSendCoin(t, ctx, acc, sdk.NewCoins(denom3CoinToLock)) + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + denom3CoinToLock, + sdk.NewInt64Coin(denom4, 1), + ), + ) +} + +func TestKeeper_DEXLimitsWithGlobalFreeze(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContext(false) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + ft1Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEFONE", + Subunit: "defone", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_freezing, + }, + } + ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) + requireT.NoError(err) + + ft2Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "DEFTOW", + Subunit: "deftwo", + Precision: 6, + InitialAmount: sdkmath.NewIntWithDecimal(1, 10), + Features: []types.Feature{ + types.Feature_freezing, + }, + } + ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) + requireT.NoError(err) + + // fund acc + ft1CoinToSend := sdk.NewInt64Coin(ft1Denom, 100) + ft2CoinToSend := sdk.NewInt64Coin(ft2Denom, 100) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft1CoinToSend))) + requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft2CoinToSend))) + + // check that it's allowed to increase and decrease the limits + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: acc}, + ft1CoinToSend, + ft2CoinToSend, + ), + ) + + // globally freeze + ftKeeper.SetGlobalFreeze(ctx, ft1CoinToSend.Denom, true) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + ft1CoinToSend, + ft2CoinToSend, + ), + fmt.Sprintf("usage of %s for DEX is blocked because the token is globally frozen", ft1CoinToSend.Denom), + ) + + ftKeeper.SetGlobalFreeze(ctx, ft1CoinToSend.Denom, false) + ftKeeper.SetGlobalFreeze(ctx, ft2CoinToSend.Denom, true) + requireT.ErrorContains( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: acc}, + ft1CoinToSend, + ft2CoinToSend, + ), + fmt.Sprintf("usage of %s for DEX is blocked because the token is globally frozen", ft2CoinToSend.Denom), + ) + + // admin still can increase the limits + requireT.NoError( + ftKeeper.DEXCheckOrderAmounts( + simapp.CopyContextWithMultiStore(ctx), + types.DEXOrder{Creator: issuer}, + ft1CoinToSend, + ft2CoinToSend, + ), + ) +} + +func TestKeeper_LockedNotFT(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContext(false) + + ftKeeper := testApp.AssetFTKeeper + bankKeeper := testApp.BankKeeper + + faucet := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + requireT.NoError(testApp.FundAccount(ctx, faucet, sdk.NewCoins(sdk.NewCoin(denom1, sdkmath.NewIntWithDecimal(1, 10))))) + + // create acc with permanently locked coins (native) + vestingCoin := sdk.NewInt64Coin(denom1, 50) + baseVestingAccount, err := vestingtypes.NewDelayedVestingAccount( + authtypes.NewBaseAccountWithAddress(acc), + sdk.NewCoins(vestingCoin), + math.MaxInt64, + ) + requireT.NoError(err) + account := testApp.App.AccountKeeper.NewAccount(ctx, baseVestingAccount) + testApp.AccountKeeper.SetAccount(ctx, account) + requireT.NoError(bankKeeper.SendCoins(ctx, faucet, acc, sdk.NewCoins(vestingCoin))) + // check bank locked amount + requireT.Equal(vestingCoin.Amount.String(), bankKeeper.LockedCoins(ctx, acc).AmountOf(denom1).String()) + + coinToSend := sdk.NewInt64Coin(denom1, 1000) + // try to lock more than balance + requireT.ErrorIs(ftKeeper.DEXIncreaseLocked(ctx, acc, coinToSend), types.ErrDEXInsufficientSpendableBalance) + requireT.NoError(bankKeeper.SendCoins(ctx, faucet, acc, sdk.NewCoins(coinToSend))) + + // try to send full balance with the vesting locked coins + requireT.ErrorIs( + bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend.Add(vestingCoin))), + cosmoserrors.ErrInsufficientFunds, + ) + + // lock full allowed amount (but without the amount locked by vesting) + requireT.NoError(ftKeeper.DEXIncreaseLocked(ctx, acc, coinToSend)) + + // try to send at least one coin + requireT.ErrorIs( + bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))), + cosmoserrors.ErrInsufficientFunds, + ) + + balance := bankKeeper.GetBalance(ctx, acc, denom1) + requireT.Equal(coinToSend.Add(vestingCoin).String(), balance.String()) + + // try lock coins which are locked by the vesting + requireT.ErrorIs(ftKeeper.DEXIncreaseLocked(ctx, acc, vestingCoin), types.ErrDEXInsufficientSpendableBalance) + + // try decrease locked full balance + requireT.ErrorIs(ftKeeper.DEXDecreaseLocked(ctx, acc, balance), cosmoserrors.ErrInsufficientFunds) + + // decrease locked part + requireT.NoError(ftKeeper.DEXDecreaseLocked(ctx, acc, sdk.NewInt64Coin(denom1, 400))) + requireT.Equal(sdk.NewInt64Coin(denom1, 600).String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom1).String()) +} + +func TestKeeper_UpdateDEXUnifiedRefAmount(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) + + ftKeeper := testApp.AssetFTKeeper + + issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + ft1Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "ABC", + Subunit: "abc", + Precision: 8, + InitialAmount: sdkmath.NewInt(777), + DEXSettings: &types.DEXSettings{ + UnifiedRefAmount: lo.ToPtr(sdkmath.LegacyMustNewDecFromStr("0.01")), + }, + } + + // try to issue without the feature enabled, but with the settings + _, err := ftKeeper.Issue(simapp.CopyContextWithMultiStore(ctx), ft1Settings) + requireT.ErrorIs(err, types.ErrFeatureDisabled) + + ft1Settings.Features = []types.Feature{ + types.Feature_dex_unified_ref_amount_change, + } + + ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) + requireT.NoError(err) + + gotToken, err := ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken := types.Token{ + Denom: ft1Denom, + Issuer: ft1Settings.Issuer.String(), + Symbol: ft1Settings.Symbol, + Subunit: strings.ToLower(ft1Settings.Subunit), + Precision: ft1Settings.Precision, + BurnRate: sdkmath.LegacyNewDec(0), + SendCommissionRate: sdkmath.LegacyNewDec(0), + Version: types.CurrentTokenVersion, + Admin: ft1Settings.Issuer.String(), + Features: ft1Settings.Features, + DEXSettings: ft1Settings.DEXSettings, + } + requireT.Equal(expectToken, gotToken) + + // try to update with the invalid settings + unifiedRefAmount := sdkmath.LegacyMustNewDecFromStr("-0.01") + requireT.ErrorIs( + ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, ft1Denom, unifiedRefAmount), types.ErrInvalidInput, + ) + + // try to update from not issuer + unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("0.01") + randomAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + requireT.ErrorIs(ftKeeper.UpdateDEXUnifiedRefAmount( + ctx, randomAddr, ft1Denom, unifiedRefAmount), cosmoserrors.ErrUnauthorized, + ) + + // update the settings + requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, ft1Denom, unifiedRefAmount)) + + gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken.DEXSettings = &types.DEXSettings{ + UnifiedRefAmount: &unifiedRefAmount, + } + requireT.Equal(expectToken, gotToken) + + // update the settings one more time but with the gov acc + unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("999") + requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, unifiedRefAmount), + ) + + gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken.DEXSettings = &types.DEXSettings{ + UnifiedRefAmount: &unifiedRefAmount, + } + requireT.Equal(expectToken, gotToken) + + // update the different setting to check that we don't affect other + whitelistedDenoms := []string{denom1} + requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, whitelistedDenoms, + )) + unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("777") + requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, unifiedRefAmount), + ) + gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken.DEXSettings = &types.DEXSettings{ + UnifiedRefAmount: &unifiedRefAmount, + WhitelistedDenoms: whitelistedDenoms, + } + requireT.Equal(expectToken, gotToken) + + // try to update settings for the not FT denom from not gov + requireT.ErrorIs( + ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, denom1, unifiedRefAmount), cosmoserrors.ErrUnauthorized, + ) + requireT.NoError( + ftKeeper.UpdateDEXUnifiedRefAmount( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), denom1, unifiedRefAmount, + ), + ) + + dexSettings, err := ftKeeper.GetDEXSettings(ctx, denom1) + requireT.NoError(err) + + requireT.Equal(types.DEXSettings{ + UnifiedRefAmount: &unifiedRefAmount, + }, dexSettings) +} + +func TestKeeper_UpdateDEXWhitelistedDenoms(t *testing.T) { + requireT := require.New(t) + + testApp := simapp.New() + ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) + + ftKeeper := testApp.AssetFTKeeper + + issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + ft1Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "ABC", + Subunit: "abc", + Precision: 8, + InitialAmount: sdkmath.NewInt(777), + Features: []types.Feature{ + types.Feature_dex_whitelisted_denoms, + }, + } + + ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) + requireT.NoError(err) + + gotToken, err := ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken := types.Token{ + Denom: ft1Denom, + Issuer: ft1Settings.Issuer.String(), + Symbol: ft1Settings.Symbol, + Subunit: strings.ToLower(ft1Settings.Subunit), + Precision: ft1Settings.Precision, + BurnRate: sdkmath.LegacyNewDec(0), + SendCommissionRate: sdkmath.LegacyNewDec(0), + Version: types.CurrentTokenVersion, + Admin: ft1Settings.Issuer.String(), + DEXSettings: ft1Settings.DEXSettings, + Features: []types.Feature{ + types.Feature_dex_whitelisted_denoms, + }, + } + requireT.Equal(expectToken, gotToken) + + // try to update with the invalid whitelisted denoms + whitelistedDenoms := []string{"1denom1"} + requireT.ErrorIs(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms), types.ErrInvalidInput) + + // try to update from not issuer + whitelistedDenoms = []string{denom1} + randomAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + requireT.ErrorIs( + ftKeeper.UpdateDEXWhitelistedDenoms(ctx, randomAddr, ft1Denom, whitelistedDenoms), cosmoserrors.ErrUnauthorized, + ) + + requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms)) + + gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken.DEXSettings = &types.DEXSettings{ + WhitelistedDenoms: whitelistedDenoms, + } + requireT.Equal(expectToken, gotToken) + + // update the to empty list to allow all denoms + whitelistedDenoms = make([]string, 0) + requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms)) + + gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) + requireT.NoError(err) + expectToken.DEXSettings = &types.DEXSettings{ + WhitelistedDenoms: nil, + } + requireT.Equal(expectToken, gotToken) + + whitelistedDenoms = []string{denom1} + + // try to update settings for the not FT denom from not gov + requireT.ErrorIs( + ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, denom1, whitelistedDenoms), cosmoserrors.ErrUnauthorized, + ) + // update from gov + requireT.NoError( + ftKeeper.UpdateDEXWhitelistedDenoms( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), denom1, whitelistedDenoms, + ), + ) + + dexSettings, err := ftKeeper.GetDEXSettings(ctx, denom1) + requireT.NoError(err) + + requireT.Equal(types.DEXSettings{ + WhitelistedDenoms: whitelistedDenoms, + }, dexSettings) + + ft2Settings := types.IssueSettings{ + Issuer: issuer, + Symbol: "ABC2", + Subunit: "abc2", + Precision: 8, + InitialAmount: sdkmath.NewInt(777), + // no features + } + + ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) + requireT.NoError(err) + + whitelistedDenoms = []string{denom2} + + // try to update settings from issuer + requireT.ErrorIs( + ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft2Denom, whitelistedDenoms), types.ErrFeatureDisabled, + ) + // update from gov + requireT.NoError( + ftKeeper.UpdateDEXWhitelistedDenoms( + ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft2Denom, whitelistedDenoms, + ), + ) + + dexSettings, err = ftKeeper.GetDEXSettings(ctx, ft2Denom) + requireT.NoError(err) + + requireT.Equal(types.DEXSettings{ + WhitelistedDenoms: whitelistedDenoms, + }, dexSettings) +} diff --git a/x/asset/ft/keeper/keeper_test.go b/x/asset/ft/keeper/keeper_test.go index 445b76a6c..35f11a53f 100644 --- a/x/asset/ft/keeper/keeper_test.go +++ b/x/asset/ft/keeper/keeper_test.go @@ -2,25 +2,19 @@ package keeper_test import ( "fmt" - "math" "slices" "strings" "testing" - "time" sdkerrors "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/query" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/google/uuid" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -29,9 +23,7 @@ import ( "github.com/CoreumFoundation/coreum/v5/pkg/config/constant" "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" "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" ) @@ -656,23 +648,19 @@ func TestKeeper_Burn(t *testing.T) { requireT.NoError(err) // DEX lock coins and try to burn - err = ftKeeper.DEXLock(ctx, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100))) - requireT.NoError(err) - err = ftKeeper.DEXDecreaseLimitsAndSend( - // denom1 with whitelisting disabled - ctx, recipient, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100)), sdk.NewCoin(denom1, sdkmath.NewInt(123)), - ) - requireT.NoError(err) - err = ftKeeper.DEXLock(ctx, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100))) + err = ftKeeper.DEXIncreaseLocked(ctx, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100))) requireT.NoError(err) err = ftKeeper.Burn(ctx, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100))) requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) - err = ftKeeper.DEXCheckLimitsAndSend( + err = ftKeeper.DEXCheckOrderAmounts( + ctx, + types.DEXOrder{Creator: recipient}, + sdk.NewCoin(burnableDenom, sdkmath.NewInt(100)), // denom1 with whitelisting disabled - ctx, recipient, recipient, sdk.NewCoin(burnableDenom, sdkmath.NewInt(100)), sdk.NewCoin(denom1, sdkmath.NewInt(123)), + sdk.NewCoin(denom1, sdkmath.NewInt(123)), ) - requireT.ErrorIs(err, types.ErrDEXLockFailed) + requireT.ErrorIs(err, types.ErrDEXInsufficientSpendableBalance) } func TestKeeper_BurnRate_BankSend(t *testing.T) { @@ -1436,11 +1424,11 @@ func TestKeeper_Clawback(t *testing.T) { requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) // try to clawback locked balance - err = ftKeeper.DEXLock(ctx, from, sdk.NewCoin(denom, sdkmath.NewInt(100))) + err = ftKeeper.DEXIncreaseLocked(ctx, from, sdk.NewCoin(denom, sdkmath.NewInt(100))) requireT.NoError(err) err = ftKeeper.Clawback(ctx, issuer, from, sdk.NewCoin(denom, sdkmath.NewInt(40))) requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) - err = ftKeeper.DEXUnlock(ctx, from, sdk.NewCoin(denom, sdkmath.NewInt(100))) + err = ftKeeper.DEXDecreaseLocked(ctx, from, sdk.NewCoin(denom, sdkmath.NewInt(100))) requireT.NoError(err) // clawback, query balance @@ -1583,108 +1571,6 @@ func TestKeeper_Whitelist(t *testing.T) { requireT.NoError(err) } -func TestKeeper_ExpectedToReceive(t *testing.T) { - requireT := require.New(t) - - testApp := simapp.New() - ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) - - ftKeeper := testApp.AssetFTKeeper - bankKeeper := testApp.BankKeeper - - issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - 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{ - Issuer: issuer, - Symbol: "ABC", - Subunit: "abc", - Precision: 1, - Description: "ABC Desc", - InitialAmount: sdkmath.NewInt(666), - Features: []types.Feature{}, - } - - unwhitelistableDenom, err := ftKeeper.Issue(ctx, unwhitelistableSettings) - requireT.NoError(err) - _, err = ftKeeper.GetToken(ctx, unwhitelistableDenom) - requireT.NoError(err) - - // function passed but nothing is reserved - requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( - ctx, recipient, sdk.NewCoin(unwhitelistableDenom, sdkmath.NewInt(1)), - )) - requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, unwhitelistableDenom).IsZero()) - - // increase for not asset FT denom, passes but nothing is reserved - notFTDenom := types.BuildDenom("nonexist", issuer) - requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( - ctx, recipient, sdk.NewCoin(notFTDenom, sdkmath.NewInt(10)), - )) - requireT.True( - ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, "nonexist").IsZero(), - ) - - // increase and decrease the whitelisting for the admin - requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive( - ctx, issuer, sdk.NewCoin(denom, sdkmath.NewInt(10_000)), - )) - requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, issuer, denom).IsZero()) - requireT.NoError(ftKeeper.DEXDecreaseExpectedToReceive( - ctx, issuer, sdk.NewCoin(denom, sdkmath.NewInt(10_000)), - )) - - // set whitelisted balance - coinToSend := sdk.NewCoin(denom, sdkmath.NewInt(100)) - // whitelist sender and fund - 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 - requireT.NoError(ftKeeper.SetWhitelistedBalance(ctx, issuer, recipient, coinToSend)) - requireT.NoError(bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend))) - // return coin - requireT.NoError(bankKeeper.SendCoins(ctx, recipient, sender, sdk.NewCoins(coinToSend))) - // increase expected to received balance - coinToIncreaseExpectedToReceive := sdk.NewCoin(denom, sdkmath.NewInt(1)) - requireT.NoError(ftKeeper.DEXIncreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) - requireT.Equal( - coinToIncreaseExpectedToReceive.String(), - ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).String(), - ) - // try to send with the increased part - requireT.ErrorIs( - bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend)), - types.ErrWhitelistedLimitExceeded, - ) - - // try to decrease more that the balance - requireT.ErrorIs( - cosmoserrors.ErrInsufficientFunds, - ftKeeper.DEXDecreaseExpectedToReceive( - ctx, recipient, coinToIncreaseExpectedToReceive.Add(coinToIncreaseExpectedToReceive), - ), - ) - - requireT.NoError(ftKeeper.DEXDecreaseExpectedToReceive(ctx, recipient, coinToIncreaseExpectedToReceive)) - requireT.True(ftKeeper.GetDEXExpectedToReceivedBalance(ctx, recipient, denom).IsZero()) - // send without decreased amount - requireT.NoError(bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(coinToSend))) -} - func TestKeeper_FreezeWhitelistMultiSend(t *testing.T) { requireT := require.New(t) @@ -1799,492 +1685,6 @@ func TestKeeper_FreezeWhitelistMultiSend(t *testing.T) { requireT.ErrorIs(err, types.ErrWhitelistedLimitExceeded) } -func TestKeeper_DEXLockAndUnlock(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()) - - settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEF", - Subunit: "def", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{types.Feature_freezing}, - } - denom, err := ftKeeper.Issue(ctx, settings) - requireT.NoError(err) - - acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - // create acc with permanently vesting locked coins - vestingCoin := sdk.NewInt64Coin(denom, 50) - baseVestingAccount, err := vestingtypes.NewDelayedVestingAccount( - authtypes.NewBaseAccountWithAddress(acc), - sdk.NewCoins(vestingCoin), - math.MaxInt64, - ) - requireT.NoError(err) - account := testApp.App.AccountKeeper.NewAccount(ctx, baseVestingAccount) - testApp.AccountKeeper.SetAccount(ctx, account) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(vestingCoin))) - // check vesting locked amount - requireT.Equal(vestingCoin.Amount.String(), bankKeeper.LockedCoins(ctx, acc).AmountOf(denom).String()) - - coinToSend := sdk.NewInt64Coin(denom, 1000) - // try to DEX lock more than balance - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, coinToSend), types.ErrDEXLockFailed) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(coinToSend))) - - // try to send full balance with the vesting locked coins - requireT.ErrorIs( - bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend.Add(vestingCoin))), - cosmoserrors.ErrInsufficientFunds, - ) - requireT.ErrorIs( - ftKeeper.DEXCheckLimitsAndSend(ctx, acc, acc, coinToSend.Add(vestingCoin), sdk.NewInt64Coin(denom1, 1)), - types.ErrDEXLockFailed, - ) - // send max allowed amount - requireT.NoError(bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend))) - - // lock full allowed amount (but without the amount locked by vesting) - requireT.NoError(ftKeeper.DEXLock(ctx, acc, coinToSend)) - // try to send at least one coin - requireT.ErrorIs( - bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom, 1))), - cosmoserrors.ErrInsufficientFunds, - ) - requireT.ErrorIs( - ftKeeper.DEXCheckLimitsAndSend(ctx, acc, acc, sdk.NewInt64Coin(denom, 1), sdk.NewInt64Coin(denom1, 1)), - types.ErrDEXLockFailed, - ) - // DEX unlock full balance - requireT.NoError(ftKeeper.DEXUnlock(ctx, acc, coinToSend)) - // DEX lock one more time - requireT.NoError(ftKeeper.DEXLock(ctx, acc, coinToSend)) - - balance := bankKeeper.GetBalance(ctx, acc, denom) - requireT.Equal(coinToSend.Add(vestingCoin).String(), balance.String()) - - // try to DEX lock coins which are locked by the vesting - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, vestingCoin), types.ErrDEXLockFailed) - - // try lock unlock full balance - requireT.ErrorIs(ftKeeper.DEXUnlock(ctx, acc, balance), cosmoserrors.ErrInsufficientFunds) - requireT.ErrorIs( - ftKeeper.DEXDecreaseLimitsAndSend(ctx, acc, acc, balance, sdk.NewInt64Coin(denom1, 1)), - cosmoserrors.ErrInsufficientFunds, - ) - - // unlock part - requireT.NoError(ftKeeper.DEXUnlock(ctx, acc, sdk.NewInt64Coin(denom, 400))) - requireT.Equal(sdk.NewInt64Coin(denom, 600).String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom).String()) - requireT.Equal(sdk.NewInt64Coin(denom, 400).String(), ftKeeper.GetSpendableBalance(ctx, acc, denom).String()) - - // freeze locked balance - requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, coinToSend)) - // 1050 - total, 600 locked by dex, 50 locked by bank, 1000 frozen - requireT.Equal(sdk.NewInt64Coin(denom, 50).String(), ftKeeper.GetSpendableBalance(ctx, acc, denom).String()) - - // unlock 2d part, even when it's frozen we allow it - requireT.NoError(ftKeeper.DEXUnlock(ctx, acc, sdk.NewInt64Coin(denom, 600))) - requireT.Equal(sdkmath.ZeroInt().String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom).Amount.String()) - - // try to lock now with the frozen coins - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, coinToSend), types.ErrDEXLockFailed) - - // unfreeze part - requireT.NoError(ftKeeper.Unfreeze(ctx, issuer, acc, sdk.NewInt64Coin(denom, 300))) - requireT.Equal(sdk.NewInt64Coin(denom, 700).String(), ftKeeper.GetFrozenBalance(ctx, acc, denom).String()) - - // now 700 frozen, 50 locked by vesting, 1050 balance - // try to lock more than allowed - err = ftKeeper.DEXLock(ctx, acc, sdk.NewInt64Coin(denom, 351)) - requireT.ErrorIs(err, types.ErrDEXLockFailed) - requireT.ErrorContains(err, "available 350") - - // try to send more than allowed - err = bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom, 351))) - requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds) - requireT.ErrorContains(err, "available 350") - - // try to lock with global freezing - requireT.NoError(ftKeeper.GloballyFreeze(ctx, issuer, denom)) - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, sdk.NewInt64Coin(denom, 350)), types.ErrDEXLockFailed) - requireT.True(ftKeeper.GetSpendableBalance(ctx, acc, denom).IsZero()) - // globally unfreeze now and check that we can lock max allowed - requireT.NoError(ftKeeper.GloballyUnfreeze(ctx, issuer, denom)) - requireT.NoError( - ftKeeper.DEXCheckLimitsAndSend(ctx, acc, acc, sdk.NewInt64Coin(denom, 350), sdk.NewInt64Coin(denom1, 350)), - ) - requireT.NoError(ftKeeper.DEXLock(ctx, acc, sdk.NewInt64Coin(denom, 350))) - // freeze more than balance - requireT.NoError(ftKeeper.Freeze(ctx, issuer, acc, sdk.NewInt64Coin(denom, 1_000_000))) - - // 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, - }, - } - denomWithExtension, 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.DEXIncreaseLimits(ctx, acc, extensionCoin, sdk.NewInt64Coin(denom1, 1)), - "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 DEXCheckLimitsAndSend - requireT.ErrorContains( - ftKeeper.DEXCheckLimitsAndSend(ctxFromSmartContract, acc, acc, blockSmartContractCoin, sdk.NewInt64Coin(denom1, 1)), - blockingErr, - ) - requireT.ErrorContains( - ftKeeper.DEXCheckLimitsAndSend(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.DEXCheckLimitsAndSend( - ctxFromSmartContract, issuer, issuer, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin, - ), - ) - requireT.NoError( - ftKeeper.DEXIncreaseLimits(ctxFromSmartContract, issuer, sdk.NewInt64Coin(denom1, 1), blockSmartContractCoin), - ) -} - -func TestKeeper_DEXSettings_BlockDEX(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 - - issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - - ft1Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEF", - Subunit: "def", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_freezing, - types.Feature_dex_block, - }, - } - - invalidFT1Settings := ft1Settings - invalidFT1Settings.DEXSettings = &types.DEXSettings{ - WhitelistedDenoms: []string{denom1}, - } - trialCtx := simapp.CopyContextWithMultiStore(ctx) - _, err := ftKeeper.Issue(trialCtx, invalidFT1Settings) - requireT.ErrorIs(err, types.ErrFeatureDisabled) - - ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) - requireT.NoError(err) - - 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) - requireT.ErrorContains( - ftKeeper.DEXIncreaseLimits( - ctx, acc, sdk.NewInt64Coin(denom1, 50), sdk.NewInt64Coin(ft1Denom, 1), - ), errStr) - requireT.ErrorContains( - ftKeeper.DEXCheckLimitsAndSend( - ctx, acc, acc, sdk.NewInt64Coin(denom1, 50), sdk.NewInt64Coin(ft1Denom, 1), - ), errStr) -} - -func TestKeeper_DEXSettings_WhitelistedDenom(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()) - - ft1Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEF", - Subunit: "def", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_dex_whitelisted_denoms, - }, - DEXSettings: &types.DEXSettings{ - WhitelistedDenoms: []string{ - denom1, - }, - }, - } - ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) - requireT.NoError(err) - - ft2Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEF2", - Subunit: "def2", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_dex_whitelisted_denoms, - }, - DEXSettings: &types.DEXSettings{ - WhitelistedDenoms: []string{ - ft1Denom, - }, - }, - } - ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) - requireT.NoError(err) - - ft1CoinToLock := sdk.NewInt64Coin(ft1Denom, 10) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft1CoinToLock))) - errStr := fmt.Sprintf("denom %s not whitelisted for %s", denom2, ft1Denom) - requireT.ErrorContains(ftKeeper.DEXIncreaseLimits(ctx, acc, ft1CoinToLock, sdk.NewInt64Coin(denom2, 1)), errStr) - requireT.ErrorContains( - ftKeeper.DEXCheckLimitsAndSend(ctx, acc, acc, ft1CoinToLock, sdk.NewInt64Coin(denom2, 1)), errStr, - ) - requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, ft1CoinToLock, sdk.NewInt64Coin(denom1, 1))) - - denom2CoinToLock := sdk.NewInt64Coin(denom2, 10) - testApp.MintAndSendCoin(t, ctx, acc, sdk.NewCoins(denom2CoinToLock)) - // can't lock the receive denom - errStr = fmt.Sprintf("denom %s not whitelisted for %s", denom2, ft1Denom) - requireT.ErrorContains(ftKeeper.DEXIncreaseLimits(ctx, acc, denom2CoinToLock, sdk.NewInt64Coin(ft1Denom, 1)), errStr) - - // both not ft - requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, denom2CoinToLock, sdk.NewInt64Coin(denom1, 1))) - - // try to lock both not ft coins - ft2CoinToLock := sdk.NewInt64Coin(ft2Denom, 10) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft2CoinToLock))) - errStr = fmt.Sprintf("denom %s not whitelisted for %s", ft2Denom, ft1Denom) - requireT.ErrorContains(ftKeeper.DEXIncreaseLimits(ctx, acc, ft2CoinToLock, sdk.NewInt64Coin(ft1Denom, 1)), errStr) - requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, []string{ft2Denom})) - // now we can lock - requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, ft2CoinToLock, sdk.NewInt64Coin(ft1Denom, 1))) - // - // lock not ft denoms without settings - denom3CoinToLock := sdk.NewInt64Coin(denom3, 10) - testApp.MintAndSendCoin(t, ctx, acc, sdk.NewCoins(denom3CoinToLock)) - requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, denom3CoinToLock, sdk.NewInt64Coin(denom4, 1))) -} - -func TestKeeper_DEXLimitsWithGlobalFreeze(t *testing.T) { - requireT := require.New(t) - - testApp := simapp.New() - ctx := testApp.BaseApp.NewContext(false) - - ftKeeper := testApp.AssetFTKeeper - bankKeeper := testApp.BankKeeper - - issuer := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - - ft1Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEFONE", - Subunit: "defone", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_freezing, - }, - } - ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) - requireT.NoError(err) - - ft2Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "DEFTOW", - Subunit: "deftwo", - Precision: 6, - InitialAmount: sdkmath.NewIntWithDecimal(1, 10), - Features: []types.Feature{ - types.Feature_freezing, - }, - } - ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) - requireT.NoError(err) - - // fund acc - ft1CoinToSend := sdk.NewInt64Coin(ft1Denom, 100) - ft2CoinToSend := sdk.NewInt64Coin(ft2Denom, 100) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft1CoinToSend))) - requireT.NoError(bankKeeper.SendCoins(ctx, issuer, acc, sdk.NewCoins(ft2CoinToSend))) - - // check that it's allowed to increase and decrease the limits - requireT.NoError(ftKeeper.DEXIncreaseLimits(ctx, acc, ft1CoinToSend, ft2CoinToSend)) - requireT.NoError(ftKeeper.DEXDecreaseLimits(ctx, acc, ft1CoinToSend, ft2CoinToSend)) - - // globally freeze - ftKeeper.SetGlobalFreeze(ctx, ft1CoinToSend.Denom, true) - requireT.ErrorContains( - ftKeeper.DEXIncreaseLimits(simapp.CopyContextWithMultiStore(ctx), acc, ft1CoinToSend, ft2CoinToSend), - fmt.Sprintf("usage of %s for DEX is blocked because the token is globally frozen", ft1CoinToSend.Denom), - ) - - ftKeeper.SetGlobalFreeze(ctx, ft1CoinToSend.Denom, false) - ftKeeper.SetGlobalFreeze(ctx, ft2CoinToSend.Denom, true) - requireT.ErrorContains( - ftKeeper.DEXIncreaseLimits(simapp.CopyContextWithMultiStore(ctx), acc, ft1CoinToSend, ft2CoinToSend), - fmt.Sprintf("usage of %s for DEX is blocked because the token is globally frozen", ft2CoinToSend.Denom), - ) - - // admin still can increase the limits - requireT.NoError( - ftKeeper.DEXIncreaseLimits(simapp.CopyContextWithMultiStore(ctx), issuer, ft1CoinToSend, ft2CoinToSend), - ) -} - -func TestKeeper_LockAndUnlockNotFT(t *testing.T) { - requireT := require.New(t) - - testApp := simapp.New() - ctx := testApp.BaseApp.NewContext(false) - - ftKeeper := testApp.AssetFTKeeper - bankKeeper := testApp.BankKeeper - - faucet := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - acc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - requireT.NoError(testApp.FundAccount(ctx, faucet, sdk.NewCoins(sdk.NewCoin(denom1, sdkmath.NewIntWithDecimal(1, 10))))) - - // create acc with permanently locked coins (native) - vestingCoin := sdk.NewInt64Coin(denom1, 50) - baseVestingAccount, err := vestingtypes.NewDelayedVestingAccount( - authtypes.NewBaseAccountWithAddress(acc), - sdk.NewCoins(vestingCoin), - math.MaxInt64, - ) - requireT.NoError(err) - account := testApp.App.AccountKeeper.NewAccount(ctx, baseVestingAccount) - testApp.AccountKeeper.SetAccount(ctx, account) - requireT.NoError(bankKeeper.SendCoins(ctx, faucet, acc, sdk.NewCoins(vestingCoin))) - // check bank locked amount - requireT.Equal(vestingCoin.Amount.String(), bankKeeper.LockedCoins(ctx, acc).AmountOf(denom1).String()) - - coinToSend := sdk.NewInt64Coin(denom1, 1000) - // try to lock more than balance - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, coinToSend), types.ErrDEXLockFailed) - requireT.NoError(bankKeeper.SendCoins(ctx, faucet, acc, sdk.NewCoins(coinToSend))) - - // try to send full balance with the vesting locked coins - requireT.ErrorIs( - bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(coinToSend.Add(vestingCoin))), - cosmoserrors.ErrInsufficientFunds, - ) - - // lock full allowed amount (but without the amount locked by vesting) - requireT.NoError(ftKeeper.DEXLock(ctx, acc, coinToSend)) - - // try to send at least one coin - requireT.ErrorIs( - bankKeeper.SendCoins(ctx, acc, acc, sdk.NewCoins(sdk.NewInt64Coin(denom1, 1))), - cosmoserrors.ErrInsufficientFunds, - ) - - balance := bankKeeper.GetBalance(ctx, acc, denom1) - requireT.Equal(coinToSend.Add(vestingCoin).String(), balance.String()) - - // try lock coins which are locked by the vesting - requireT.ErrorIs(ftKeeper.DEXLock(ctx, acc, vestingCoin), types.ErrDEXLockFailed) - - // try lock unlock full balance - requireT.ErrorIs(ftKeeper.DEXUnlock(ctx, acc, balance), cosmoserrors.ErrInsufficientFunds) - - // unlock part - requireT.NoError(ftKeeper.DEXUnlock(ctx, acc, sdk.NewInt64Coin(denom1, 400))) - requireT.Equal(sdk.NewInt64Coin(denom1, 600).String(), ftKeeper.GetDEXLockedBalance(ctx, acc, denom1).String()) -} - func TestKeeper_IBC(t *testing.T) { requireT := require.New(t) @@ -3459,248 +2859,3 @@ func TestKeeper_ClearAdmin(t *testing.T) { err = bankKeeper.SendCoins(ctx, sender, recipient, sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(100)))) requireT.NoError(err) } - -func TestKeeper_UpdateDEXUnifiedRefAmount(t *testing.T) { - requireT := require.New(t) - - testApp := simapp.New() - ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) - - ftKeeper := testApp.AssetFTKeeper - - issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - ft1Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC", - Subunit: "abc", - Precision: 8, - InitialAmount: sdkmath.NewInt(777), - DEXSettings: &types.DEXSettings{ - UnifiedRefAmount: lo.ToPtr(sdkmath.LegacyMustNewDecFromStr("0.01")), - }, - } - - // try to issue without the feature enabled, but with the settings - _, err := ftKeeper.Issue(simapp.CopyContextWithMultiStore(ctx), ft1Settings) - requireT.ErrorIs(err, types.ErrFeatureDisabled) - - ft1Settings.Features = []types.Feature{ - types.Feature_dex_unified_ref_amount_change, - } - - ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) - requireT.NoError(err) - - gotToken, err := ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken := types.Token{ - Denom: ft1Denom, - Issuer: ft1Settings.Issuer.String(), - Symbol: ft1Settings.Symbol, - Subunit: strings.ToLower(ft1Settings.Subunit), - Precision: ft1Settings.Precision, - BurnRate: sdkmath.LegacyNewDec(0), - SendCommissionRate: sdkmath.LegacyNewDec(0), - Version: types.CurrentTokenVersion, - Admin: ft1Settings.Issuer.String(), - Features: ft1Settings.Features, - DEXSettings: ft1Settings.DEXSettings, - } - requireT.Equal(expectToken, gotToken) - - // try to update with the invalid settings - unifiedRefAmount := sdkmath.LegacyMustNewDecFromStr("-0.01") - requireT.ErrorIs( - ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, ft1Denom, unifiedRefAmount), types.ErrInvalidInput, - ) - - // try to update from not issuer - unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("0.01") - randomAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - requireT.ErrorIs(ftKeeper.UpdateDEXUnifiedRefAmount( - ctx, randomAddr, ft1Denom, unifiedRefAmount), cosmoserrors.ErrUnauthorized, - ) - - // update the settings - requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, ft1Denom, unifiedRefAmount)) - - gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken.DEXSettings = &types.DEXSettings{ - UnifiedRefAmount: &unifiedRefAmount, - } - requireT.Equal(expectToken, gotToken) - - // update the settings one more time but with the gov acc - unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("999") - requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, unifiedRefAmount), - ) - - gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken.DEXSettings = &types.DEXSettings{ - UnifiedRefAmount: &unifiedRefAmount, - } - requireT.Equal(expectToken, gotToken) - - // update the different setting to check that we don't affect other - whitelistedDenoms := []string{denom1} - requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, whitelistedDenoms, - )) - unifiedRefAmount = sdkmath.LegacyMustNewDecFromStr("777") - requireT.NoError(ftKeeper.UpdateDEXUnifiedRefAmount( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft1Denom, unifiedRefAmount), - ) - gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken.DEXSettings = &types.DEXSettings{ - UnifiedRefAmount: &unifiedRefAmount, - WhitelistedDenoms: whitelistedDenoms, - } - requireT.Equal(expectToken, gotToken) - - // try to update settings for the not FT denom from not gov - requireT.ErrorIs( - ftKeeper.UpdateDEXUnifiedRefAmount(ctx, issuer, denom1, unifiedRefAmount), cosmoserrors.ErrUnauthorized, - ) - requireT.NoError( - ftKeeper.UpdateDEXUnifiedRefAmount( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), denom1, unifiedRefAmount, - ), - ) - - dexSettings, err := ftKeeper.GetDEXSettings(ctx, denom1) - requireT.NoError(err) - - requireT.Equal(types.DEXSettings{ - UnifiedRefAmount: &unifiedRefAmount, - }, dexSettings) -} - -func TestKeeper_UpdateDEXWhitelistedDenoms(t *testing.T) { - requireT := require.New(t) - - testApp := simapp.New() - ctx := testApp.BaseApp.NewContextLegacy(false, tmproto.Header{}) - - ftKeeper := testApp.AssetFTKeeper - - issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - ft1Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC", - Subunit: "abc", - Precision: 8, - InitialAmount: sdkmath.NewInt(777), - Features: []types.Feature{ - types.Feature_dex_whitelisted_denoms, - }, - } - - ft1Denom, err := ftKeeper.Issue(ctx, ft1Settings) - requireT.NoError(err) - - gotToken, err := ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken := types.Token{ - Denom: ft1Denom, - Issuer: ft1Settings.Issuer.String(), - Symbol: ft1Settings.Symbol, - Subunit: strings.ToLower(ft1Settings.Subunit), - Precision: ft1Settings.Precision, - BurnRate: sdkmath.LegacyNewDec(0), - SendCommissionRate: sdkmath.LegacyNewDec(0), - Version: types.CurrentTokenVersion, - Admin: ft1Settings.Issuer.String(), - DEXSettings: ft1Settings.DEXSettings, - Features: []types.Feature{ - types.Feature_dex_whitelisted_denoms, - }, - } - requireT.Equal(expectToken, gotToken) - - // try to update with the invalid whitelisted denoms - whitelistedDenoms := []string{"1denom1"} - requireT.ErrorIs(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms), types.ErrInvalidInput) - - // try to update from not issuer - whitelistedDenoms = []string{denom1} - randomAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - requireT.ErrorIs( - ftKeeper.UpdateDEXWhitelistedDenoms(ctx, randomAddr, ft1Denom, whitelistedDenoms), cosmoserrors.ErrUnauthorized, - ) - - requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms)) - - gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken.DEXSettings = &types.DEXSettings{ - WhitelistedDenoms: whitelistedDenoms, - } - requireT.Equal(expectToken, gotToken) - - // update the to empty list to allow all denoms - whitelistedDenoms = make([]string, 0) - requireT.NoError(ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft1Denom, whitelistedDenoms)) - - gotToken, err = ftKeeper.GetToken(ctx, ft1Denom) - requireT.NoError(err) - expectToken.DEXSettings = &types.DEXSettings{ - WhitelistedDenoms: nil, - } - requireT.Equal(expectToken, gotToken) - - whitelistedDenoms = []string{denom1} - - // try to update settings for the not FT denom from not gov - requireT.ErrorIs( - ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, denom1, whitelistedDenoms), cosmoserrors.ErrUnauthorized, - ) - // update from gov - requireT.NoError( - ftKeeper.UpdateDEXWhitelistedDenoms( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), denom1, whitelistedDenoms, - ), - ) - - dexSettings, err := ftKeeper.GetDEXSettings(ctx, denom1) - requireT.NoError(err) - - requireT.Equal(types.DEXSettings{ - WhitelistedDenoms: whitelistedDenoms, - }, dexSettings) - - ft2Settings := types.IssueSettings{ - Issuer: issuer, - Symbol: "ABC2", - Subunit: "abc2", - Precision: 8, - InitialAmount: sdkmath.NewInt(777), - // no features - } - - ft2Denom, err := ftKeeper.Issue(ctx, ft2Settings) - requireT.NoError(err) - - whitelistedDenoms = []string{denom2} - - // try to update settings from issuer - requireT.ErrorIs( - ftKeeper.UpdateDEXWhitelistedDenoms(ctx, issuer, ft2Denom, whitelistedDenoms), types.ErrFeatureDisabled, - ) - // update from gov - requireT.NoError( - ftKeeper.UpdateDEXWhitelistedDenoms( - ctx, authtypes.NewModuleAddress(govtypes.ModuleName), ft2Denom, whitelistedDenoms, - ), - ) - - dexSettings, err = ftKeeper.GetDEXSettings(ctx, ft2Denom) - requireT.NoError(err) - - requireT.Equal(types.DEXSettings{ - WhitelistedDenoms: whitelistedDenoms, - }, dexSettings) -} diff --git a/x/asset/ft/types/dex.go b/x/asset/ft/types/dex.go new file mode 100644 index 000000000..b73ab0f3b --- /dev/null +++ b/x/asset/ft/types/dex.go @@ -0,0 +1,125 @@ +package types + +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// AccountToCoin is mapping of address and coin. +type AccountToCoin struct { + Address sdk.AccAddress + Coin sdk.Coin +} + +// CoinToSend represents a coin to be sent from one address to another. +type CoinToSend struct { + FromAddress sdk.AccAddress + ToAddress sdk.AccAddress + Coin sdk.Coin +} + +// DEXOrder is DEX order. +// +//nolint:tagliatelle +type DEXOrder struct { + Creator sdk.AccAddress `json:"creator"` + Type string `json:"type"` + ID string `json:"id"` + BaseDenom string `json:"base_denom"` + QuoteDenom string `json:"quote_denom"` + Price *string `json:"price,omitempty"` // might be nil + Quantity sdkmath.Int `json:"quantity"` + Side string `json:"side"` +} + +// DEXActions is a set of DEX actions to be executed corresponding to one order. +type DEXActions struct { + Order DEXOrder + CreatorExpectedToSpend sdk.Coin + CreatorExpectedToReceive sdk.Coin + IncreaseLocked []AccountToCoin + DecreaseLocked []AccountToCoin + IncreaseExpectedToReceive []AccountToCoin + DecreaseExpectedToReceive []AccountToCoin + Send []CoinToSend +} + +// NewDEXActions returns new instance of DEXActions. +func NewDEXActions(order DEXOrder) DEXActions { + return DEXActions{ + Order: order, + IncreaseLocked: make([]AccountToCoin, 0), + DecreaseLocked: make([]AccountToCoin, 0), + IncreaseExpectedToReceive: make([]AccountToCoin, 0), + DecreaseExpectedToReceive: make([]AccountToCoin, 0), + Send: make([]CoinToSend, 0), + } +} + +// AddCreatorExpectedToSpend adds the given coin to the CreatorExpectedToSpend field of DEXActions. +func (da *DEXActions) AddCreatorExpectedToSpend(coin sdk.Coin) { + if da.CreatorExpectedToSpend.IsNil() { + da.CreatorExpectedToSpend = coin + return + } + da.CreatorExpectedToSpend = da.CreatorExpectedToSpend.Add(coin) +} + +// AddCreatorExpectedToReceive adds the given coin to the CreatorExpectedToReceive field of DEXActions. +func (da *DEXActions) AddCreatorExpectedToReceive(coin sdk.Coin) { + if da.CreatorExpectedToReceive.IsNil() { + da.CreatorExpectedToReceive = coin + return + } + da.CreatorExpectedToReceive = da.CreatorExpectedToReceive.Add(coin) +} + +// AddIncreaseLocked adds the specified coin to the IncreaseLocked list for the given address. +func (da *DEXActions) AddIncreaseLocked(address sdk.AccAddress, coin sdk.Coin) { + da.IncreaseLocked = appendOrAddToAccountsToCoin(da.IncreaseLocked, AccountToCoin{Address: address, Coin: coin}) +} + +// AddDecreaseLocked adds the specified coin to the DecreaseLocked list for the given address. +func (da *DEXActions) AddDecreaseLocked(address sdk.AccAddress, coin sdk.Coin) { + da.DecreaseLocked = appendOrAddToAccountsToCoin(da.DecreaseLocked, AccountToCoin{Address: address, Coin: coin}) +} + +// AddIncreaseExpectedToReceive adds the specified coin to the IncreaseExpectedToReceive list for the given address. +func (da *DEXActions) AddIncreaseExpectedToReceive(address sdk.AccAddress, coin sdk.Coin) { + da.IncreaseExpectedToReceive = appendOrAddToAccountsToCoin( + da.IncreaseExpectedToReceive, AccountToCoin{Address: address, Coin: coin}, + ) +} + +// AddDecreaseExpectedToReceive adds the specified coin to the DecreaseExpectedToReceive list for the given address. +func (da *DEXActions) AddDecreaseExpectedToReceive(address sdk.AccAddress, coin sdk.Coin) { + da.DecreaseExpectedToReceive = appendOrAddToAccountsToCoin( + da.DecreaseExpectedToReceive, AccountToCoin{Address: address, Coin: coin}, + ) +} + +// AddSend appends a new CoinToSend to the Send list with the specified fromAddr, toAddr, and coin. +func (da *DEXActions) AddSend(fromAddr, toAddr sdk.AccAddress, coin sdk.Coin) { + for i, send := range da.Send { + if send.FromAddress.String() == fromAddr.String() && + send.ToAddress.String() == toAddr.String() && + send.Coin.Denom == coin.Denom { + da.Send[i].Coin = send.Coin.Add(coin) + return + } + } + da.Send = append(da.Send, CoinToSend{FromAddress: fromAddr, ToAddress: toAddr, Coin: coin}) +} + +func appendOrAddToAccountsToCoin(accountsToCoin []AccountToCoin, accountToCoin AccountToCoin) []AccountToCoin { + for i, item := range accountsToCoin { + if item.Address.String() == accountToCoin.Address.String() && + item.Coin.Denom == accountToCoin.Coin.Denom { + accountsToCoin[i].Coin = item.Coin.Add(accountToCoin.Coin) + return accountsToCoin + } + } + // append if not found + accountsToCoin = append(accountsToCoin, accountToCoin) + return accountsToCoin +} diff --git a/x/asset/ft/types/errors.go b/x/asset/ft/types/errors.go index 5c00d1c21..80e65a854 100644 --- a/x/asset/ft/types/errors.go +++ b/x/asset/ft/types/errors.go @@ -25,6 +25,8 @@ var ( ErrExtensionCallFailed = sdkerrors.Register(ModuleName, 9, "call to asset extension failed") // ErrDEXSettingsNotFound error for a DEX settings not found in the store. ErrDEXSettingsNotFound = sdkerrors.Register(ModuleName, 10, "DEX settings not found") - // ErrDEXLockFailed is returned when DEX lock is failed. - ErrDEXLockFailed = sdkerrors.Register(ModuleName, 11, "DEX lock is failed") + // ErrDEXInsufficientSpendableBalance is returned when the amount to use for the DEX is more than the user has. + ErrDEXInsufficientSpendableBalance = sdkerrors.Register( + ModuleName, 11, "DEX insufficient spendable balance", + ) ) diff --git a/x/dex/genesis_test.go b/x/dex/genesis_test.go index 1a1f1e60e..47579f5ee 100644 --- a/x/dex/genesis_test.go +++ b/x/dex/genesis_test.go @@ -161,11 +161,11 @@ func TestInitAndExportGenesis(t *testing.T) { lockedBalance, err := orderWithSeq.Order.ComputeLimitOrderLockedBalance() require.NoError(t, err) testApp.MintAndSendCoin(t, sdkCtx, creator, sdk.NewCoins(lockedBalance)) - require.NoError(t, testApp.AssetFTKeeper.DEXLock( + require.NoError(t, testApp.AssetFTKeeper.DEXIncreaseLocked( sdkCtx, creator, lockedBalance, )) testApp.MintAndSendCoin(t, sdkCtx, creator, sdk.NewCoins(prams.OrderReserve)) - require.NoError(t, testApp.AssetFTKeeper.DEXLock( + require.NoError(t, testApp.AssetFTKeeper.DEXIncreaseLocked( sdkCtx, creator, orderWithSeq.Order.Reserve, )) } diff --git a/x/dex/keeper/keeper.go b/x/dex/keeper/keeper.go index 6c10f11be..4ad41a512 100644 --- a/x/dex/keeper/keeper.go +++ b/x/dex/keeper/keeper.go @@ -607,12 +607,6 @@ func (k Keeper) createOrder( "record", record.String(), ) - var err error - record, err = k.lockRequiredBalances(ctx, params, order, record) - if err != nil { - return err - } - if err := k.incrementAccountDenomsOrdersCounter( ctx, record.AccountNumber, @@ -631,7 +625,6 @@ func (k Keeper) createOrder( if params.OrderReserve.IsPositive() { order.Reserve = params.OrderReserve } - order.RemainingQuantity = record.RemainingQuantity order.RemainingBalance = record.RemainingBalance @@ -710,7 +703,7 @@ func (k Keeper) saveOrderWithOrderBookRecord( func (k Keeper) removeOrderByRecord( ctx sdk.Context, - acc sdk.AccAddress, + creator sdk.AccAddress, record types.OrderBookRecord, ) error { k.logger(ctx).Debug( @@ -733,13 +726,6 @@ func (k Keeper) removeOrderByRecord( } k.removeOrderData(ctx, record.OrderSeq) - // unlock the reserve is present - if orderData.Reserve.IsPositive() { - if err := k.unlockFT(ctx, acc, orderData.Reserve); err != nil { - return err - } - } - if err := k.removeOrderIDToSeq(ctx, record.AccountNumber, record.OrderID); err != nil { return err } @@ -759,7 +745,7 @@ func (k Keeper) removeOrderByRecord( if err := ctx.EventManager().EmitTypedEvent(&types.EventOrderClosed{ Order: types.Order{ - Creator: acc.String(), + Creator: creator.String(), Type: types.ORDER_TYPE_LIMIT, ID: record.OrderID, BaseDenom: orderBookData.BaseDenom, @@ -802,7 +788,7 @@ func (k Keeper) cancelOrder(ctx sdk.Context, acc sdk.AccAddress, orderID string) return err } - unlockCoin := sdk.NewCoin(order.GetSpendDenom(), order.RemainingBalance) + lockedCoins := sdk.NewCoins(sdk.NewCoin(order.GetSpendDenom(), order.RemainingBalance)) expectedToReceiveCoin, err := types.ComputeLimitOrderExpectedToReceiveBalance( order.Side, order.BaseDenom, order.QuoteDenom, record.RemainingQuantity, *order.Price, ) @@ -810,8 +796,13 @@ func (k Keeper) cancelOrder(ctx sdk.Context, acc sdk.AccAddress, orderID string) return err } - return k.decreaseFTLimits( - ctx, acc, unlockCoin, expectedToReceiveCoin, + // unlock the reserve if present + if order.Reserve.IsPositive() { + lockedCoins = lockedCoins.Add(order.Reserve) + } + + return k.assetFTKeeper.DEXDecreaseLimits( + ctx, acc, lockedCoins, expectedToReceiveCoin, ) } diff --git a/x/dex/keeper/keeper_ft_lock.go b/x/dex/keeper/keeper_ft_lock.go deleted file mode 100644 index 52ae6b06a..000000000 --- a/x/dex/keeper/keeper_ft_lock.go +++ /dev/null @@ -1,128 +0,0 @@ -package keeper - -import ( - sdkerrors "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/CoreumFoundation/coreum/v5/x/dex/types" -) - -func (k Keeper) increaseFTLimits( - ctx sdk.Context, - addr sdk.AccAddress, - lockedCoin, expectedToReceiveCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Increasing DEX FT limits.", - "addr", addr, - "lockedCoin", lockedCoin.String(), - "expectedToReceiveCoin", expectedToReceiveCoin.String(), - ) - - if err := k.assetFTKeeper.DEXIncreaseLimits(ctx, addr, lockedCoin, expectedToReceiveCoin); err != nil { - return sdkerrors.Wrap(err, "failed to increase DEX FT limits") - } - - return nil -} - -func (k Keeper) decreaseFTLimits( - ctx sdk.Context, - addr sdk.AccAddress, - lockedCoin, expectedToReceiveCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Decreasing DEX FT limits.", - "addr", addr, - "lockedCoin", lockedCoin.String(), - "expectedToReceiveCoin", expectedToReceiveCoin.String(), - ) - - if err := k.assetFTKeeper.DEXDecreaseLimits(ctx, addr, lockedCoin, expectedToReceiveCoin); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to decrease DEX FT limits, err: %s", err) - } - - return nil -} - -func (k Keeper) decreaseFTLimitsAndSend( - ctx sdk.Context, - fromAddr, toAddr sdk.AccAddress, - unlockAndSendCoin, expectedToReceiveCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Decreasing DEX FT limits and sending.", - "fromAddr", fromAddr, - "toAddr", toAddr, - "unlockAndSendCoin", unlockAndSendCoin.String(), - "expectedToReceiveCoin", expectedToReceiveCoin.String(), - ) - - if err := k.assetFTKeeper.DEXDecreaseLimitsAndSend( - ctx, fromAddr, toAddr, unlockAndSendCoin, expectedToReceiveCoin, - ); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to decrease DEX FT limits and send, err: %s", err) - } - - return nil -} - -func (k Keeper) checkFTLimitsAndSend( - ctx sdk.Context, - fromAddr, toAddr sdk.AccAddress, - sendCoin, checkExpectedToReceiveCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Checking DEX FT limits and sending.", - "fromAddr", fromAddr, - "toAddr", toAddr, - "sendCoin", sendCoin.String(), - "checkExpectedToReceiveCoin", checkExpectedToReceiveCoin.String(), - ) - - if err := k.assetFTKeeper.DEXCheckLimitsAndSend( - ctx, - fromAddr, toAddr, - sendCoin, checkExpectedToReceiveCoin, - ); err != nil { - return sdkerrors.Wrap(err, "failed to check DEX FT limits and send") - } - - return nil -} - -func (k Keeper) lockFT( - ctx sdk.Context, - addr sdk.AccAddress, - lockCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Locking FT coin.", - "addr", addr, - "lockCoin", lockCoin.String(), - ) - - if err := k.assetFTKeeper.DEXLock(ctx, addr, lockCoin); err != nil { - return sdkerrors.Wrap(err, "failed to lock DEX FT coin") - } - - return nil -} - -func (k Keeper) unlockFT( - ctx sdk.Context, - addr sdk.AccAddress, - unlockCoin sdk.Coin, -) error { - k.logger(ctx).Debug( - "Unlocking FT coin.", - "addr", addr, - "unlockCoin", unlockCoin.String(), - ) - - if err := k.assetFTKeeper.DEXUnlock(ctx, addr, unlockCoin); err != nil { - return sdkerrors.Wrapf(types.ErrInvalidState, "failed to unlock DEX FT coin, err: %s", err) - } - - return nil -} diff --git a/x/dex/keeper/keeper_matching.go b/x/dex/keeper/keeper_matching.go index 72bb2f793..a8dbc4d66 100644 --- a/x/dex/keeper/keeper_matching.go +++ b/x/dex/keeper/keeper_matching.go @@ -39,6 +39,7 @@ func (k Keeper) matchOrder( if err != nil { return err } + accNumberToAddCache := make(map[uint64]sdk.AccAddress) for { makerRecord, matches, err := mf.Next() if err != nil { @@ -47,7 +48,7 @@ func (k Keeper) matchOrder( if !matches { break } - stop, err := k.matchRecords(ctx, mr, &takerRecord, &makerRecord, order) + stop, err := k.matchRecords(ctx, mr, &takerRecord, &makerRecord, order, accNumberToAddCache) if err != nil { return err } @@ -60,14 +61,18 @@ func (k Keeper) matchOrder( case types.ORDER_TYPE_LIMIT: switch order.TimeInForce { case types.TIME_IN_FORCE_GTC: + if err := mr.IncreaseTakerLimitsForRecord(params, order, &takerRecord); err != nil { + return err + } // create new order with the updated record if err := k.applyMatchingResult(ctx, mr); err != nil { return err } - if takerRecord.RemainingBalance.IsPositive() { - return k.createOrder(ctx, params, order, takerRecord) + if takerRecord.RemainingBalance.IsZero() { + return nil } - return nil + + return k.createOrder(ctx, params, order, takerRecord) case types.TIME_IN_FORCE_IOC: return k.applyMatchingResult(ctx, mr) case types.TIME_IN_FORCE_FOK: @@ -154,6 +159,7 @@ func (k Keeper) matchRecords( mr *MatchingResult, takerRecord, makerRecord *types.OrderBookRecord, order types.Order, + accNumberToAddCache map[uint64]sdk.AccAddress, ) (bool, error) { recordToClose, recordToReduce := k.getRecordToCloseAndReduce(ctx, takerRecord, makerRecord) k.logger(ctx).Debug( @@ -177,31 +183,36 @@ func (k Keeper) matchRecords( recordToCloseRemainingQuantity := recordToClose.RemainingQuantity.Sub(recordToCloseReducedQuantity) if recordToClose.IsMaker() { - mr.RegisterTakerCheckLimitsAndSendCoin( - recordToClose.AccountNumber, recordToClose.OrderID, recordToCloseReceiveCoin, recordToReduceReceiveCoin, - ) - mr.RegisterMakerUnlockAndSend( - recordToClose.AccountNumber, recordToClose.OrderID, recordToReduceReceiveCoin, recordToCloseReceiveCoin, + makerAddr, err := k.getAccountAddressWithCache(ctx, recordToClose.AccountNumber, accNumberToAddCache) + if err != nil { + return false, err + } + mr.TakerSend( + makerAddr, recordToClose.OrderID, recordToCloseReceiveCoin, ) - unlockCoin := sdk.NewCoin( - recordToReduceReceiveCoin.Denom, recordToClose.RemainingBalance.Sub(recordToReduceReceiveCoin.Amount), + mr.MakerSend( + makerAddr, recordToClose.OrderID, recordToReduceReceiveCoin, ) - expectedToReceiveAmt, err := types.ComputeLimitOrderExpectedToReceiveAmount( - recordToClose.Side, recordToCloseRemainingQuantity, recordToClose.Price, + + lockedCoins, expectedToReceiveCoin, err := k.getMakerLockerAndExpectedToReceiveCoins( + ctx, recordToReduceReceiveCoin, recordToClose, recordToCloseRemainingQuantity, recordToCloseReceiveCoin, ) if err != nil { return false, err } - mr.RegisterMakerUnlock( - recordToClose.AccountNumber, unlockCoin, sdk.NewCoin(recordToCloseReceiveCoin.Denom, expectedToReceiveAmt), - ) - mr.RegisterMakerRemoveRecord(recordToClose) + + mr.DecreaseMakerLimits(makerAddr, lockedCoins, expectedToReceiveCoin) + mr.RemoveRecord(makerAddr, recordToClose) } else { - mr.RegisterTakerCheckLimitsAndSendCoin( - recordToReduce.AccountNumber, recordToReduce.OrderID, recordToReduceReceiveCoin, recordToCloseReceiveCoin, + makerAddr, err := k.getAccountAddressWithCache(ctx, recordToReduce.AccountNumber, accNumberToAddCache) + if err != nil { + return false, err + } + mr.TakerSend( + makerAddr, recordToReduce.OrderID, recordToReduceReceiveCoin, ) - mr.RegisterMakerUnlockAndSend( - recordToReduce.AccountNumber, recordToReduce.OrderID, recordToCloseReceiveCoin, recordToReduceReceiveCoin, + mr.MakerSend( + makerAddr, recordToReduce.OrderID, recordToCloseReceiveCoin, ) } @@ -224,10 +235,40 @@ func (k Keeper) matchRecords( return false, nil } - mr.RegisterMakerUpdateRecord(*makerRecord) + mr.UpdateRecord(*makerRecord) return true, nil } +func (k Keeper) getMakerLockerAndExpectedToReceiveCoins( + ctx sdk.Context, + recordToReduceReceiveCoin sdk.Coin, + recordToClose *types.OrderBookRecord, + recordToCloseRemainingQuantity sdkmath.Int, + recordToCloseReceiveCoin sdk.Coin, +) (sdk.Coins, sdk.Coin, error) { + lockedCoins := sdk.NewCoins(sdk.NewCoin( + recordToReduceReceiveCoin.Denom, recordToClose.RemainingBalance.Sub(recordToReduceReceiveCoin.Amount), + )) + // get the record data to unlock the reserve if present + recordToCloseOrderData, err := k.getOrderData(ctx, recordToClose.OrderSeq) + if err != nil { + return nil, sdk.Coin{}, err + } + if recordToCloseOrderData.Reserve.IsPositive() { + lockedCoins = lockedCoins.Add(recordToCloseOrderData.Reserve) + } + + expectedToReceiveAmt, err := types.ComputeLimitOrderExpectedToReceiveAmount( + recordToClose.Side, recordToCloseRemainingQuantity, recordToClose.Price, + ) + if err != nil { + return nil, sdk.Coin{}, err + } + expectedToReceiveCoin := sdk.NewCoin(recordToCloseReceiveCoin.Denom, expectedToReceiveAmt) + + return lockedCoins, expectedToReceiveCoin, nil +} + func (k Keeper) getRecordToCloseAndReduce(ctx sdk.Context, takerRecord, makerRecord *types.OrderBookRecord) ( *types.OrderBookRecord, *types.OrderBookRecord, ) { @@ -257,49 +298,6 @@ func (k Keeper) getRecordToCloseAndReduce(ctx sdk.Context, takerRecord, makerRec return recordToClose, recordToReduce } -func (k Keeper) lockRequiredBalances( - ctx sdk.Context, params types.Params, order types.Order, takerRecord types.OrderBookRecord, -) (types.OrderBookRecord, error) { - creatorAddr, err := sdk.AccAddressFromBech32(order.Creator) - if err != nil { - return types.OrderBookRecord{}, sdkerrors.Wrapf(types.ErrInvalidInput, "invalid address: %s", order.Creator) - } - // recompute the min required balance to be locked based on record remaining quantity - lockCoin, err := types.ComputeLimitOrderLockedBalance( - order.Side, order.BaseDenom, order.QuoteDenom, takerRecord.RemainingQuantity, *order.Price, - ) - if err != nil { - return types.OrderBookRecord{}, err - } - expectedToReceiveCoin, err := types.ComputeLimitOrderExpectedToReceiveBalance( - order.Side, order.BaseDenom, order.QuoteDenom, takerRecord.RemainingQuantity, *order.Price, - ) - if err != nil { - return types.OrderBookRecord{}, err - } - - // lock reserve if positive - if params.OrderReserve.IsPositive() { - // don't check for the DEX FT limits since it's independent of the trading limits - if err := k.lockFT(ctx, creatorAddr, params.OrderReserve); err != nil { - return types.OrderBookRecord{}, - sdkerrors.Wrapf(err, "failed to lock order reserve: %s", params.OrderReserve.String()) - } - } - - if err := k.increaseFTLimits( - ctx, - creatorAddr, - lockCoin, - expectedToReceiveCoin, - ); err != nil { - return types.OrderBookRecord{}, err - } - takerRecord.RemainingBalance = lockCoin.Amount - - return takerRecord, nil -} - func getRecordsReceiveCoins( makerRecord, recordToClose, recordToReduce *types.OrderBookRecord, order types.Order, diff --git a/x/dex/keeper/keeper_matching_fuzz_test.go b/x/dex/keeper/keeper_matching_fuzz_test.go index 6e33047b3..e6b3cd247 100644 --- a/x/dex/keeper/keeper_matching_fuzz_test.go +++ b/x/dex/keeper/keeper_matching_fuzz_test.go @@ -442,7 +442,7 @@ func (fa *FuzzApp) PlaceOrder(t *testing.T, sdkCtx sdk.Context, order types.Orde t.Logf("Placement failed, err: %s", err.Error()) creator := sdk.MustAccAddressFromBech32(order.Creator) switch { - case sdkerrors.IsOf(err, assetfttypes.ErrDEXLockFailed): + case sdkerrors.IsOf(err, assetfttypes.ErrDEXInsufficientSpendableBalance): // check that the order can't be placed because of the lack of balance if order.Type != types.ORDER_TYPE_LIMIT { return diff --git a/x/dex/keeper/keeper_matching_result.go b/x/dex/keeper/keeper_matching_result.go index 2d79a8fbc..6cf604b72 100644 --- a/x/dex/keeper/keeper_matching_result.go +++ b/x/dex/keeper/keeper_matching_result.go @@ -4,43 +4,27 @@ import ( sdkerrors "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/samber/lo" + assetfttypes "github.com/CoreumFoundation/coreum/v5/x/asset/ft/types" "github.com/CoreumFoundation/coreum/v5/x/dex/types" ) -// TakerCheckLimitsAndSendCoin is the taker coin to check limits send. -type TakerCheckLimitsAndSendCoin struct { - MakerAccNumber uint64 - MakerOrderID string - SendCoin sdk.Coin - CheckExpectedToReceiveCoin sdk.Coin -} - -// MakerUnlockAndSendCoin is the maker coin to unlock and send. -type MakerUnlockAndSendCoin struct { - MakerAccNumber uint64 - MakerOrderID string - UnlockAndSendCoin sdk.Coin - DecreaseExpectedToReceiveCoin sdk.Coin -} - -// MakerUnlockCoin is the maker coin to unlock. -type MakerUnlockCoin struct { - MakerAccNumber uint64 - UnlockCoin sdk.Coin - DecreaseExpectedToReceiveCoin sdk.Coin +// RecordToAddress is acc address mapped to record. +type RecordToAddress struct { + Address sdk.AccAddress + Record *types.OrderBookRecord } // MatchingResult is the result of a matching operation. type MatchingResult struct { TakerAddress sdk.AccAddress + FTActions assetfttypes.DEXActions TakerOrderReducedEvent types.EventOrderReduced - TakerCheckLimitsAndSend []TakerCheckLimitsAndSendCoin - MakerUnlockAndSend []MakerUnlockAndSendCoin - MakerUnlock []MakerUnlockCoin - MakerRemoveRecords []*types.OrderBookRecord MakerOrderReducedEvents []types.EventOrderReduced - MakerUpdateRecord *types.OrderBookRecord + + RecordsToRemove []RecordToAddress + RecordToUpdate *types.OrderBookRecord } // NewMatchingResult creates a new MatchingResult. @@ -50,250 +34,187 @@ func NewMatchingResult(order types.Order) (*MatchingResult, error) { return nil, sdkerrors.Wrapf(types.ErrInvalidInput, "invalid address: %s", order.Creator) } + var orderStrPrice *string + if order.Price != nil { + orderStrPrice = lo.ToPtr(order.Price.String()) + } + return &MatchingResult{ TakerAddress: takerAddress, + FTActions: assetfttypes.NewDEXActions( + assetfttypes.DEXOrder{ + Creator: takerAddress, + Type: order.Type.String(), + ID: order.ID, + BaseDenom: order.BaseDenom, + QuoteDenom: order.QuoteDenom, + Price: orderStrPrice, + Quantity: order.Quantity, + Side: order.Side.String(), + }, + ), TakerOrderReducedEvent: types.EventOrderReduced{ Creator: order.Creator, ID: order.ID, SentCoin: sdk.NewCoin(order.GetSpendDenom(), sdkmath.ZeroInt()), ReceivedCoin: sdk.NewCoin(order.GetReceiveDenom(), sdkmath.ZeroInt()), }, - TakerCheckLimitsAndSend: make([]TakerCheckLimitsAndSendCoin, 0), - MakerUnlockAndSend: make([]MakerUnlockAndSendCoin, 0), - MakerUnlock: make([]MakerUnlockCoin, 0), - MakerRemoveRecords: make([]*types.OrderBookRecord, 0), MakerOrderReducedEvents: make([]types.EventOrderReduced, 0), - MakerUpdateRecord: nil, + RecordsToRemove: make([]RecordToAddress, 0), + RecordToUpdate: nil, }, nil } -// RegisterTakerCheckLimitsAndSendCoin sets the taker coin to check limits and send. -func (mr *MatchingResult) RegisterTakerCheckLimitsAndSendCoin( - makerAccNumber uint64, - makerOrderID string, - sendCoin, checkExpectedToReceiveCoin sdk.Coin, -) { - if sendCoin.IsZero() { +// TakerSend registers the coin to send from taker to maker. +func (mr *MatchingResult) TakerSend(makerAddr sdk.AccAddress, makerOrderID string, coin sdk.Coin) { + if coin.IsZero() { return } - mr.TakerCheckLimitsAndSend = append(mr.TakerCheckLimitsAndSend, TakerCheckLimitsAndSendCoin{ - MakerAccNumber: makerAccNumber, - MakerOrderID: makerOrderID, - SendCoin: sendCoin, - CheckExpectedToReceiveCoin: checkExpectedToReceiveCoin, - }) -} + mr.FTActions.AddCreatorExpectedToSpend(coin) + mr.FTActions.AddSend(mr.TakerAddress, makerAddr, coin) + mr.FTActions.AddDecreaseExpectedToReceive(makerAddr, coin) -// RegisterMakerUnlockAndSend sets the maker coin to unlock and send. -func (mr *MatchingResult) RegisterMakerUnlockAndSend( - makerAccNumber uint64, - makerOrderID string, - unlockAndSendCoin, decreaseExpectedToReceiveCoin sdk.Coin, -) { - if unlockAndSendCoin.IsZero() { - return - } - - mr.MakerUnlockAndSend = append(mr.MakerUnlockAndSend, MakerUnlockAndSendCoin{ - MakerOrderID: makerOrderID, - MakerAccNumber: makerAccNumber, - UnlockAndSendCoin: unlockAndSendCoin, - DecreaseExpectedToReceiveCoin: decreaseExpectedToReceiveCoin, - }) + mr.updateTakerSendEvents(makerAddr, makerOrderID, coin) } -// RegisterMakerUnlock sets the maker coin to unlock. -func (mr *MatchingResult) RegisterMakerUnlock( - makerAccNumber uint64, unlockCoin, decreaseExpectedToReceiveCoin sdk.Coin, -) { - if unlockCoin.IsZero() { +// MakerSend registers the coin to send from maker to taker. +func (mr *MatchingResult) MakerSend(makerAddr sdk.AccAddress, makerOrderID string, coin sdk.Coin) { + if coin.IsZero() { return } - mr.MakerUnlock = append(mr.MakerUnlock, MakerUnlockCoin{ - MakerAccNumber: makerAccNumber, - UnlockCoin: unlockCoin, - DecreaseExpectedToReceiveCoin: decreaseExpectedToReceiveCoin, - }) -} - -// RegisterMakerRemoveRecord sets the record to remove. -func (mr *MatchingResult) RegisterMakerRemoveRecord(record *types.OrderBookRecord) { - mr.MakerRemoveRecords = append(mr.MakerRemoveRecords, record) -} - -// RegisterMakerUpdateRecord sets the record to update. -func (mr *MatchingResult) RegisterMakerUpdateRecord(record types.OrderBookRecord) { - mr.MakerUpdateRecord = &record -} - -type accountToCoinsMapping struct { - AccAddress sdk.AccAddress - Coin1 sdk.Coin - Coin2 sdk.Coin -} + // call `AddCreatorExpectedToReceive` but don't call AddIncreaseExpectedToReceive since + // `AddIncreaseExpectedToReceive` is used for the state after the matching, but CreatorExpectedToReceive before + mr.FTActions.AddCreatorExpectedToReceive(coin) + mr.FTActions.AddDecreaseLocked(makerAddr, coin) + mr.FTActions.AddSend(makerAddr, mr.TakerAddress, coin) -type accountsToCoins struct { - mapping []accountToCoinsMapping + mr.updateMakerSendEvents(makerAddr, makerOrderID, coin) } -func newAccountsToCoins() *accountsToCoins { - return &accountsToCoins{ - mapping: make([]accountToCoinsMapping, 0), +// DecreaseMakerLimits registers the coins to unlock and decrease expected to receive. +func (mr *MatchingResult) DecreaseMakerLimits( + makerAddr sdk.AccAddress, + lockedCoins sdk.Coins, expectedToReceiveCoin sdk.Coin, +) { + for _, coin := range lockedCoins { + if coin.IsZero() { + continue + } + mr.FTActions.AddDecreaseLocked(makerAddr, coin) } -} -func (a *accountsToCoins) Add(acc sdk.AccAddress, coin1, coin2 sdk.Coin) { - for i := range a.mapping { - if a.mapping[i].AccAddress.String() == acc.String() { - a.mapping[i].Coin1 = a.mapping[i].Coin1.Add(coin1) - a.mapping[i].Coin2 = a.mapping[i].Coin2.Add(coin2) - return - } + if !expectedToReceiveCoin.IsZero() { + mr.FTActions.AddDecreaseExpectedToReceive(makerAddr, expectedToReceiveCoin) } - a.mapping = append(a.mapping, accountToCoinsMapping{ - AccAddress: acc, - Coin1: coin1, - Coin2: coin2, - }) } -func (k Keeper) applyMatchingResult(ctx sdk.Context, mr *MatchingResult) error { - accCache := make(map[uint64]sdk.AccAddress) - - if err := k.applyMatchingResultTakerCheckLimitsAndSend(ctx, mr, accCache); err != nil { - return err +// IncreaseTakerLimitsForRecord increase required limits for the taker record. +func (mr *MatchingResult) IncreaseTakerLimitsForRecord( + params types.Params, + order types.Order, + takerRecord *types.OrderBookRecord, +) error { + // if the order is filled fully + if takerRecord.RemainingBalance.IsZero() { + return nil } - if err := k.applyMatchingResultMakerUnlockAndSend(ctx, mr, accCache); err != nil { + lockedCoin, err := types.ComputeLimitOrderLockedBalance( + order.Side, order.BaseDenom, order.QuoteDenom, takerRecord.RemainingQuantity, *order.Price, + ) + if err != nil { return err } + // update taker record with the remaining balance + takerRecord.RemainingBalance = lockedCoin.Amount - if err := k.applyMatchingResultMakerUnlock(ctx, mr, accCache); err != nil { - return err - } + mr.FTActions.AddCreatorExpectedToSpend(lockedCoin) + mr.FTActions.AddIncreaseLocked(mr.TakerAddress, lockedCoin) - if err := k.applyMatchingResultMakerRemoveRecords(ctx, mr, accCache); err != nil { + expectedToReceiveCoin, err := types.ComputeLimitOrderExpectedToReceiveBalance( + order.Side, order.BaseDenom, order.QuoteDenom, takerRecord.RemainingQuantity, *order.Price, + ) + if err != nil { return err } + mr.FTActions.AddCreatorExpectedToReceive(expectedToReceiveCoin) + mr.FTActions.AddIncreaseExpectedToReceive(mr.TakerAddress, expectedToReceiveCoin) - if mr.MakerUpdateRecord != nil { - if err := k.saveOrderBookRecord(ctx, *mr.MakerUpdateRecord); err != nil { - return err - } + // lock reserve if is set + if params.OrderReserve.IsPositive() { + // lock but don't increase expected to receive + mr.FTActions.AddIncreaseLocked(mr.TakerAddress, params.OrderReserve) } - return k.publishMatchingEvents(ctx, mr) + return nil } -func (k Keeper) applyMatchingResultTakerCheckLimitsAndSend( - ctx sdk.Context, - mr *MatchingResult, - accCache map[uint64]sdk.AccAddress, -) error { - accsToCoins := newAccountsToCoins() - for _, item := range mr.TakerCheckLimitsAndSend { - makerAddr, err := k.getAccountAddressWithCache(ctx, item.MakerAccNumber, accCache) - if err != nil { - return err - } - accsToCoins.Add(makerAddr, item.SendCoin, item.CheckExpectedToReceiveCoin) - - // init event - mr.MakerOrderReducedEvents = append(mr.MakerOrderReducedEvents, types.EventOrderReduced{ - Creator: makerAddr.String(), - ID: item.MakerOrderID, - ReceivedCoin: item.SendCoin, - }) - mr.TakerOrderReducedEvent.SentCoin = mr.TakerOrderReducedEvent.SentCoin.Add(item.SendCoin) - } - for _, accToCoins := range accsToCoins.mapping { - if err := k.checkFTLimitsAndSend( - ctx, mr.TakerAddress, accToCoins.AccAddress, accToCoins.Coin1, accToCoins.Coin2, - ); err != nil { - return err - } - } - - return nil +// RemoveRecord registers the record to remove. +func (mr *MatchingResult) RemoveRecord(creator sdk.AccAddress, record *types.OrderBookRecord) { + mr.RecordsToRemove = append(mr.RecordsToRemove, RecordToAddress{ + Address: creator, + Record: record, + }) } -func (k Keeper) applyMatchingResultMakerUnlockAndSend( - ctx sdk.Context, - mr *MatchingResult, - accCache map[uint64]sdk.AccAddress, -) error { - accsToCoins := newAccountsToCoins() - for _, item := range mr.MakerUnlockAndSend { - makerAddr, err := k.getAccountAddressWithCache(ctx, item.MakerAccNumber, accCache) - if err != nil { - return err - } - accsToCoins.Add(makerAddr, item.UnlockAndSendCoin, item.DecreaseExpectedToReceiveCoin) - - // add sent part - for i := range mr.MakerOrderReducedEvents { - if mr.MakerOrderReducedEvents[i].Creator == makerAddr.String() && - mr.MakerOrderReducedEvents[i].ID == item.MakerOrderID { - mr.MakerOrderReducedEvents[i].SentCoin = item.UnlockAndSendCoin - } - } +// UpdateRecord registers the record to update. +func (mr *MatchingResult) UpdateRecord(record types.OrderBookRecord) { + mr.RecordToUpdate = &record +} - mr.TakerOrderReducedEvent.ReceivedCoin = mr.TakerOrderReducedEvent.ReceivedCoin.Add(item.UnlockAndSendCoin) - } +func (mr *MatchingResult) updateTakerSendEvents( + makerAddr sdk.AccAddress, + makerOrderID string, + coin sdk.Coin, +) { + mr.TakerOrderReducedEvent.SentCoin = mr.TakerOrderReducedEvent.SentCoin.Add(coin) + mr.MakerOrderReducedEvents = append(mr.MakerOrderReducedEvents, types.EventOrderReduced{ + Creator: makerAddr.String(), + ID: makerOrderID, + ReceivedCoin: coin, + }) +} - for _, accToCoins := range accsToCoins.mapping { - if err := k.decreaseFTLimitsAndSend( - ctx, accToCoins.AccAddress, mr.TakerAddress, accToCoins.Coin1, accToCoins.Coin2, - ); err != nil { - return err +func (mr *MatchingResult) updateMakerSendEvents( + makerAddr sdk.AccAddress, + makerOrderID string, + coin sdk.Coin, +) { + mr.TakerOrderReducedEvent.ReceivedCoin = mr.TakerOrderReducedEvent.ReceivedCoin.Add(coin) + for i := range mr.MakerOrderReducedEvents { + // find corresponding event created by `updateTakerSendEvents` + if mr.MakerOrderReducedEvents[i].Creator == makerAddr.String() && mr.MakerOrderReducedEvents[i].ID == makerOrderID { + mr.MakerOrderReducedEvents[i].SentCoin = coin + break } } - - return nil } -func (k Keeper) applyMatchingResultMakerUnlock( - ctx sdk.Context, - mr *MatchingResult, - accCache map[uint64]sdk.AccAddress, -) error { - accsToCoins := newAccountsToCoins() - for _, item := range mr.MakerUnlock { - makerAddr, err := k.getAccountAddressWithCache(ctx, item.MakerAccNumber, accCache) - if err != nil { - return err - } - accsToCoins.Add(makerAddr, item.UnlockCoin, item.DecreaseExpectedToReceiveCoin) +func (k Keeper) applyMatchingResult(ctx sdk.Context, mr *MatchingResult) error { + // if matched passed but no changes are applied return + if mr.FTActions.CreatorExpectedToSpend.IsNil() { + return nil } - for _, accToCoins := range accsToCoins.mapping { - if err := k.decreaseFTLimits( - ctx, accToCoins.AccAddress, accToCoins.Coin1, accToCoins.Coin2, - ); err != nil { - return err - } + if err := k.assetFTKeeper.DEXExecuteActions(ctx, mr.FTActions); err != nil { + return err } - return nil -} - -func (k Keeper) applyMatchingResultMakerRemoveRecords( - ctx sdk.Context, - mr *MatchingResult, - accCache map[uint64]sdk.AccAddress, -) error { - for _, item := range mr.MakerRemoveRecords { - makerAddr, err := k.getAccountAddressWithCache(ctx, item.AccountNumber, accCache) - if err != nil { + for _, item := range mr.RecordsToRemove { + if err := k.removeOrderByRecord(ctx, item.Address, *item.Record); err != nil { return err } - if err := k.removeOrderByRecord(ctx, makerAddr, *item); err != nil { + } + + if mr.RecordToUpdate != nil { + if err := k.saveOrderBookRecord(ctx, *mr.RecordToUpdate); err != nil { return err } } - return nil + + return k.publishMatchingEvents(ctx, mr) } func (k Keeper) publishMatchingEvents( diff --git a/x/dex/keeper/keeper_matching_test.go b/x/dex/keeper/keeper_matching_test.go index 69996c98b..8967b3e26 100644 --- a/x/dex/keeper/keeper_matching_test.go +++ b/x/dex/keeper/keeper_matching_test.go @@ -433,9 +433,9 @@ func TestKeeper_MatchOrders(t *testing.T) { }, } }, - // we fill the id1 first, so the remaining balance will be 3759 - 1000 * 375e-3 = 3384, - // but we need to lock (10000 - 1000) * 376e-3 = 3384 - wantErrorContains: "3384denom2 is not available, available 3383denom2", + // we fill the id1 first, so the used balance from id2 is 1000 * 375e-3 = 1000 * 375e-3 = 375 + // to fill remaining part we need (10000 - 1000) * 376e-3 = 3384, so total expected to send 3384 + 375 = 3759 + wantErrorContains: "3759denom2 is not available, available 3758denom2", }, { name: "match_limit_self_maker_sell_taker_buy_close_maker_with_partial_filling", @@ -763,7 +763,7 @@ func TestKeeper_MatchOrders(t *testing.T) { }, } }, - wantErrorContains: "9000denom1 is not available, available 8999denom1", + wantErrorContains: "10000denom1 is not available, available 9999denom1", }, { name: "match_limit_self_maker_buy_taker_sell_close_taker", @@ -1929,7 +1929,7 @@ func TestKeeper_MatchOrders(t *testing.T) { }, } }, - wantErrorContains: "9625denom2 is not available, available 9624denom2", + wantErrorContains: "10000denom2 is not available, available 9999denom2", }, { name: "match_limit_opposite_maker_sell_taker_sell_close_maker_with_partial_filling", @@ -2247,7 +2247,7 @@ func TestKeeper_MatchOrders(t *testing.T) { }, } }, - wantErrorContains: "25491denom1 is not available, available 25490denom1", + wantErrorContains: "26491denom1 is not available, available 26490denom1", }, { name: "match_limit_opposite_maker_buy_taker_buy_close_taker_with_partial_filling", @@ -5821,6 +5821,175 @@ func TestKeeper_MatchOrders(t *testing.T) { } }, }, + + { + name: "match_whitelisting_limit_self_maker_sell_taker_buy_close_maker_with_zero_filled_quantity", + balances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), + ), + testSet.acc2.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 10000), + ), + } + }, + whitelistedBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), // initial + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 1), // expected to receive + ), + testSet.acc2.String(): sdk.NewCoins( + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 1000), // expected to receive + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 10000), + ), + } + }, + orders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-5")), + // can't fill since 111 * 376e-5 ~= 0.41736 + Quantity: sdkmath.NewInt(111), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("1e1")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + } + }, + wantOrders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("1e1")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(10000), + }, + } + }, + wantAvailableBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), + ), + } + }, + wantExpectedToReceiveBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc2.String(): sdk.NewCoins(sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 1000)), + } + }, + }, + { + name: "match_whitelisting_limit_self_maker_buy_taker_sell_close_taker_with_zero_filled_quantity", + balances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 4), + ), + testSet.acc2.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), + ), + } + }, + whitelistedBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 1000), // expected to receive + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 4), // initial + ), + testSet.acc2.String(): sdk.NewCoins( + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), // initial + sdk.NewInt64Coin(testSet.ftDenomWhitelisting2, 1), // expected to receive + ), + } + }, + orders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-5")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-5")), + // can't fill since 111 * 376e-5 ~= 0.41736 + Quantity: sdkmath.NewInt(111), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + } + }, + wantOrders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: testSet.ftDenomWhitelisting1, + QuoteDenom: testSet.ftDenomWhitelisting2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-5")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(4), + }, + } + }, + wantAvailableBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc2.String(): sdk.NewCoins( + testSet.orderReserve, + sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 111), + ), + } + }, + wantExpectedToReceiveBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins(sdk.NewInt64Coin(testSet.ftDenomWhitelisting1, 1000)), + } + }, + }, { name: "match_limit_opposite_multiple_maker_buy_taker_buy_close_taker_with_same_price_fifo_priority", balances: func(testSet TestSet) map[string]sdk.Coins { @@ -5941,6 +6110,202 @@ func TestKeeper_MatchOrders(t *testing.T) { } }, }, + + { + name: "no_match_limit_self_and_opposite_buy_and_sell", + balances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserveTimes(2), + sdk.NewInt64Coin(denom1, 1000), + sdk.NewInt64Coin(denom2, 1000), + ), + testSet.acc2.String(): sdk.NewCoins( + testSet.orderReserveTimes(2), + sdk.NewInt64Coin(denom1, 2659), + sdk.NewInt64Coin(denom2, 375), + ), + } + }, + orders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("375e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id3", + BaseDenom: denom2, + QuoteDenom: denom1, + Price: lo.ToPtr(types.MustNewPriceFromString("266e-2")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id4", + BaseDenom: denom2, + QuoteDenom: denom1, + Price: lo.ToPtr(types.MustNewPriceFromString("2659e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }, + } + }, + wantOrders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id1", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("376e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(1000), + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id2", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("375e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(375), + }, + { + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id3", + BaseDenom: denom2, + QuoteDenom: denom1, + Price: lo.ToPtr(types.MustNewPriceFromString("266e-2")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(1000), + }, + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id4", + BaseDenom: denom2, + QuoteDenom: denom1, + Price: lo.ToPtr(types.MustNewPriceFromString("2659e-3")), + Quantity: sdkmath.NewInt(1000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(1000), + RemainingBalance: sdkmath.NewInt(2659), + }, + } + }, + wantAvailableBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{} + }, + }, + { + name: "match_limit_self_maker_sell_taker_buy_ten_orders", + balances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserveTimes(10), + sdk.NewInt64Coin(denom1, 100000), + ), + testSet.acc2.String(): sdk.NewCoins( + testSet.orderReserveTimes(1), + sdk.NewInt64Coin(denom2, 98991560000), + ), + } + }, + orders: func(testSet TestSet) []types.Order { + orders := make([]types.Order, 0) + for i := 0; i < 10; i++ { + orders = append(orders, types.Order{ + Creator: testSet.acc1.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: fmt.Sprintf("id%d", i), + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString(fmt.Sprintf("1%d1e-1", i))), + Quantity: sdkmath.NewInt(10_000), + Side: types.SIDE_SELL, + TimeInForce: types.TIME_IN_FORCE_GTC, + }) + } + orders = append(orders, types.Order{ + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id101", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("9999")), + Quantity: sdkmath.NewInt(10_000_000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + }) + return orders + }, + wantOrders: func(testSet TestSet) []types.Order { + return []types.Order{ + { + Creator: testSet.acc2.String(), + Type: types.ORDER_TYPE_LIMIT, + ID: "id101", + BaseDenom: denom1, + QuoteDenom: denom2, + Price: lo.ToPtr(types.MustNewPriceFromString("9999")), + Quantity: sdkmath.NewInt(10_000_000), + Side: types.SIDE_BUY, + TimeInForce: types.TIME_IN_FORCE_GTC, + RemainingQuantity: sdkmath.NewInt(9900000), + RemainingBalance: sdkmath.NewInt(98990100000), + }, + } + }, + wantAvailableBalances: func(testSet TestSet) map[string]sdk.Coins { + return map[string]sdk.Coins{ + testSet.acc1.String(): sdk.NewCoins( + testSet.orderReserveTimes(10), + sdk.NewInt64Coin(denom2, 1460000), + ), + testSet.acc2.String(): sdk.NewCoins( + sdk.NewInt64Coin(denom1, 100000), + ), + } + }, + }, } for _, tt := range tests { tt := tt @@ -5969,19 +6334,25 @@ func TestKeeper_MatchOrders(t *testing.T) { initialOrders := tt.orders(testSet) ordersDenoms := make(map[string]struct{}, 0) - for _, order := range initialOrders { + for i, order := range initialOrders { ordersDenoms[order.BaseDenom] = struct{}{} ordersDenoms[order.QuoteDenom] = struct{}{} availableBalancesBefore := getAvailableBalances(sdkCtx, testApp, sdk.MustAccAddressFromBech32(order.Creator)) // use new event manager for each order sdkCtx = sdkCtx.WithEventManager(sdk.NewEventManager()) + gasBefore := sdkCtx.GasMeter().GasConsumed() err := testApp.DEXKeeper.PlaceOrder(sdkCtx, order) if err != nil && tt.wantErrorContains != "" { - require.True(t, sdkerrors.IsOf(err, assetfttypes.ErrDEXLockFailed, assetfttypes.ErrWhitelistedLimitExceeded)) + require.True(t, sdkerrors.IsOf( + err, + assetfttypes.ErrDEXInsufficientSpendableBalance, assetfttypes.ErrWhitelistedLimitExceeded, + )) require.ErrorContains(t, err, tt.wantErrorContains) return } + gasAfter := sdkCtx.GasMeter().GasConsumed() + t.Logf("Used gas for order %d placement: %d", i, gasAfter-gasBefore) require.NoError(t, err) assertOrderPlacementResult(t, sdkCtx, testApp, availableBalancesBefore, order) orderBooksID, err := testApp.DEXKeeper.GetOrderBookIDByDenoms(sdkCtx, order.BaseDenom, order.QuoteDenom) diff --git a/x/dex/types/expected_keepers.go b/x/dex/types/expected_keepers.go index 057c34564..59b7a04d9 100644 --- a/x/dex/types/expected_keepers.go +++ b/x/dex/types/expected_keepers.go @@ -25,20 +25,8 @@ type AccountQueryServer interface { // AssetFTKeeper represents required methods of asset ft keeper. type AssetFTKeeper interface { - DEXIncreaseLimits(ctx sdk.Context, addr sdk.AccAddress, lockedCoin, expectedToReceiveCoin sdk.Coin) error - DEXDecreaseLimits(ctx sdk.Context, addr sdk.AccAddress, lockedCoin, expectedToReceiveCoin sdk.Coin) error - DEXDecreaseLimitsAndSend( - ctx sdk.Context, - fromAddr, toAddr sdk.AccAddress, - unlockAndSendCoin, decreaseExpectedToReceiveCoin sdk.Coin, - ) error - DEXCheckLimitsAndSend( - ctx sdk.Context, - fromAddr, toAddr sdk.AccAddress, - sendCoin, checkExpectedToReceiveCoin sdk.Coin, - ) error - DEXLock(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error - DEXUnlock(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error + DEXExecuteActions(ctx sdk.Context, actions dextypes.DEXActions) error + DEXDecreaseLimits(ctx sdk.Context, addr sdk.AccAddress, lockedCoin sdk.Coins, expectedToReceiveCoin sdk.Coin) error GetSpendableBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin GetDEXSettings(ctx sdk.Context, denom string) (dextypes.DEXSettings, error) ValidateDEXCancelOrdersByDenomIsAllowed(ctx sdk.Context, addr sdk.AccAddress, denom string) error diff --git a/x/wbank/keeper/keeper_test.go b/x/wbank/keeper/keeper_test.go index a65716232..7ab588ed1 100644 --- a/x/wbank/keeper/keeper_test.go +++ b/x/wbank/keeper/keeper_test.go @@ -148,7 +148,7 @@ func TestBaseKeeperWrapper_SpendableBalanceByDenom(t *testing.T) { // check that after the locking the spendable balance is different coinToLock := sdk.NewCoin(denom, sdkmath.NewInt(10)) - err = ftKeeper.DEXLock(ctx, recipient, coinToLock) + err = ftKeeper.DEXIncreaseLocked(ctx, recipient, coinToLock) requireT.NoError(err) spendableBalanceRes, err = bankKeeper.SpendableBalanceByDenom(ctx, &banktypes.QuerySpendableBalanceByDenomRequest{ Address: recipient.String(),