From 2455d6ea6e140424fdf5d994f04add3d83912e4c Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Thu, 12 Mar 2026 11:24:33 +0700 Subject: [PATCH 1/4] chore: Buy orders match cross price with sell orders --- .../migrations/032-order-book-actions.sql | 143 ++++++++----- .../order_book/matching_engine_test.go | 200 +++++++++++++++++- 2 files changed, 289 insertions(+), 54 deletions(-) diff --git a/internal/migrations/032-order-book-actions.sql b/internal/migrations/032-order-book-actions.sql index ca256c3b..dd554c5c 100644 --- a/internal/migrations/032-order-book-actions.sql +++ b/internal/migrations/032-order-book-actions.sql @@ -416,34 +416,35 @@ PUBLIC VIEW RETURNS (market_exists BOOLEAN) { */ -- ============================================================================= --- match_direct: Direct match implementation +-- match_direct: Direct match implementation with price-crossing -- ============================================================================= /** - * Matches buy and sell orders at the same price for the same outcome. + * Matches overlapping buy and sell orders for the same outcome. + * Supports price-crossing: a buy@52 can match a sell@51 (standard order book behavior). * - * Example: Buy YES @ $0.56 matches Sell YES @ $0.56 + * Price-Crossing Semantics: + * - A match occurs when the best buy price >= the best sell price + * - The match executes at the SELL price (buyer gets price improvement) + * - If buy_price > sell_price, the buyer is refunded the difference + * - Example: Buy YES @ $0.52 matches Sell YES @ $0.51 + * → Seller receives $0.51/share, buyer is refunded $0.01/share * * Collateral Flow: - * - Buyer's locked collateral is transferred to seller as payment + * - Seller receives: match_amount × sell_price × 10^16 + * - Buyer refund (if price improvement): match_amount × (buy_price - sell_price) × 10^16 * - Shares transfer from seller's holdings to buyer's holdings * * Recursion Behavior: * - Uses tail recursion to process multiple matches sequentially * - Each iteration matches one order pair and removes/reduces them from ob_positions - * - Recursion depth = number of orders matched at this price level - * - Natural termination when no more matching orders exist (LIMIT 1 returns nothing) + * - Natural termination when no more overlapping orders exist * - Maximum depth is bounded by the number of orders in the order book - * - In practice, depth rarely exceeds 10-20 due to: - * * Economic constraints (gas costs for creating many small orders) - * * Market maker behavior (traders prefer larger consolidated orders) - * * Natural order book dynamics - * - Worst case: One large order matching 100+ tiny orders requires 100+ separate - * transactions to create those orders, making it economically impractical * * Parameters: * - $query_id: Market identifier * - $outcome: TRUE (YES) or FALSE (NO) - * - $price: Positive price (1-99 cents) + * - $price: Unused (kept for call-site compatibility). Sweep finds best overlap. + * - $bridge: Bridge identifier for collateral operations */ CREATE OR REPLACE ACTION match_direct( $query_id INT, @@ -451,20 +452,48 @@ CREATE OR REPLACE ACTION match_direct( $price INT, $bridge TEXT ) PRIVATE { - -- Get first buy order (FIFO: earliest first) + -- Find the cheapest sell order (best ask) + $sell_price INT; + $sell_participant_id INT; + $sell_amount INT8; + $sell_found BOOL := false; + + for $sell_order in + SELECT price, participant_id, amount + FROM ob_positions + WHERE query_id = $query_id + AND outcome = $outcome + AND price > 0 -- Sell orders have positive price + ORDER BY price ASC, last_updated ASC -- Cheapest first, then FIFO + LIMIT 1 + { + $sell_price := $sell_order.price; + $sell_participant_id := $sell_order.participant_id; + $sell_amount := $sell_order.amount; + $sell_found := true; + } + + -- No sell order, exit + if NOT $sell_found { + RETURN; + } + + -- Find the most expensive buy order (best bid) + $buy_price_neg INT; $buy_participant_id INT; $buy_amount INT8; $buy_found BOOL := false; for $buy_order in - SELECT participant_id, amount + SELECT price, participant_id, amount FROM ob_positions WHERE query_id = $query_id AND outcome = $outcome - AND price = -$price -- Buy orders have negative price - ORDER BY last_updated ASC -- FIFO + AND price < 0 -- Buy orders have negative price + ORDER BY price ASC, last_updated ASC -- Most negative first = highest buy price, then FIFO LIMIT 1 { + $buy_price_neg := $buy_order.price; $buy_participant_id := $buy_order.participant_id; $buy_amount := $buy_order.amount; $buy_found := true; @@ -475,27 +504,11 @@ CREATE OR REPLACE ACTION match_direct( RETURN; } - -- Get first sell order (FIFO: earliest first) - $sell_participant_id INT; - $sell_amount INT8; - $sell_found BOOL := false; + -- Convert negative stored price to positive buy price + $buy_price INT := 0 - $buy_price_neg; - for $sell_order in - SELECT participant_id, amount - FROM ob_positions - WHERE query_id = $query_id - AND outcome = $outcome - AND price = $price -- Sell orders have positive price - ORDER BY last_updated ASC -- FIFO - LIMIT 1 - { - $sell_participant_id := $sell_order.participant_id; - $sell_amount := $sell_order.amount; - $sell_found := true; - } - - -- No sell order, exit - if NOT $sell_found { + -- Check price crossing: buy price must be >= sell price for a match + if $buy_price < $sell_price { RETURN; } @@ -519,27 +532,51 @@ CREATE OR REPLACE ACTION match_direct( } $seller_wallet_address TEXT := '0x' || encode($seller_wallet_bytes, 'hex'); - -- Calculate payment to seller + -- Calculate payment to seller (at sell price) $multiplier NUMERIC(78, 0) := '10000000000000000'::NUMERIC(78, 0); $seller_payment NUMERIC(78, 0) := ($match_amount::NUMERIC(78, 0) * - $price::NUMERIC(78, 0) * + $sell_price::NUMERIC(78, 0) * $multiplier); -- Transfer payment from vault to seller ob_unlock_collateral($bridge, $seller_wallet_address, $seller_payment); - -- Record impacts for P&L + -- Record sell impact for P&L ob_record_tx_impact($sell_participant_id, $outcome, -$match_amount, $seller_payment, FALSE); - ob_record_tx_impact($buy_participant_id, $outcome, $match_amount, 0::NUMERIC(78,0), FALSE); + + -- Handle buyer price improvement refund + $price_diff INT := $buy_price - $sell_price; + + -- Get buyer's wallet address (needed for potential refund) + $buyer_wallet_bytes BYTEA; + for $row in SELECT wallet_address FROM ob_participants WHERE id = $buy_participant_id { + $buyer_wallet_bytes := $row.wallet_address; + } + $buyer_wallet_address TEXT := '0x' || encode($buyer_wallet_bytes, 'hex'); + + if $price_diff > 0 { + -- Buyer locked collateral at buy_price but match executes at sell_price + -- Refund the difference: match_amount × (buy_price - sell_price) × 10^16 + $buyer_refund NUMERIC(78, 0) := ($match_amount::NUMERIC(78, 0) * + $price_diff::NUMERIC(78, 0) * + $multiplier); + ob_unlock_collateral($bridge, $buyer_wallet_address, $buyer_refund); + + -- Record buy impact with refund + ob_record_tx_impact($buy_participant_id, $outcome, $match_amount, $buyer_refund, FALSE); + } else { + -- No price improvement (exact price match) + ob_record_tx_impact($buy_participant_id, $outcome, $match_amount, 0::NUMERIC(78,0), FALSE); + } -- Transfer shares from seller to buyer -- Step 1: Delete fully matched orders FIRST (prevents amount=0 constraint violation) DELETE FROM ob_positions WHERE query_id = $query_id AND ((participant_id = $sell_participant_id AND outcome = $outcome - AND price = $price AND amount = $match_amount) + AND price = $sell_price AND amount = $match_amount) OR (participant_id = $buy_participant_id AND outcome = $outcome - AND price = -$price AND amount = $match_amount)); + AND price = $buy_price_neg AND amount = $match_amount)); -- Step 2: Reduce seller's sell order (only if partial fill) UPDATE ob_positions @@ -547,7 +584,7 @@ CREATE OR REPLACE ACTION match_direct( WHERE query_id = $query_id AND participant_id = $sell_participant_id AND outcome = $outcome - AND price = $price + AND price = $sell_price AND amount > $match_amount; -- Step 3: Add shares to buyer's holdings (price = 0) @@ -563,10 +600,10 @@ CREATE OR REPLACE ACTION match_direct( WHERE query_id = $query_id AND participant_id = $buy_participant_id AND outcome = $outcome - AND price = -$price + AND price = $buy_price_neg AND amount > $match_amount; - -- Recursively call to match next orders + -- Recursively call to match next overlapping orders match_direct($query_id, $outcome, $price, $bridge); }; @@ -904,14 +941,14 @@ CREATE OR REPLACE ACTION match_burn( * * Called automatically after every order placement to attempt matching. * Tries all three match types in sequence: - * 1. Direct match (most common, fastest) - * 2. Mint match (creates liquidity) - * 3. Burn match (removes liquidity) + * 1. Direct match with price-crossing (buy_price >= sell_price) + * 2. Mint match (creates liquidity, exact complementary prices) + * 3. Burn match (removes liquidity, exact complementary prices) * * Parameters: * - $query_id: Market identifier * - $outcome: TRUE (YES) or FALSE (NO) - * - $price: Price level that triggered matching (1-99) + * - $price: Price of the triggering order (used for mint/burn complementary calc) */ CREATE OR REPLACE ACTION match_orders( $query_id INT, @@ -936,10 +973,10 @@ CREATE OR REPLACE ACTION match_orders( } -- ========================================================================== - -- SECTION 2: TRY DIRECT MATCH + -- SECTION 2: TRY DIRECT MATCH (with price-crossing) -- ========================================================================== - -- Match buy and sell orders at the same price for same outcome - -- This is the most common match type and should be tried first + -- Sweeps to find the best overlapping buy/sell pair (buy_price >= sell_price) + -- Match executes at sell price; buyer is refunded price difference match_direct($query_id, $outcome, $price, $bridge); diff --git a/tests/streams/order_book/matching_engine_test.go b/tests/streams/order_book/matching_engine_test.go index bfc11c0d..3781349f 100644 --- a/tests/streams/order_book/matching_engine_test.go +++ b/tests/streams/order_book/matching_engine_test.go @@ -79,7 +79,11 @@ func TestMatchingEngine(t *testing.T) { // Category D: Multiple Round Tests testDirectMatchMultipleRounds(t), - // Category E: Edge Cases + // Category E: Price-Crossing Tests + testDirectMatchPriceCrossing(t), + testDirectMatchPriceCrossingSweep(t), + + // Category F: Edge Cases testNoMatchingOrders(t), }, }, testutils.GetTestOptionsWithCache()) @@ -736,3 +740,197 @@ func testNoMatchingOrders(t *testing.T) func(context.Context, *kwilTesting.Platf return nil } } + +// ============================================================================= +// Category E: Price-Crossing Tests +// ============================================================================= + +// testDirectMatchPriceCrossing tests that buy@52 matches sell@51 (price-crossing) +// The match executes at sell price and buyer is refunded the difference. +func testDirectMatchPriceCrossing(t *testing.T) func(context.Context, *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + lastBalancePoint = nil + lastTrufBalancePoint = nil + + err := erc20bridge.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err) + + buyer := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000004") + seller := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000005") + + err = giveBalanceChained(ctx, platform, buyer.Address(), "500000000000000000000") + require.NoError(t, err) + err = giveBalanceChained(ctx, platform, seller.Address(), "500000000000000000000") + require.NoError(t, err) + + // Create market + queryComponents, err := encodeQueryComponentsForTests(buyer.Address(), "sttest00000000000000000000000044", "get_record", []byte{0x01}) + require.NoError(t, err) + settleTime := time.Now().Add(24 * time.Hour).Unix() + var marketID int64 + err = callCreateMarket(ctx, platform, &buyer, queryComponents, settleTime, 5, 20, func(row *common.Row) error { + marketID = row.Values[0].(int64) + return nil + }) + require.NoError(t, err) + + // Seller: Create shares and place sell@51 + err = callPlaceSplitLimitOrder(ctx, platform, &seller, int(marketID), 51, 100) + require.NoError(t, err) + err = callPlaceSellOrder(ctx, platform, &seller, int(marketID), true, 51, 100) + require.NoError(t, err) + + // Record buyer's USDC balance before buy + buyerBalanceBefore, err := getUSDCBalance(ctx, platform, buyer.Address()) + require.NoError(t, err) + + // Buyer: Buy YES @ $0.52 — should cross and match sell@51 + err = callPlaceBuyOrder(ctx, platform, &buyer, int(marketID), true, 52, 50) + require.NoError(t, err) + + // Verify: buyer should have 50 YES holdings + positions, err := getPositions(ctx, platform, int(marketID)) + require.NoError(t, err) + + // Seller acts first (participant 1), buyer acts second (participant 2) + var buyerYesHoldings *Position + var sellerSellRemaining *Position + for i := range positions { + if positions[i].ParticipantID == 2 && positions[i].Outcome && positions[i].Price == 0 { + buyerYesHoldings = &positions[i] + } + if positions[i].ParticipantID == 1 && positions[i].Outcome && positions[i].Price == 51 { + sellerSellRemaining = &positions[i] + } + } + + require.NotNil(t, buyerYesHoldings, "Buyer should have YES holdings from price-crossing match") + require.Equal(t, int64(50), buyerYesHoldings.Amount, "Buyer should have 50 YES shares") + + // Seller's sell order should be partially filled (100 - 50 = 50 remaining) + require.NotNil(t, sellerSellRemaining, "Seller should have remaining sell order") + require.Equal(t, int64(50), sellerSellRemaining.Amount, "Seller should have 50 remaining") + + // No buy order should remain (fully matched) + hasBuyOrders := false + for i := range positions { + if positions[i].Price < 0 && positions[i].Outcome { + hasBuyOrders = true + } + } + require.False(t, hasBuyOrders, "Buy order should be fully consumed") + + // Verify buyer got price improvement refund + // Buyer locked: 50 × 52 × 10^16 = 26 × 10^18 (26 TRUF) + // Seller received: 50 × 51 × 10^16 = 25.5 × 10^18 (25.5 TRUF) + // Buyer refund: 50 × 1 × 10^16 = 0.5 × 10^18 (0.5 TRUF) + // Net cost to buyer: 25.5 TRUF (not 26 TRUF) + buyerBalanceAfter, err := getUSDCBalance(ctx, platform, buyer.Address()) + require.NoError(t, err) + + // Balance decrease = amount actually paid = 50 shares × $0.51 = 25.5 TRUF + balanceDecrease := new(big.Int).Sub(buyerBalanceBefore, buyerBalanceAfter) + expectedCost := new(big.Int).Mul(big.NewInt(50*51), new(big.Int).Exp(big.NewInt(10), big.NewInt(16), nil)) + require.Equal(t, expectedCost.String(), balanceDecrease.String(), + "Buyer should pay at sell price (51), not buy price (52)") + + return nil + } +} + +// testDirectMatchPriceCrossingSweep tests sweeping across multiple price levels +// Buy@55 should match sell@48, sell@50, sell@52 (cheapest first) +func testDirectMatchPriceCrossingSweep(t *testing.T) func(context.Context, *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + lastBalancePoint = nil + lastTrufBalancePoint = nil + + err := erc20bridge.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err) + + buyer := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000006") + seller1 := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000007") + seller2 := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000008") + seller3 := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000000009") + + err = giveBalanceChained(ctx, platform, buyer.Address(), "500000000000000000000") + require.NoError(t, err) + err = giveBalanceChained(ctx, platform, seller1.Address(), "500000000000000000000") + require.NoError(t, err) + err = giveBalanceChained(ctx, platform, seller2.Address(), "500000000000000000000") + require.NoError(t, err) + err = giveBalanceChained(ctx, platform, seller3.Address(), "500000000000000000000") + require.NoError(t, err) + + // Create market + queryComponents, err := encodeQueryComponentsForTests(buyer.Address(), "sttest00000000000000000000000045", "get_record", []byte{0x01}) + require.NoError(t, err) + settleTime := time.Now().Add(24 * time.Hour).Unix() + var marketID int64 + err = callCreateMarket(ctx, platform, &buyer, queryComponents, settleTime, 5, 20, func(row *common.Row) error { + marketID = row.Values[0].(int64) + return nil + }) + require.NoError(t, err) + + // Three sellers at different prices + // Seller1: sell YES @ 48 (30 shares) + err = callPlaceSplitLimitOrder(ctx, platform, &seller1, int(marketID), 48, 30) + require.NoError(t, err) + err = callPlaceSellOrder(ctx, platform, &seller1, int(marketID), true, 48, 30) + require.NoError(t, err) + + // Seller2: sell YES @ 50 (40 shares) + err = callPlaceSplitLimitOrder(ctx, platform, &seller2, int(marketID), 50, 40) + require.NoError(t, err) + err = callPlaceSellOrder(ctx, platform, &seller2, int(marketID), true, 50, 40) + require.NoError(t, err) + + // Seller3: sell YES @ 52 (50 shares) + err = callPlaceSplitLimitOrder(ctx, platform, &seller3, int(marketID), 52, 50) + require.NoError(t, err) + err = callPlaceSellOrder(ctx, platform, &seller3, int(marketID), true, 52, 50) + require.NoError(t, err) + + // Buyer: Buy 100 YES @ $0.55 — should sweep sell@48 (30), sell@50 (40), sell@52 (30 of 50) + err = callPlaceBuyOrder(ctx, platform, &buyer, int(marketID), true, 55, 100) + require.NoError(t, err) + + // Verify positions + positions, err := getPositions(ctx, platform, int(marketID)) + require.NoError(t, err) + + // Participant order: seller1=1, seller2=2, seller3=3, buyer=4 + var buyerYesHoldings *Position + for i := range positions { + if positions[i].ParticipantID == 4 && positions[i].Outcome && positions[i].Price == 0 { + buyerYesHoldings = &positions[i] + } + } + + require.NotNil(t, buyerYesHoldings, "Buyer should have YES holdings from sweep") + require.Equal(t, int64(100), buyerYesHoldings.Amount, + "Buyer should have 100 YES shares (30@48 + 40@50 + 30@52)") + + // Seller3 should have 20 remaining (50 - 30 = 20) + var seller3Remaining *Position + for i := range positions { + if positions[i].ParticipantID == 3 && positions[i].Outcome && positions[i].Price == 52 { + seller3Remaining = &positions[i] + } + } + require.NotNil(t, seller3Remaining, "Seller3 should have remaining sell order") + require.Equal(t, int64(20), seller3Remaining.Amount, "Seller3 should have 20 remaining at price 52") + + // No buy orders should remain + hasBuyOrders := false + for i := range positions { + if positions[i].Price < 0 && positions[i].Outcome { + hasBuyOrders = true + } + } + require.False(t, hasBuyOrders, "Buy order should be fully consumed by sweep") + + return nil + } +} From 8694008096da2773479e0b87a9efa0ce4569bf1a Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Thu, 12 Mar 2026 13:05:09 +0700 Subject: [PATCH 2/4] chore: apply suggestion --- .../order_book/matching_engine_test.go | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/tests/streams/order_book/matching_engine_test.go b/tests/streams/order_book/matching_engine_test.go index 3781349f..e7384a47 100644 --- a/tests/streams/order_book/matching_engine_test.go +++ b/tests/streams/order_book/matching_engine_test.go @@ -873,26 +873,31 @@ func testDirectMatchPriceCrossingSweep(t *testing.T) func(context.Context, *kwil }) require.NoError(t, err) - // Three sellers at different prices - // Seller1: sell YES @ 48 (30 shares) - err = callPlaceSplitLimitOrder(ctx, platform, &seller1, int(marketID), 48, 30) - require.NoError(t, err) - err = callPlaceSellOrder(ctx, platform, &seller1, int(marketID), true, 48, 30) - require.NoError(t, err) - - // Seller2: sell YES @ 50 (40 shares) + // Place sellers OUT OF price order to prove price-priority matching + // Seller2: sell YES @ 50 (40 shares) — placed FIRST err = callPlaceSplitLimitOrder(ctx, platform, &seller2, int(marketID), 50, 40) require.NoError(t, err) err = callPlaceSellOrder(ctx, platform, &seller2, int(marketID), true, 50, 40) require.NoError(t, err) - // Seller3: sell YES @ 52 (50 shares) + // Seller3: sell YES @ 52 (50 shares) — placed SECOND err = callPlaceSplitLimitOrder(ctx, platform, &seller3, int(marketID), 52, 50) require.NoError(t, err) err = callPlaceSellOrder(ctx, platform, &seller3, int(marketID), true, 52, 50) require.NoError(t, err) + // Seller1: sell YES @ 48 (30 shares) — placed LAST (cheapest, but latest) + err = callPlaceSplitLimitOrder(ctx, platform, &seller1, int(marketID), 48, 30) + require.NoError(t, err) + err = callPlaceSellOrder(ctx, platform, &seller1, int(marketID), true, 48, 30) + require.NoError(t, err) + + // Record buyer's balance before the buy + buyerBalanceBefore, err := getUSDCBalance(ctx, platform, buyer.Address()) + require.NoError(t, err) + // Buyer: Buy 100 YES @ $0.55 — should sweep sell@48 (30), sell@50 (40), sell@52 (30 of 50) + // Price-priority: cheapest first regardless of insertion order err = callPlaceBuyOrder(ctx, platform, &buyer, int(marketID), true, 55, 100) require.NoError(t, err) @@ -900,7 +905,7 @@ func testDirectMatchPriceCrossingSweep(t *testing.T) func(context.Context, *kwil positions, err := getPositions(ctx, platform, int(marketID)) require.NoError(t, err) - // Participant order: seller1=1, seller2=2, seller3=3, buyer=4 + // Participant order: seller2=1, seller3=2, seller1=3, buyer=4 var buyerYesHoldings *Position for i := range positions { if positions[i].ParticipantID == 4 && positions[i].Outcome && positions[i].Price == 0 { @@ -912,10 +917,10 @@ func testDirectMatchPriceCrossingSweep(t *testing.T) func(context.Context, *kwil require.Equal(t, int64(100), buyerYesHoldings.Amount, "Buyer should have 100 YES shares (30@48 + 40@50 + 30@52)") - // Seller3 should have 20 remaining (50 - 30 = 20) + // Seller3 (pid=2) should have 20 remaining (50 - 30 = 20) var seller3Remaining *Position for i := range positions { - if positions[i].ParticipantID == 3 && positions[i].Outcome && positions[i].Price == 52 { + if positions[i].ParticipantID == 2 && positions[i].Outcome && positions[i].Price == 52 { seller3Remaining = &positions[i] } } @@ -931,6 +936,17 @@ func testDirectMatchPriceCrossingSweep(t *testing.T) func(context.Context, *kwil } require.False(t, hasBuyOrders, "Buy order should be fully consumed by sweep") + // Verify buyer's balance delta = cost at execution prices (best-price execution) + // 30@48 + 40@50 + 30@52 = 1440 + 2000 + 1560 = 5000 (in cents) + // 5000 × 10^16 = 5 × 10^19 wei + buyerBalanceAfter, err := getUSDCBalance(ctx, platform, buyer.Address()) + require.NoError(t, err) + + balanceDecrease := new(big.Int).Sub(buyerBalanceBefore, buyerBalanceAfter) + expectedCost := new(big.Int).Mul(big.NewInt(5000), new(big.Int).Exp(big.NewInt(10), big.NewInt(16), nil)) + require.Equal(t, expectedCost.String(), balanceDecrease.String(), + "Buyer cost should be 30×48 + 40×50 + 30×52 = 5000 cents (best-price execution)") + return nil } } From 4ce69c0cf9e4e8de60caf10b17d92c6e50057e50 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Thu, 12 Mar 2026 13:21:21 +0700 Subject: [PATCH 3/4] chore: apply suggestion --- tests/streams/order_book/matching_engine_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/streams/order_book/matching_engine_test.go b/tests/streams/order_book/matching_engine_test.go index e7384a47..52003a4a 100644 --- a/tests/streams/order_book/matching_engine_test.go +++ b/tests/streams/order_book/matching_engine_test.go @@ -663,7 +663,7 @@ func testDirectMatchMultipleRounds(t *testing.T) func(context.Context, *kwilTest } // ============================================================================= -// Category E: Edge Cases +// Category F: Edge Cases // ============================================================================= // testNoMatchingOrders tests that no-op occurs when no matching orders exist @@ -821,14 +821,14 @@ func testDirectMatchPriceCrossing(t *testing.T) func(context.Context, *kwilTesti require.False(t, hasBuyOrders, "Buy order should be fully consumed") // Verify buyer got price improvement refund - // Buyer locked: 50 × 52 × 10^16 = 26 × 10^18 (26 TRUF) - // Seller received: 50 × 51 × 10^16 = 25.5 × 10^18 (25.5 TRUF) - // Buyer refund: 50 × 1 × 10^16 = 0.5 × 10^18 (0.5 TRUF) - // Net cost to buyer: 25.5 TRUF (not 26 TRUF) + // Buyer locked: 50 × 52 × 10^16 = 26 × 10^18 (26 USDC) + // Seller received: 50 × 51 × 10^16 = 25.5 × 10^18 (25.5 USDC) + // Buyer refund: 50 × 1 × 10^16 = 0.5 × 10^18 (0.5 USDC) + // Net cost to buyer: 25.5 USDC (not 26 USDC) buyerBalanceAfter, err := getUSDCBalance(ctx, platform, buyer.Address()) require.NoError(t, err) - // Balance decrease = amount actually paid = 50 shares × $0.51 = 25.5 TRUF + // Balance decrease = amount actually paid = 50 shares × $0.51 = 25.5 USDC balanceDecrease := new(big.Int).Sub(buyerBalanceBefore, buyerBalanceAfter) expectedCost := new(big.Int).Mul(big.NewInt(50*51), new(big.Int).Exp(big.NewInt(10), big.NewInt(16), nil)) require.Equal(t, expectedCost.String(), balanceDecrease.String(), From 8feb205d1dd2873854b2ee0af9fdf16c2a0a17e2 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Thu, 12 Mar 2026 18:19:02 +0700 Subject: [PATCH 4/4] chore: patch CI --- tests/streams/order_book/queries_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/streams/order_book/queries_test.go b/tests/streams/order_book/queries_test.go index c460ce31..8eb5ed08 100644 --- a/tests/streams/order_book/queries_test.go +++ b/tests/streams/order_book/queries_test.go @@ -455,8 +455,9 @@ func testGetUserPositionsMixed(t *testing.T) func(context.Context, *kwilTesting. queryID, _ := createTestMarketQueries(t, ctx, platform, &user) - // Buy + split - err = callPlaceBuyOrderQueries(ctx, platform, &user, queryID, false, 45, 50, nil) + // Buy + split (buy price must be below split's NO sell price to avoid price-crossing match) + // Split at true_price=60 creates NO sell @ 40, so NO buy must be < 40 + err = callPlaceBuyOrderQueries(ctx, platform, &user, queryID, false, 35, 50, nil) require.NoError(t, err) err = callPlaceSplitOrderQueries(ctx, platform, &user, queryID, 60, 100, nil)