Skip to content
Merged
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
207 changes: 187 additions & 20 deletions extensions/tn_settlement/settlement_integration_test.go

Large diffs are not rendered by default.

35 changes: 32 additions & 3 deletions internal/migrations/033-order-book-settlement.sql
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@

-- Batch unlock collateral for multiple wallets
-- This helper processes all unlocks in a single call, avoiding nested queries in settlement
-- The $bridge parameter specifies which bridge to use (hoodi_tt2, sepolia_bridge, ethereum_bridge)
CREATE OR REPLACE ACTION ob_batch_unlock_collateral(
$bridge TEXT,
$wallet_addresses TEXT[],
$amounts NUMERIC(78, 0)[]
) PRIVATE {
Expand All @@ -50,7 +52,16 @@ CREATE OR REPLACE ACTION ob_batch_unlock_collateral(
SELECT wallet, amount
FROM UNNEST($wallet_addresses, $amounts) AS u(wallet, amount)
{
ethereum_bridge.unlock($payout.wallet, $payout.amount);
-- Use the correct bridge based on market configuration
if $bridge = 'hoodi_tt2' {
hoodi_tt2.unlock($payout.wallet, $payout.amount);
} else if $bridge = 'sepolia_bridge' {
sepolia_bridge.unlock($payout.wallet, $payout.amount);
} else if $bridge = 'ethereum_bridge' {
ethereum_bridge.unlock($payout.wallet, $payout.amount);
} else {
ERROR('Invalid bridge in ob_batch_unlock_collateral: ' || COALESCE($bridge, 'NULL'));
}
}
};

Expand Down Expand Up @@ -109,6 +120,15 @@ CREATE OR REPLACE ACTION distribute_fees(
RETURN;
}

-- Get market's bridge for unlock operations
$bridge TEXT;
for $row in SELECT bridge FROM ob_queries WHERE id = $query_id {
$bridge := $row.bridge;
}
if $bridge IS NULL {
ERROR('Market not found for query_id: ' || $query_id::TEXT);
}

-- Step 1: Count distinct blocks sampled for this market
$block_count INT := 0;
for $row in SELECT COUNT(DISTINCT block) as cnt FROM ob_rewards WHERE query_id = $query_id {
Expand Down Expand Up @@ -181,7 +201,7 @@ CREATE OR REPLACE ACTION distribute_fees(

-- Step 5: Batch unlock to all LPs (single call, no loops)
if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 {
ob_batch_unlock_collateral($wallet_addresses, $amounts);
ob_batch_unlock_collateral($bridge, $wallet_addresses, $amounts);
}

-- Step 5.5: CREATE AUDIT RECORDS
Expand Down Expand Up @@ -290,6 +310,15 @@ CREATE OR REPLACE ACTION process_settlement(
$total_fees_collected NUMERIC(78, 0) := '0'::NUMERIC(78, 0);
$one_token NUMERIC(78, 0) := '1000000000000000000'::NUMERIC(78, 0);

-- Get market's bridge for unlock operations
$bridge TEXT;
for $row in SELECT bridge FROM ob_queries WHERE id = $query_id {
$bridge := $row.bridge;
}
if $bridge IS NULL {
ERROR('Market not found for query_id: ' || $query_id::TEXT);
}

-- Step 1: Bulk delete all losing positions (efficient single operation)
-- Price semantics: price=0 (holdings), price>0 (open sells), price<0 (open buys)
-- Deletes losing outcome holdings and sells, which have zero value after settlement
Expand Down Expand Up @@ -376,7 +405,7 @@ CREATE OR REPLACE ACTION process_settlement(

-- Step 4: Process ALL payouts in a SINGLE batch call (no nested queries!)
if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 {
ob_batch_unlock_collateral($wallet_addresses, $amounts);
ob_batch_unlock_collateral($bridge, $wallet_addresses, $amounts);
}

-- Step 5: Fee distribution to liquidity providers
Expand Down
34 changes: 28 additions & 6 deletions internal/migrations/037-order-book-validation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,41 @@ PUBLIC VIEW RETURNS (
$buys_collateral NUMERIC(78, 0) := ($open_buys_value::NUMERIC(78, 0) * '1000000000000000000'::NUMERIC(78, 0)) / 100::NUMERIC(78, 0);
$expected_collateral := ($shares_collateral + $buys_collateral)::NUMERIC(78, 0);

-- Step 5: Get actual vault balance from ethereum_bridge
-- The ethereum_bridge.info() precompile returns network ownedBalance
-- Step 5: Get market's bridge and retrieve actual vault balance
$bridge TEXT;
for $row in SELECT bridge FROM ob_queries WHERE id = $query_id {
$bridge := $row.bridge;
}
if $bridge IS NULL {
ERROR('Market not found for query_id: ' || $query_id::TEXT);
}

-- The bridge.info() precompile returns network ownedBalance
$vault_balance NUMERIC(78, 0) := 0::NUMERIC(78, 0);
$row_count INT := 0;

for $info in ethereum_bridge.info() {
$vault_balance := $info.balance;
$row_count := $row_count + 1;
if $bridge = 'hoodi_tt2' {
for $info in hoodi_tt2.info() {
$vault_balance := $info.balance;
$row_count := $row_count + 1;
}
} else if $bridge = 'sepolia_bridge' {
for $info in sepolia_bridge.info() {
$vault_balance := $info.balance;
$row_count := $row_count + 1;
}
} else if $bridge = 'ethereum_bridge' {
for $info in ethereum_bridge.info() {
$vault_balance := $info.balance;
$row_count := $row_count + 1;
}
} else {
ERROR('Invalid bridge in validate_market_collateral: ' || COALESCE($bridge, 'NULL'));
}

-- Validate that bridge returned data (distinguish unavailable from empty vault)
if $row_count = 0 {
ERROR('Cannot validate collateral: ethereum_bridge.info() returned no data. Bridge may be unavailable or not initialized.');
ERROR('Cannot validate collateral: bridge.info() returned no data. Bridge may be unavailable or not initialized.');
}

-- Step 6: Validate binary token parity
Expand Down
18 changes: 9 additions & 9 deletions tests/streams/order_book/cancel_order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,33 +418,33 @@ func testCancelBuyOrderVerifyRefund(t *testing.T) func(ctx context.Context, plat
})
require.NoError(t, err)

// Get balance before buy order
balanceBefore, err := getBalance(ctx, platform, userAddr.Address())
// Get USDC balance before buy order (buy orders lock USDC, not TRUF)
balanceBefore, err := getUSDCBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

// Place buy order: 10 YES @ $0.56 (costs 5.6 TRUF)
// Place buy order: 10 YES @ $0.56 (costs 5.6 USDC)
err = callPlaceBuyOrder(ctx, platform, &userAddr, int(marketID), true, 56, 10)
require.NoError(t, err)

// Get balance after buy order (should be 5.6 TRUF less)
balanceAfterBuy, err := getBalance(ctx, platform, userAddr.Address())
// Get USDC balance after buy order (should be 5.6 USDC less)
balanceAfterBuy, err := getUSDCBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

expectedAfterBuy := new(big.Int).Sub(balanceBefore, toWei("5.6"))
require.Equal(t, 0, expectedAfterBuy.Cmp(balanceAfterBuy),
fmt.Sprintf("Balance after buy should be 5.6 TRUF less. Before: %s, After: %s, Expected: %s",
fmt.Sprintf("USDC balance after buy should be 5.6 less. Before: %s, After: %s, Expected: %s",
balanceBefore.String(), balanceAfterBuy.String(), expectedAfterBuy.String()))

// Cancel the order
err = callCancelOrder(ctx, platform, &userAddr, int(marketID), true, -56)
require.NoError(t, err)

// Get balance after cancel (should be back to balanceBefore)
balanceAfterCancel, err := getBalance(ctx, platform, userAddr.Address())
// Get USDC balance after cancel (should be back to balanceBefore)
balanceAfterCancel, err := getUSDCBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

require.Equal(t, 0, balanceBefore.Cmp(balanceAfterCancel),
fmt.Sprintf("Balance after cancel should equal balance before buy. Before: %s, After: %s",
fmt.Sprintf("USDC balance after cancel should equal balance before buy. Before: %s, After: %s",
balanceBefore.String(), balanceAfterCancel.String()))

return nil
Expand Down
22 changes: 14 additions & 8 deletions tests/streams/order_book/fee_distribution_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func testAuditRecordCreation(t *testing.T) func(context.Context, *kwilTesting.Pl
return func(ctx context.Context, platform *kwilTesting.Platform) error {
// Reset balance point tracker
lastBalancePoint = nil
lastTrufBalancePoint = nil

// Initialize ERC20 extension
err := erc20bridge.ForTestingInitializeExtension(ctx, platform)
Expand Down Expand Up @@ -173,6 +174,7 @@ func testAuditRecordCreation(t *testing.T) func(context.Context, *kwilTesting.Pl
func testAuditMultiBlock(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)

Expand Down Expand Up @@ -246,6 +248,7 @@ func testAuditMultiBlock(t *testing.T) func(context.Context, *kwilTesting.Platfo
func testAuditNoLPs(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)

Expand Down Expand Up @@ -301,6 +304,7 @@ func testAuditNoLPs(t *testing.T) func(context.Context, *kwilTesting.Platform) e
func testAuditZeroFees(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)

Expand Down Expand Up @@ -356,6 +360,7 @@ func testAuditZeroFees(t *testing.T) func(context.Context, *kwilTesting.Platform
func testAuditDataIntegrity(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)

Expand All @@ -381,10 +386,11 @@ func testAuditDataIntegrity(t *testing.T) func(context.Context, *kwilTesting.Pla
// Setup LP scenario (places orders which spend collateral)
setupLPScenario(t, ctx, platform, &user1, &user2, int(marketID))

// Get initial balances AFTER order placement (to measure only fee distribution increase)
bal1Before, err := getBalance(ctx, platform, user1.Address())
// Get initial USDC balances AFTER order placement (to measure only fee distribution increase)
// Note: Fee distribution pays in USDC (hoodi_tt2), not TRUF (hoodi_tt)
bal1Before, err := getUSDCBalance(ctx, platform, user1.Address())
require.NoError(t, err)
bal2Before, err := getBalance(ctx, platform, user2.Address())
bal2Before, err := getUSDCBalance(ctx, platform, user2.Address())
require.NoError(t, err)

// Sample
Expand All @@ -396,10 +402,10 @@ func testAuditDataIntegrity(t *testing.T) func(context.Context, *kwilTesting.Pla
err = fundVaultAndDistributeFees(t, ctx, platform, &user1, int(marketID), totalFees)
require.NoError(t, err)

// Get final balances
bal1After, err := getBalance(ctx, platform, user1.Address())
// Get final USDC balances
bal1After, err := getUSDCBalance(ctx, platform, user1.Address())
require.NoError(t, err)
bal2After, err := getBalance(ctx, platform, user2.Address())
bal2After, err := getUSDCBalance(ctx, platform, user2.Address())
require.NoError(t, err)

// Calculate actual balance increases
Expand Down Expand Up @@ -510,9 +516,9 @@ func setupLPScenario(t *testing.T, ctx context.Context, platform *kwilTesting.Pl
func fundVaultAndDistributeFees(t *testing.T, ctx context.Context, platform *kwilTesting.Platform,
user *util.EthereumAddress, marketID int, totalFees *big.Int) error {

// Fund vault if fees > 0
// Fund vault if fees > 0 (use USDC-only since vault doesn't need TRUF)
if totalFees.Sign() > 0 {
err := giveBalanceChained(ctx, platform, testEscrow, totalFees.String())
err := giveUSDCBalanceChained(ctx, platform, testUSDCEscrow, totalFees.String())
if err != nil {
return err
}
Expand Down
Loading
Loading