Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 387 additions & 0 deletions integration-tests/modules/dex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3046,3 +3046,390 @@ func assertBalance(
require.NoError(t, err)
require.Equal(t, sdkmath.NewInt(expectedBalance).String(), acc1Denom2BalanceRes.Balance.Amount.String())
}

// TestFrozenTokenEscapeViaDEX verifies that frozen tokens cannot be DEX-locked
// or transferred via DEX settlement.
//
// This is the integration test counterpart of TestKeeper_FrozenTokenEscapeViaDEX.
// If the fix were missing: placing multiple sell orders could lock frozen tokens
// (DEXIncreaseLocked had no frozen check), and DEX settlement would transfer them
// via raw bank keeper. This test asserts the fix: the second order (which would
// lock frozen tokens) must be rejected.
func TestFrozenTokenEscapeViaDEX(t *testing.T) {
t.Parallel()
ctx, chain := integrationtests.NewTXChainTestingContext(t)

requireT := require.New(t)
assetFTClient := assetfttypes.NewQueryClient(chain.ClientContext)
dexClient := dextypes.NewQueryClient(chain.ClientContext)

dexParamsRes, err := dexClient.Params(ctx, &dextypes.QueryParamsRequest{})
requireT.NoError(err)

// ========================================================================
// SETUP: Create token with Feature_freezing, fund Alice, freeze tokens
// ========================================================================

// Issue base token with freezing feature (the token Alice sells)
// issuer already has the initial amount from issuance
issuer, denom := genAccountAndIssueFT(
ctx, t, chain, 1_000_000, sdkmath.NewInt(1_000_000), assetfttypes.Feature_freezing,
)

// Issue quote token (the token Bob uses to buy) - issuer issues both tokens
quoteDenom := issueFT(ctx, t, chain, issuer, sdkmath.NewInt(1_000_000))

alice := chain.GenAccount()
bob := chain.GenAccount()

// Fund accounts with necessary messages and amounts
chain.FundAccountsWithOptions(ctx, t, []integration.AccWithBalancesOptions{
{
Acc: issuer,
Options: integration.BalancesOptions{
Messages: []sdk.Msg{
&banktypes.MsgSend{},
&banktypes.MsgSend{},
&assetfttypes.MsgFreeze{},
},
},
},
{
Acc: alice,
Options: integration.BalancesOptions{
Amount: dexParamsRes.Params.OrderReserve.Amount.MulRaw(3).Add(sdkmath.NewInt(100_000)),
},
},
{
Acc: bob,
Options: integration.BalancesOptions{
Amount: dexParamsRes.Params.OrderReserve.Amount.Add(sdkmath.NewInt(100_000)),
},
},
})

// Fund Alice with 1,000,000 base tokens (100 * 10,000 to meet quantity step requirement)
aliceBaseAmount := int64(1_000_000)
sendMsg := &banktypes.MsgSend{
FromAddress: issuer.String(),
ToAddress: alice.String(),
Amount: sdk.NewCoins(sdk.NewInt64Coin(denom, aliceBaseAmount)),
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg)),
sendMsg,
)
requireT.NoError(err)

// Fund Bob with 1,000,000 quote tokens (for buying)
bobQuoteAmount := int64(1_000_000)
bobSendMsg := &banktypes.MsgSend{
FromAddress: issuer.String(),
ToAddress: bob.String(),
Amount: sdk.NewCoins(sdk.NewInt64Coin(quoteDenom, bobQuoteAmount)),
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(bobSendMsg)),
bobSendMsg,
)
requireT.NoError(err)

// Admin freezes 600,000 of Alice's base tokens (60 * 10,000)
frozenAmount := int64(600_000)
freezeMsg := &assetfttypes.MsgFreeze{
Sender: issuer.String(),
Account: alice.String(),
Coin: sdk.NewInt64Coin(denom, frozenAmount),
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(freezeMsg)),
freezeMsg,
)
requireT.NoError(err)

// Verify initial state
balanceRes, err := assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: alice.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(aliceBaseAmount, balanceRes.Balance.Int64(), "Alice should have 1,000,000 tokens")
requireT.Equal(frozenAmount, balanceRes.Frozen.Int64(), "Alice should have 600,000 frozen tokens")
spendableAmount := aliceBaseAmount - frozenAmount // 400,000
spendable := balanceRes.Balance.Sub(balanceRes.Frozen).Sub(balanceRes.LockedInDEX)
requireT.Equal(spendableAmount, spendable.Int64(), "Alice should have 400,000 spendable tokens")

t.Logf("=== Initial state: balance=%d, frozen=%d, dexLocked=0, spendable=%d ===",
aliceBaseAmount, frozenAmount, spendableAmount)

// ========================================================================
// PHASE 1: Verify that frozen tokens cannot be DEX-locked via orders
//
// If the fix were missing: we could place multiple sell orders that together
// lock more than spendable (400k + 400k + 200k = 1M, but only 400k spendable).
// Each order would pass because validation checked (balance - dexLocked)
// without subtracting frozen. The fix ensures only spendable tokens can be locked.
// ========================================================================

