diff --git a/extensions/tn_utils/precompiles.go b/extensions/tn_utils/precompiles.go index 7f257f083..3e9c991b0 100644 --- a/extensions/tn_utils/precompiles.go +++ b/extensions/tn_utils/precompiles.go @@ -33,10 +33,50 @@ func buildPrecompile() precompiles.Precompile { forceLastArgFalseMethod(), parseAttestationBooleanMethod(), computeAttestationHashMethod(), + unpackQueryComponentsMethod(), }, } } +// unpackQueryComponentsMethod extracts (dataProvider, streamID, actionID, args) from ABI-encoded bytes. +func unpackQueryComponentsMethod() precompiles.Method { + return precompiles.Method{ + Name: "unpack_query_components", + AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC}, + Parameters: []precompiles.PrecompileValue{ + precompiles.NewPrecompileValue("query_components", types.ByteaType, false), + }, + Returns: &precompiles.MethodReturn{ + IsTable: false, + Fields: []precompiles.PrecompileValue{ + precompiles.NewPrecompileValue("data_provider", types.ByteaType, false), + precompiles.NewPrecompileValue("stream_id", types.ByteaType, false), + precompiles.NewPrecompileValue("action_id", types.TextType, false), + precompiles.NewPrecompileValue("args", types.ByteaType, false), + }, + }, + Handler: unpackQueryComponentsHandler, + } +} + +func unpackQueryComponentsHandler(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + queryComponents, err := toByteSliceAllowNil(inputs[0]) + if err != nil { + return err + } + + if len(queryComponents) == 0 { + return fmt.Errorf("query_components cannot be empty") + } + + dataProvider, streamID, actionID, args, err := unpackQueryComponents(queryComponents) + if err != nil { + return err + } + + return resultFn([]any{dataProvider, streamID, actionID, args}) +} + // callDispatchMethod exposes deterministic metered dispatch to another action. // Arguments and results are transferred as canonical byte blobs so cross-validator // comparisons remain byte-for-byte identical. diff --git a/tests/streams/order_book/fee_distribution_audit_test.go b/tests/streams/order_book/fee_distribution_audit_test.go index 0cca36e29..dafe2b0ae 100644 --- a/tests/streams/order_book/fee_distribution_audit_test.go +++ b/tests/streams/order_book/fee_distribution_audit_test.go @@ -102,7 +102,9 @@ func testAuditRecordCreation(t *testing.T) func(context.Context, *kwilTesting.Pl // Query distribution summary using callback pattern var distributionID int - var totalFeesStr string + var totalLPFeesStr string + var totalDPFeesStr string + var totalValFeesStr string var lpCount int var blockCount int var summaryRowCount int @@ -110,19 +112,29 @@ func testAuditRecordCreation(t *testing.T) func(context.Context, *kwilTesting.Pl _, err = platform.Engine.Call(engineCtx, platform.DB, "", "get_distribution_summary", []any{int(marketID)}, func(row *common.Row) error { distributionID = int(row.Values[0].(int64)) - // total_fees_distributed is NUMERIC(78, 0) which comes as *types.Decimal - totalFeesDecimal := row.Values[1].(*kwilTypes.Decimal) - totalFeesStr = totalFeesDecimal.String() - lpCount = int(row.Values[2].(int64)) - blockCount = int(row.Values[3].(int64)) + // total_lp_fees_distributed is NUMERIC(78, 0) which comes as *types.Decimal + totalLPFeesDecimal := row.Values[1].(*kwilTypes.Decimal) + totalLPFeesStr = totalLPFeesDecimal.String() + totalDPFeesStr = row.Values[2].(*kwilTypes.Decimal).String() + totalValFeesStr = row.Values[3].(*kwilTypes.Decimal).String() + lpCount = int(row.Values[4].(int64)) + blockCount = int(row.Values[5].(int64)) summaryRowCount++ return nil }) require.NoError(t, err) require.Equal(t, 1, summaryRowCount, "Should have 1 distribution summary record") - t.Logf("✅ Audit summary: distribution_id=%d, total_fees=%s, lp_count=%d, block_count=%d", - distributionID, totalFeesStr, lpCount, blockCount) + t.Logf("✅ Audit summary: distribution_id=%d, total_lp_fees=%s, total_dp_fees=%s, total_val_fees=%s, lp_count=%d, block_count=%d", + distributionID, totalLPFeesStr, totalDPFeesStr, totalValFeesStr, lpCount, blockCount) + + // Verify 75/12.5/12.5 split + expectedInfraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + expectedLPShare := new(big.Int).Sub(totalFees, new(big.Int).Mul(expectedInfraShare, big.NewInt(2))) + + require.Equal(t, expectedLPShare.String(), totalLPFeesStr, "LP share in audit should match 75% (+ dust)") + require.Equal(t, expectedInfraShare.String(), totalDPFeesStr, "DP share in audit should match 12.5%") + require.Equal(t, expectedInfraShare.String(), totalValFeesStr, "Validator share in audit should match 12.5%") require.Equal(t, 2, lpCount, "LP count should be 2") require.Equal(t, 1, blockCount, "Block count should be 1") @@ -153,18 +165,18 @@ func testAuditRecordCreation(t *testing.T) func(context.Context, *kwilTesting.Pl require.NoError(t, err) require.Len(t, detailRows, 2, "Should have 2 LP detail records") - // Verify zero-loss: SUM(reward_amount) = total_fees_distributed - totalFeesFromAudit, _ := new(big.Int).SetString(totalFeesStr, 10) + // Verify zero-loss: SUM(reward_amount) = total_lp_fees_distributed + totalLPFeesFromAudit, _ := new(big.Int).SetString(totalLPFeesStr, 10) var totalDistributed big.Int for _, detail := range detailRows { amt, _ := new(big.Int).SetString(detail.rewardAmount, 10) totalDistributed.Add(&totalDistributed, amt) } - require.Equal(t, totalFeesFromAudit.String(), totalDistributed.String(), - "Zero-loss audit: SUM(reward_amount) should equal total_fees_distributed") + require.Equal(t, totalLPFeesFromAudit.String(), totalDistributed.String(), + "Zero-loss audit: SUM(reward_amount) should equal total_lp_fees_distributed") - t.Logf("✅ Audit record creation verified: %s wei distributed across %d LPs", totalFeesFromAudit.String(), lpCount) + t.Logf("✅ Audit record creation verified: %s wei distributed across %d LPs", totalLPFeesFromAudit.String(), lpCount) return nil } @@ -230,7 +242,7 @@ func testAuditMultiBlock(t *testing.T) func(context.Context, *kwilTesting.Platfo _, err = platform.Engine.Call(engineCtx, platform.DB, "", "get_distribution_summary", []any{int(marketID)}, func(row *common.Row) error { - blockCount = int(row.Values[3].(int64)) + blockCount = int(row.Values[5].(int64)) rowCount++ return nil }) @@ -268,6 +280,10 @@ func testAuditNoLPs(t *testing.T) func(context.Context, *kwilTesting.Platform) e }) require.NoError(t, err) + // Lock some collateral to ensure bridge liquidity for payouts + err = callPlaceSplitLimitOrder(ctx, platform, &user1, int(marketID), 50, 100) + require.NoError(t, err) + // Don't sample LP rewards (no LP samples) totalFees := new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)) err = fundVaultAndDistributeFees(t, ctx, platform, &user1, int(marketID), totalFees) @@ -284,20 +300,29 @@ func testAuditNoLPs(t *testing.T) func(context.Context, *kwilTesting.Platform) e } engineCtx := &common.EngineContext{TxContext: tx, OverrideAuthz: true} - // Query distribution summary - should have 1 row with 0 LPs + // Query distribution summary - should have 1 row with 0 LPs but with DP/Val fees var rowCount int var lpCount int + var totalDPFeesStr string + var totalValFeesStr string + _, err = platform.Engine.Call(engineCtx, platform.DB, "", "get_distribution_summary", []any{int(marketID)}, func(row *common.Row) error { - lpCount = int(row.Values[2].(int64)) + totalDPFeesStr = row.Values[2].(*kwilTypes.Decimal).String() + totalValFeesStr = row.Values[3].(*kwilTypes.Decimal).String() + lpCount = int(row.Values[4].(int64)) rowCount++ return nil }) require.NoError(t, err) require.Equal(t, 1, rowCount, "Should have 1 distribution record even with no LPs") require.Equal(t, 0, lpCount, "LP count should be 0") + + expectedInfraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + require.Equal(t, expectedInfraShare.String(), totalDPFeesStr, "DP fees should be recorded") + require.Equal(t, expectedInfraShare.String(), totalValFeesStr, "Validator fees should be recorded") - t.Logf("✅ Audit record correctly created with 0 LPs") + t.Logf("✅ Audit record correctly created with 0 LPs and recorded infra fees") return nil } @@ -349,9 +374,9 @@ func testAuditZeroFees(t *testing.T) func(context.Context, *kwilTesting.Platform func(row *common.Row) error { rowCount++ // Verify zero values in the summary - // get_distribution_summary returns (distribution_id, total_fees_distributed, total_lp_count, block_count, distributed_at) + // get_distribution_summary returns (distribution_id, total_lp_fees_distributed, total_dp_fees, total_validator_fees, total_lp_count, block_count, distributed_at) require.Equal(t, "0", row.Values[1].(*kwilTypes.Decimal).String(), "Total fees should be 0") - require.Equal(t, int64(0), row.Values[2].(int64), "Total LP count should be 0") + require.Equal(t, int64(0), row.Values[4].(int64), "Total LP count should be 0") return nil }) require.NoError(t, err) @@ -432,15 +457,17 @@ func testAuditDataIntegrity(t *testing.T) func(context.Context, *kwilTesting.Pla // Query distribution summary using callback pattern var distributionID int - var totalFeesStr string + var totalLPFeesStr string + var totalDPFeesStr string + var totalValFeesStr string var summaryRowCount int _, err = platform.Engine.Call(engineCtx, platform.DB, "", "get_distribution_summary", []any{int(marketID)}, func(row *common.Row) error { distributionID = int(row.Values[0].(int64)) - // total_fees_distributed is NUMERIC(78, 0) which comes as *types.Decimal - totalFeesDecimal := row.Values[1].(*kwilTypes.Decimal) - totalFeesStr = totalFeesDecimal.String() + totalLPFeesStr = row.Values[1].(*kwilTypes.Decimal).String() + totalDPFeesStr = row.Values[2].(*kwilTypes.Decimal).String() + totalValFeesStr = row.Values[3].(*kwilTypes.Decimal).String() summaryRowCount++ return nil }) @@ -477,18 +504,40 @@ func testAuditDataIntegrity(t *testing.T) func(context.Context, *kwilTesting.Pla require.NotNil(t, auditReward1, "User1 should have audit reward") require.NotNil(t, auditReward2, "User2 should have audit reward") - require.Equal(t, increase1.String(), auditReward1.String(), "Audit reward1 should match balance increase") - require.Equal(t, increase2.String(), auditReward2.String(), "Audit reward2 should match balance increase") + + // Expected balance increases: + // User1 = auditReward1 (LP) + totalDPFees (DP) [+ totalValFees (if Leader)] + // User2 = auditReward2 (LP) + infraShare, _ := new(big.Int).SetString(totalDPFeesStr, 10) + expectedIncrease1 := new(big.Int).Add(auditReward1, infraShare) + + if increase1.Cmp(expectedIncrease1) != 0 { + // Try adding validator share if User1 is leader + infraShareVal, _ := new(big.Int).SetString(totalValFeesStr, 10) + expectedIncrease1WithVal := new(big.Int).Add(expectedIncrease1, infraShareVal) + if increase1.Cmp(expectedIncrease1WithVal) == 0 { + t.Logf("User1 appears to be the leader, adding Validator share to expectation") + expectedIncrease1 = expectedIncrease1WithVal + } + } + + require.Equal(t, expectedIncrease1.String(), increase1.String(), "User1 balance increase should match LP + DP (+ Leader) audit rewards") + require.Equal(t, increase2.String(), auditReward2.String(), "User2 balance increase should match LP audit reward") // Verify zero-loss in audit - totalFeesFromAudit, _ := new(big.Int).SetString(totalFeesStr, 10) - auditSum := new(big.Int).Add(auditReward1, auditReward2) - require.Equal(t, totalFeesFromAudit.String(), auditSum.String(), "Audit rewards should sum to total fees") + totalLPFeesFromAudit, _ := new(big.Int).SetString(totalLPFeesStr, 10) + auditLPSum := new(big.Int).Add(auditReward1, auditReward2) + require.Equal(t, totalLPFeesFromAudit.String(), auditLPSum.String(), "Audit LP rewards should sum to total LP fees") + + totalDPFees, _ := new(big.Int).SetString(totalDPFeesStr, 10) + totalValFees, _ := new(big.Int).SetString(totalValFeesStr, 10) + totalFeesFromAudit := new(big.Int).Add(totalLPFeesFromAudit, new(big.Int).Add(totalDPFees, totalValFees)) + require.Equal(t, totalFees.String(), totalFeesFromAudit.String(), "Total fees from audit should match input totalFees") t.Logf("✅ Audit data integrity verified:") - t.Logf(" - Audit User1 reward: %s wei (balance increase: %s)", auditReward1.String(), increase1.String()) - t.Logf(" - Audit User2 reward: %s wei (balance increase: %s)", auditReward2.String(), increase2.String()) - t.Logf(" - Audit total: %s wei (zero-loss verified)", auditSum.String()) + t.Logf(" - Audit User1 LP reward: %s wei (total increase: %s)", auditReward1.String(), increase1.String()) + t.Logf(" - Audit User2 LP reward: %s wei (total increase: %s)", auditReward2.String(), increase2.String()) + t.Logf(" - Audit total: %s wei (zero-loss verified)", totalFeesFromAudit.String()) return nil } @@ -542,12 +591,16 @@ func fundVaultAndDistributeFees(t *testing.T, ctx context.Context, platform *kwi return err } + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + // Call distribute_fees tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user.Bytes(), Caller: user.Address(), diff --git a/tests/streams/order_book/fee_distribution_test.go b/tests/streams/order_book/fee_distribution_test.go index e4b47a505..3a6c2e762 100644 --- a/tests/streams/order_book/fee_distribution_test.go +++ b/tests/streams/order_book/fee_distribution_test.go @@ -117,14 +117,6 @@ func testDistribution1Block2LPs(t *testing.T) func(context.Context, *kwilTesting require.Len(t, rewards, 2, "Should have 2 LPs") t.Logf("Sampled rewards: %+v", rewards) - // Get participant IDs (user1=1, user2=2 since user1 created market first) - participant1ID := 1 - participant2ID := 2 - - // Verify percentages sum to ~100% - total := rewards[participant1ID] + rewards[participant2ID] - require.InDelta(t, 100.0, total, 0.01, "Rewards should sum to 100%") - // Get balances before distribution balance1Before, err := getUSDCBalance(ctx, platform, user1.Address()) require.NoError(t, err) @@ -150,11 +142,15 @@ func testDistribution1Block2LPs(t *testing.T) func(context.Context, *kwilTesting totalFeesDecimal, err := kwilTypes.ParseDecimalExplicit(totalFees.String(), 78, 0) require.NoError(t, err) + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user1.Bytes(), Caller: user1.Address(), @@ -190,26 +186,39 @@ func testDistribution1Block2LPs(t *testing.T) func(context.Context, *kwilTesting new(big.Int).Div(dist1, big.NewInt(1e18)).String(), new(big.Int).Div(dist2, big.NewInt(1e18)).String()) - // ZERO-LOSS VERIFICATION: Total distributed must equal total fees exactly - totalDistributed := new(big.Int).Add(dist1, dist2) - require.Equal(t, totalFees, totalDistributed, - fmt.Sprintf("Zero-loss verification failed: distributed %s, expected %s", - totalDistributed.String(), totalFees.String())) - - // Verify proportional distribution - // User1: (10e18 * 64) / 100 = 6.4e18 (exactly, no truncation with these percentages) - // User2: (10e18 * 36) / 100 = 3.6e18 (exactly, no truncation) - // Total: 6.4e18 + 3.6e18 = 10e18 ✅ (zero loss, but no dust because math is exact) - expectedDist1 := new(big.Int).Mul(big.NewInt(64), new(big.Int).Div(totalFees, big.NewInt(100))) // 64% = 6.4 TRUF - expectedDist2 := new(big.Int).Mul(big.NewInt(36), new(big.Int).Div(totalFees, big.NewInt(100))) // 36% = 3.6 TRUF + // Step 0: Calculate Expected Shares (75/12.5/12.5 split) + infraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + lpShareTotal := new(big.Int).Sub(totalFees, new(big.Int).Mul(infraShare, big.NewInt(2))) + + // User1 is DP, so they get 12.5% infraShare + their LP share + // User1 LP percentage is 64% of lpShareTotal + // User2 LP percentage is 36% of lpShareTotal + expectedLP1 := new(big.Int).Div(new(big.Int).Mul(lpShareTotal, big.NewInt(64)), big.NewInt(100)) + expectedLP2 := new(big.Int).Div(new(big.Int).Mul(lpShareTotal, big.NewInt(36)), big.NewInt(100)) + + // User1 gets expectedLP1 + infraShare (as DP) + // They might also get another infraShare if they are the leader (@leader_sender) + // In kwil-db testing, @leader_sender usually defaults to the first validator + expectedDist1 := new(big.Int).Add(expectedLP1, infraShare) + expectedDist2 := expectedLP2 + + // Total distributed to our test users (might not be 100% if leader is different) + totalToUsers := new(big.Int).Add(dist1, dist2) + + // If totalToUsers is totalFees, then User1 was also the leader + if totalToUsers.Cmp(totalFees) == 0 { + t.Logf("User1 appears to be the leader, adding second infraShare to expectation") + expectedDist1 = new(big.Int).Add(expectedDist1, infraShare) + } else { + // If not, verify total matches expectedLP1 + expectedLP2 + infraShare (DP) + expectedTotalToUsers := new(big.Int).Add(lpShareTotal, infraShare) + require.Equal(t, expectedTotalToUsers.String(), totalToUsers.String(), "Total to users should be LP shares + DP share") + } - require.Equal(t, expectedDist1, dist1, "User1 should get exactly 64%") - require.Equal(t, expectedDist2, dist2, "User2 should get exactly 36%") + require.Equal(t, expectedDist1.String(), dist1.String(), "User1 should get LP share + DP share (+ Leader share if applicable)") + require.Equal(t, expectedDist2.String(), dist2.String(), "User2 should get exactly their LP share") - t.Logf("✅ Zero-loss distribution verified: User1=%s%%, User2=%s%%, Total=%s wei", - new(big.Int).Div(new(big.Int).Mul(dist1, big.NewInt(100)), totalFees).String(), - new(big.Int).Div(new(big.Int).Mul(dist2, big.NewInt(100)), totalFees).String(), - totalDistributed.String()) + t.Logf("✅ Fee split verified: User1 (LP+DP)=%s, User2 (LP)=%s", dist1.String(), dist2.String()) // Verify ob_rewards table is cleaned up rewardsAfter, err := getRewards(ctx, platform, int(marketID), 1000) @@ -301,10 +310,6 @@ func testDistribution3Blocks2LPs(t *testing.T) func(context.Context, *kwilTestin t.Logf("Sampled rewards - Block 1000: %+v, Block 2000: %+v, Block 3000: %+v", rewards1000, rewards2000, rewards3000) - // Get participant IDs - participant1ID := 1 - participant2ID := 2 - // Get balances before distribution balance1Before, err := getUSDCBalance(ctx, platform, user1.Address()) require.NoError(t, err) @@ -324,11 +329,15 @@ func testDistribution3Blocks2LPs(t *testing.T) func(context.Context, *kwilTestin totalFeesDecimal, err := kwilTypes.ParseDecimalExplicit(totalFees.String(), 78, 0) require.NoError(t, err) + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user1.Bytes(), Caller: user1.Address(), @@ -363,41 +372,41 @@ func testDistribution3Blocks2LPs(t *testing.T) func(context.Context, *kwilTestin new(big.Int).Div(dist1, big.NewInt(1e18)).String(), new(big.Int).Div(dist2, big.NewInt(1e18)).String()) - // ZERO-LOSS VERIFICATION: Total distributed must equal total fees exactly - totalDistributed := new(big.Int).Add(dist1, dist2) - require.Equal(t, totalFees, totalDistributed, - fmt.Sprintf("Zero-loss verification failed: distributed %s, expected %s", - totalDistributed.String(), totalFees.String())) - - // Verify proportional distribution - // Total percentages: User1 = 64+64+64 = 192%, User2 = 36+36+36 = 108% - // User1: (30e18 * 192) / (100 * 3) = 19.2e18 (exactly, no truncation) - // User2: (30e18 * 108) / (100 * 3) = 10.8e18 (exactly, no truncation) - // Total: 19.2e18 + 10.8e18 = 30e18 ✅ (zero loss) - totalPct1 := rewards1000[participant1ID] + rewards2000[participant1ID] + rewards3000[participant1ID] - totalPct2 := rewards1000[participant2ID] + rewards2000[participant2ID] + rewards3000[participant2ID] - - // Calculate expected distributions - // totalPct1 = 192.00, totalPct2 = 108.00 - // (30e18 * 192) / (100 * 3) = 19.2e18 - expectedDist1 := new(big.Int).Div( - new(big.Int).Mul(totalFees, big.NewInt(int64(totalPct1))), - new(big.Int).Mul(big.NewInt(100), big.NewInt(3)), + // Step 0: Calculate Expected Shares (75/12.5/12.5 split) + infraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + lpShareTotal := new(big.Int).Sub(totalFees, new(big.Int).Mul(infraShare, big.NewInt(2))) + + // Total percentages: User1 = 192%, User2 = 108% + // User1 LP: (lpShareTotal * 192) / (100 * 3) + // User2 LP: (lpShareTotal * 108) / (100 * 3) + expectedLP1 := new(big.Int).Div( + new(big.Int).Mul(lpShareTotal, big.NewInt(192)), + big.NewInt(300), ) - expectedDist2 := new(big.Int).Div( - new(big.Int).Mul(totalFees, big.NewInt(int64(totalPct2))), - new(big.Int).Mul(big.NewInt(100), big.NewInt(3)), + expectedLP2 := new(big.Int).Div( + new(big.Int).Mul(lpShareTotal, big.NewInt(108)), + big.NewInt(300), ) - require.Equal(t, expectedDist1, dist1, - fmt.Sprintf("User1 should get (30 TRUF * %v) / 300 = %s", - totalPct1, expectedDist1.String())) - require.Equal(t, expectedDist2, dist2, - fmt.Sprintf("User2 should get (30 TRUF * %v) / 300 = %s", - totalPct2, expectedDist2.String())) + // User1 is DP, so gets expectedLP1 + infraShare (+ Leader share if applicable) + expectedDist1 := new(big.Int).Add(expectedLP1, infraShare) + expectedDist2 := expectedLP2 + + // Total distributed to our test users + totalToUsers := new(big.Int).Add(dist1, dist2) + + if totalToUsers.Cmp(totalFees) == 0 { + t.Logf("User1 appears to be the leader, adding second infraShare to expectation") + expectedDist1 = new(big.Int).Add(expectedDist1, infraShare) + } else { + expectedTotalToUsers := new(big.Int).Add(lpShareTotal, infraShare) + require.Equal(t, expectedTotalToUsers.String(), totalToUsers.String(), "Total to users should be LP shares + DP share") + } + + require.Equal(t, expectedDist1.String(), dist1.String(), "User1 should get LP share + DP share (+ Leader share if applicable)") + require.Equal(t, expectedDist2.String(), dist2.String(), "User2 should get exactly their LP share") - t.Logf("✅ Zero-loss distribution verified: ALL %s wei distributed (User1: %v%%, User2: %v%%)", - totalFees.String(), totalPct1, totalPct2) + t.Logf("✅ Fee split verified: User1=%s, User2=%s", dist1.String(), dist2.String()) // Verify ob_rewards table is cleaned up rewardsAfter, err := getRewards(ctx, platform, int(marketID), 1000) @@ -440,6 +449,10 @@ func testDistributionNoSamples(t *testing.T) func(context.Context, *kwilTesting. require.NoError(t, err) t.Logf("Created market ID: %d", marketID) + // Lock some collateral to ensure bridge liquidity for payouts + err = callPlaceSplitLimitOrder(ctx, platform, &user1, int(marketID), 50, 100) + require.NoError(t, err) + // DO NOT call sample_lp_rewards - no samples! // Get balance before distribution @@ -459,11 +472,15 @@ func testDistributionNoSamples(t *testing.T) func(context.Context, *kwilTesting. totalFeesDecimal, err := kwilTypes.ParseDecimalExplicit(totalFees.String(), 78, 0) require.NoError(t, err) + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user1.Bytes(), Caller: user1.Address(), @@ -489,15 +506,26 @@ func testDistributionNoSamples(t *testing.T) func(context.Context, *kwilTesting. balanceAfter, err := getUSDCBalance(ctx, platform, user1.Address()) require.NoError(t, err) - // Verify NO distribution occurred - require.Equal(t, balanceBefore, balanceAfter, "User balance should be unchanged (no samples)") + // Step 0: Calculate Expected Share (12.5% DP + potentially 12.5% Leader) + infraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + expectedIncrease := infraShare + + if balanceAfter.Cmp(new(big.Int).Add(balanceBefore, infraShare)) > 0 { + t.Logf("User1 appears to be the leader, expecting 2x infraShare") + expectedIncrease = new(big.Int).Add(infraShare, infraShare) + } + + // Verify distribution occurred (DP should get paid) + require.Equal(t, new(big.Int).Add(balanceBefore, expectedIncrease).String(), balanceAfter.String(), + "User should get DP (+ Leader) share even with no LPs") - // Verify vault still has the fees (they weren't distributed) + // Verify vault still has the remaining fees vaultBalance, err := getUSDCBalance(ctx, platform, testEscrow) require.NoError(t, err) - require.True(t, vaultBalance.Cmp(totalFees) >= 0, "Vault should retain fees when no samples exist") + remainingFees := new(big.Int).Sub(totalFees, expectedIncrease) + require.True(t, vaultBalance.Cmp(remainingFees) >= 0, "Vault should retain remaining fees") - t.Logf("✅ Fees correctly stayed in vault (no LPs to reward)") + t.Logf("✅ DP and Validator correctly received shares even with no LPs") return nil } @@ -572,11 +600,15 @@ func testDistributionZeroFees(t *testing.T) func(context.Context, *kwilTesting.P zeroFeesDecimal, err := kwilTypes.ParseDecimalExplicit(zeroFees.String(), 78, 0) require.NoError(t, err) + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user1.Bytes(), Caller: user1.Address(), @@ -697,11 +729,15 @@ func testDistribution1LP(t *testing.T) func(context.Context, *kwilTesting.Platfo totalFeesDecimal, err := kwilTypes.ParseDecimalExplicit(totalFees.String(), 78, 0) require.NoError(t, err) + // Generate leader key for fee transfers + pub := NewTestProposerPub(t) + tx := &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ Height: 1, Timestamp: time.Now().Unix(), + Proposer: pub, }, Signer: user1.Bytes(), Caller: user1.Address(), @@ -732,13 +768,24 @@ func testDistribution1LP(t *testing.T) func(context.Context, *kwilTesting.Platfo t.Logf("Distribution: User1=%s TRUF", new(big.Int).Div(dist, big.NewInt(1e18)).String()) - // Verify user got ~100% of fees (allow 1% tolerance for rounding) - tolerance := new(big.Int).Div(totalFees, big.NewInt(100)) - diff := new(big.Int).Sub(dist, totalFees) - diff.Abs(diff) - require.True(t, diff.Cmp(tolerance) <= 0, - fmt.Sprintf("Single LP should get all fees. Got %s, expected %s (diff %s)", - dist.String(), totalFees.String(), diff.String())) + // Step 0: Calculate Expected Share (75/12.5/12.5 split) + infraShare := new(big.Int).Div(new(big.Int).Mul(totalFees, big.NewInt(125)), big.NewInt(1000)) + lpShareTotal := new(big.Int).Sub(totalFees, new(big.Int).Mul(infraShare, big.NewInt(2))) + + // User1 is LP (100%), DP, and potentially leader. + expectedDist := new(big.Int).Add(lpShareTotal, infraShare) + + if dist.Cmp(totalFees) == 0 { + t.Logf("User1 appears to be the leader, adding second infraShare to expectation") + expectedDist = totalFees + } + + // Verify user got exactly LP + DP share + require.Equal(t, 0, dist.Cmp(expectedDist), + fmt.Sprintf("User1 should get exactly LP + DP fees. Got %s, expected %s", + dist.String(), expectedDist.String())) + + t.Logf("✅ Single LP (plus DP/Leader roles) correctly received fees: %s", dist.String()) // Verify ob_rewards table is cleaned up rewardsAfter, err := getRewards(ctx, platform, int(marketID), 1000) diff --git a/tests/streams/order_book/lp_rewards_config_test.go b/tests/streams/order_book/lp_rewards_config_test.go index 628a24bd3..4421c19f5 100644 --- a/tests/streams/order_book/lp_rewards_config_test.go +++ b/tests/streams/order_book/lp_rewards_config_test.go @@ -422,7 +422,7 @@ func testExtensionConfigLoad(t *testing.T) func(context.Context, *kwilTesting.Pl // Verify config values are valid require.True(t, enabled) - require.Equal(t, int64(10), samplingInterval) + require.Equal(t, int64(50), samplingInterval) require.Equal(t, int64(1000), maxMarkets) return nil diff --git a/tests/streams/order_book/matching_engine_test.go b/tests/streams/order_book/matching_engine_test.go index 3bab891df..c79a9fdf8 100644 --- a/tests/streams/order_book/matching_engine_test.go +++ b/tests/streams/order_book/matching_engine_test.go @@ -29,9 +29,11 @@ var ( // giveBalanceChained gives balance (BOTH TRUF and USDC) with proper linked-list chaining for ordered-sync func giveBalanceChained(ctx context.Context, platform *kwilTesting.Platform, wallet string, amountStr string) error { - // Inject TRUF balance first (for market creation fee) - trufBalancePointCounter++ - trufPoint := trufBalancePointCounter + // Inject TRUF balance (for market creation fees) + trufPointCounter++ + trufPoint := trufPointCounter + + from := ensureNonZeroAddress(wallet) err := testerc20.InjectERC20Transfer( ctx, @@ -39,13 +41,14 @@ func giveBalanceChained(ctx context.Context, platform *kwilTesting.Platform, wal testTRUFChain, testTRUFEscrow, testTRUFERC20, - wallet, + from, wallet, amountStr, trufPoint, lastTrufBalancePoint, // Chain to previous TRUF point ) + if err != nil { return fmt.Errorf("failed to inject TRUF: %w", err) } @@ -64,7 +67,7 @@ func giveBalanceChained(ctx context.Context, platform *kwilTesting.Platform, wal testUSDCChain, testUSDCEscrow, testUSDCERC20, - wallet, + from, wallet, amountStr, usdcPoint, @@ -88,13 +91,15 @@ func giveUSDCBalanceChained(ctx context.Context, platform *kwilTesting.Platform, balancePointCounter++ usdcPoint := balancePointCounter + from := ensureNonZeroAddress(wallet) + err := testerc20.InjectERC20Transfer( ctx, platform, testUSDCChain, testUSDCEscrow, testUSDCERC20, - wallet, + from, wallet, amountStr, usdcPoint, diff --git a/tests/streams/order_book/test_helpers_orderbook.go b/tests/streams/order_book/test_helpers_orderbook.go index 4cd4545bf..473bbc498 100644 --- a/tests/streams/order_book/test_helpers_orderbook.go +++ b/tests/streams/order_book/test_helpers_orderbook.go @@ -7,9 +7,28 @@ import ( gethAbi "github.com/ethereum/go-ethereum/accounts/abi" gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/trufnetwork/kwil-db/core/crypto" "github.com/trufnetwork/node/extensions/tn_utils" ) +// NewTestProposerPub generates a new proposer public key for testing. +func NewTestProposerPub(t require.TestingT) *crypto.Secp256k1PublicKey { + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + require.NoError(t, err) + return pubGeneric.(*crypto.Secp256k1PublicKey) +} + +// ensureNonZeroAddress returns the provided address unless it's the zero address, +// in which case it returns a fallback non-zero address. +func ensureNonZeroAddress(addr string) string { + zeroAddr := "0x0000000000000000000000000000000000000000" + if addr == zeroAddr { + return "0x0000000000000000000000000000000000000001" + } + return addr +} + // encodeQueryComponentsForTests encodes query components using ABI format for testing // This is a shared helper for all order_book tests // If argsBytes is provided, it uses those args; otherwise creates default args for get_record