// Step 1: Place first sell order for 400,000 tokens (the unfrozen portion) — must succeed
order1Amount := int64(400_000) // 40 * 10,000
placeOrder1 := &dextypes.MsgPlaceOrder{
Sender: alice.String(),
Type: dextypes.ORDER_TYPE_LIMIT,
ID: "order1",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")),
Quantity: sdkmath.NewInt(order1Amount),
Side: dextypes.SIDE_SELL,
TimeInForce: dextypes.TIME_IN_FORCE_GTC,
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(alice),
chain.TxFactoryAuto(),
placeOrder1,
)
requireT.NoError(err, "First order of 400,000 should pass (400,000 unfrozen available)")

balanceRes, err = assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: alice.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(order1Amount, balanceRes.LockedInDEX.Int64(), "400,000 tokens should be DEX-locked")
t.Logf("Step 1: Placed order for %d tokens. dexLocked=%d", order1Amount, order1Amount)

// Step 2: Place second sell order for 400,000 MORE tokens — must FAIL
// Available spendable = 1,000,000 - 400,000 (dexLocked) - 600,000 (frozen) = 0.
// If the fix were missing, (balance - dexLocked) = 600,000 >= 400,000 would incorrectly pass.
order2Amount := int64(400_000) // 40 * 10,000
placeOrder2 := &dextypes.MsgPlaceOrder{
Sender: alice.String(),
Type: dextypes.ORDER_TYPE_LIMIT,
ID: "order2",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")),
Quantity: sdkmath.NewInt(order2Amount),
Side: dextypes.SIDE_SELL,
TimeInForce: dextypes.TIME_IN_FORCE_GTC,
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(alice),
chain.TxFactoryAuto(),
placeOrder2,
)
requireT.Error(err, "Second order of 400,000 must fail: it would lock frozen tokens; available spendable is 0")
t.Logf("Step 2: Second order of %d correctly rejected — frozen tokens cannot be DEX-locked", order2Amount)

// Step 2b: Try to lock 100,000 more — must FAIL (smaller portion of frozen)
order2bAmount := int64(100_000)
placeOrder2b := &dextypes.MsgPlaceOrder{
Sender: alice.String(),
Type: dextypes.ORDER_TYPE_LIMIT,
ID: "order2b",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")),
Quantity: sdkmath.NewInt(order2bAmount),
Side: dextypes.SIDE_SELL,
TimeInForce: dextypes.TIME_IN_FORCE_GTC,
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(alice),
chain.TxFactoryAuto(),
placeOrder2b,
)
requireT.Error(err, "Order of 100,000 must fail: it would lock frozen tokens; available spendable is 0")
t.Logf("Step 2b: Order of %d correctly rejected — no portion of frozen tokens can be DEX-locked", order2bAmount)

// Step 2c: Try to lock 10,000 more — must FAIL (even smaller portion of frozen)
order2cAmount := int64(10_000)
placeOrder2c := &dextypes.MsgPlaceOrder{
Sender: alice.String(),
Type: dextypes.ORDER_TYPE_LIMIT,
ID: "order2c",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")),
Quantity: sdkmath.NewInt(order2cAmount),
Side: dextypes.SIDE_SELL,
TimeInForce: dextypes.TIME_IN_FORCE_GTC,
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(alice),
chain.TxFactoryAuto(),
placeOrder2c,
)
requireT.Error(err, "Order of 10,000 must fail: it would lock frozen tokens; available spendable is 0")
t.Logf("Step 2c: Order of %d correctly rejected — no portion of frozen tokens can be DEX-locked", order2cAmount)

// Verify state unchanged: still 400,000 DEX-locked, 600,000 frozen
balanceRes, err = assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: alice.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(order1Amount, balanceRes.LockedInDEX.Int64(), "DEX-locked should remain 400,000")
requireT.Equal(frozenAmount, balanceRes.Frozen.Int64(), "Frozen should remain 600,000")
}

// TestFrozenTokenFreezeAfterOrderPlacement verifies that an order placed when the user had
// sufficient balance still executes after the issuer freezes tokens.
//
// Scenario: user has 100 TKN, places a sell order for 80 TKN, issuer freezes 20 TKN,
// then the order is matched. The order must be executed because it was valid at placement time.
// The fix must be at order placement only (reject locking frozen tokens); execution must
// not block orders that were legitimately placed before the freeze.
func TestFrozenTokenFreezeAfterOrderPlacement(t *testing.T) {
t.Parallel()
ctx, chain := integrationtests.NewTXChainTestingContext(t)

requireT := require.New(t)
assetFTClient := assetfttypes.NewQueryClient(chain.ClientContext)
dexClient := dextypes.NewQueryClient(chain.ClientContext)
bankClient := banktypes.NewQueryClient(chain.ClientContext)

dexParamsRes, err := dexClient.Params(ctx, &dextypes.QueryParamsRequest{})
requireT.NoError(err)

// Setup: token with freezing, Alice has 100 TKN (1,000,000), Bob has quote
issuer, denom := genAccountAndIssueFT(
ctx, t, chain, 1_000_000, sdkmath.NewInt(1_000_000), assetfttypes.Feature_freezing,
)
quoteDenom := issueFT(ctx, t, chain, issuer, sdkmath.NewInt(1_000_000))

alice := chain.GenAccount()
bob := chain.GenAccount()

chain.FundAccountsWithOptions(ctx, t, []integration.AccWithBalancesOptions{
{
Acc: issuer,
Options: integration.BalancesOptions{
Messages: []sdk.Msg{
&banktypes.MsgSend{},
&banktypes.MsgSend{},
&assetfttypes.MsgFreeze{},
},
},
},
{
Acc: alice,
Options: integration.BalancesOptions{
Amount: dexParamsRes.Params.OrderReserve.Amount.MulRaw(3).Add(sdkmath.NewInt(100_000)),
},
},
{
Acc: bob,
Options: integration.BalancesOptions{
Amount: dexParamsRes.Params.OrderReserve.Amount.Add(sdkmath.NewInt(100_000)),
},
},
})

aliceBaseAmount := int64(1_000_000) // 100 TKN
sendToAlice := &banktypes.MsgSend{
FromAddress: issuer.String(),
ToAddress: alice.String(),
Amount: sdk.NewCoins(sdk.NewInt64Coin(denom, aliceBaseAmount)),
}
_, err = client.BroadcastTx(ctx, chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendToAlice)), sendToAlice)
requireT.NoError(err)

bobQuoteAmount := int64(1_000_000)
sendToBob := &banktypes.MsgSend{
FromAddress: issuer.String(),
ToAddress: bob.String(),
Amount: sdk.NewCoins(sdk.NewInt64Coin(quoteDenom, bobQuoteAmount)),
}
_, err = client.BroadcastTx(ctx, chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendToBob)), sendToBob)
requireT.NoError(err)

// Step 1: Alice places sell order for 80 TKN (800,000) — no freeze yet, all 100 available
orderAmount := int64(800_000)
placeSell := &dextypes.MsgPlaceOrder{
Sender: alice.String(),
Type: dextypes.ORDER_TYPE_LIMIT,
ID: "alice-sell",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Price: lo.ToPtr(dextypes.MustNewPriceFromString("1")),
Quantity: sdkmath.NewInt(orderAmount),
Side: dextypes.SIDE_SELL,
TimeInForce: dextypes.TIME_IN_FORCE_GTC,
}
_, err = client.BroadcastTx(ctx, chain.ClientContext.WithFromAddress(alice),
chain.TxFactoryAuto(), placeSell)
requireT.NoError(err, "Alice's sell order must pass when she has 100 TKN available")

balanceRes, err := assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: alice.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(orderAmount, balanceRes.LockedInDEX.Int64(), "800,000 should be DEX-locked")

// Step 2: Issuer freezes 20 TKN (200,000) of Alice's balance
freezeAmount := int64(200_000)
freezeMsg := &assetfttypes.MsgFreeze{
Sender: issuer.String(),
Account: alice.String(),
Coin: sdk.NewInt64Coin(denom, freezeAmount),
}
_, err = client.BroadcastTx(ctx, chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(freezeMsg)), freezeMsg)
requireT.NoError(err)

// Step 3: Bob places buy order — should match Alice's sell
placeBuy := &dextypes.MsgPlaceOrder{
Sender: bob.String(),
Type: dextypes.ORDER_TYPE_MARKET,
ID: "bob-buy",
BaseDenom: denom,
QuoteDenom: quoteDenom,
Quantity: sdkmath.NewInt(orderAmount),
Side: dextypes.SIDE_BUY,
TimeInForce: dextypes.TIME_IN_FORCE_IOC,
}
_, err = client.BroadcastTx(ctx, chain.ClientContext.WithFromAddress(bob),
chain.TxFactoryAuto(), placeBuy)
requireT.NoError(err, "Bob's buy order must match and execute — order was valid at placement time")

// Step 4: Verify order executed
aliceBalanceRes, err := assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: alice.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(aliceBaseAmount-orderAmount, aliceBalanceRes.Balance.Int64(),
"Alice should have 200,000 base tokens after selling 800,000")
requireT.Equal(int64(0), aliceBalanceRes.LockedInDEX.Int64(), "Alice's DEX lock should be released")

bobBalanceRes, err := assetFTClient.Balance(ctx, &assetfttypes.QueryBalanceRequest{
Account: bob.String(),
Denom: denom,
})
requireT.NoError(err)
requireT.Equal(orderAmount, bobBalanceRes.Balance.Int64(),
"Bob should have received 800,000 base tokens")

aliceQuoteRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{
Address: alice.String(),
Denom: quoteDenom,
})
requireT.NoError(err)
requireT.True(aliceQuoteRes.Balance.Amount.GTE(sdkmath.NewInt(orderAmount)),
"Alice should have received quote tokens from Bob")
}
Loading
Loading