diff --git a/internal/migrations/erc20-bridge/001-actions.sql b/internal/migrations/erc20-bridge/001-actions.sql index f3564cfbc..e4f2a508d 100644 --- a/internal/migrations/erc20-bridge/001-actions.sql +++ b/internal/migrations/erc20-bridge/001-actions.sql @@ -22,38 +22,18 @@ CREATE OR REPLACE ACTION hoodi_tt_wallet_balance($wallet_address TEXT) PUBLIC VI }; CREATE OR REPLACE ACTION hoodi_tt_bridge_tokens($recipient TEXT DEFAULT NULL, $amount TEXT) PUBLIC { - -- ===== FEE COLLECTION (NO EXEMPTION - USER-FACING OPERATION) ===== - $withdrawal_fee := '40000000000000000000'::NUMERIC(78, 0); -- 40 TRUF with 18 decimals $withdrawal_amount := $amount::NUMERIC(78, 0); - $total_required := $withdrawal_amount + $withdrawal_fee; - $caller_balance := COALESCE(hoodi_tt.balance(@caller), 0::NUMERIC(78, 0)); - IF $caller_balance < $total_required { + IF $caller_balance < $withdrawal_amount { ERROR('Insufficient balance for withdrawal. Required: ' || - ($total_required / '1000000000000000000'::NUMERIC(78, 0))::TEXT || - ' TRUF (' || $withdrawal_amount::TEXT || ' wei withdrawal + ' || - ($withdrawal_fee / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' TRUF fee)'); + ($withdrawal_amount / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' tokens'); } - IF @leader_sender IS NULL { - ERROR('Leader address not available for fee transfer'); - } - $leader_hex TEXT := encode(@leader_sender, 'hex')::TEXT; - hoodi_tt.transfer($leader_hex, $withdrawal_fee); - -- ===== END FEE COLLECTION ===== - $bridge_recipient TEXT := LOWER(COALESCE($recipient, @caller)); -- Execute withdrawal using the bridge extension hoodi_tt.bridge($bridge_recipient, $withdrawal_amount); - - record_transaction_event( - 5, - $withdrawal_fee, - '0x' || $leader_hex, - NULL - ); }; -- HOODI TESTNET - Test Token 2 (TT2) @@ -80,38 +60,18 @@ CREATE OR REPLACE ACTION hoodi_tt2_wallet_balance($wallet_address TEXT) PUBLIC V }; CREATE OR REPLACE ACTION hoodi_tt2_bridge_tokens($recipient TEXT DEFAULT NULL, $amount TEXT) PUBLIC { - -- ===== FEE COLLECTION (NO EXEMPTION - USER-FACING OPERATION) ===== - $withdrawal_fee := '40000000000000000000'::NUMERIC(78, 0); -- 40 TRUF with 18 decimals $withdrawal_amount := $amount::NUMERIC(78, 0); - $total_required := $withdrawal_amount + $withdrawal_fee; - $caller_balance := COALESCE(hoodi_tt2.balance(@caller), 0::NUMERIC(78, 0)); - IF $caller_balance < $total_required { + IF $caller_balance < $withdrawal_amount { ERROR('Insufficient balance for withdrawal. Required: ' || - ($total_required / '1000000000000000000'::NUMERIC(78, 0))::TEXT || - ' TRUF (' || $withdrawal_amount::TEXT || ' wei withdrawal + ' || - ($withdrawal_fee / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' TRUF fee)'); + ($withdrawal_amount / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' tokens'); } - IF @leader_sender IS NULL { - ERROR('Leader address not available for fee transfer'); - } - $leader_hex TEXT := encode(@leader_sender, 'hex')::TEXT; - hoodi_tt2.transfer($leader_hex, $withdrawal_fee); - -- ===== END FEE COLLECTION ===== - $bridge_recipient TEXT := LOWER(COALESCE($recipient, @caller)); -- Execute withdrawal using the bridge extension hoodi_tt2.bridge($bridge_recipient, $withdrawal_amount); - - record_transaction_event( - 5, - $withdrawal_fee, - '0x' || $leader_hex, - NULL - ); }; -- SEPOLIA TESTNET (kept for reference) diff --git a/tests/streams/hoodi_bridge_coexistence_test.go b/tests/streams/hoodi_bridge_coexistence_test.go new file mode 100644 index 000000000..5df64930f --- /dev/null +++ b/tests/streams/hoodi_bridge_coexistence_test.go @@ -0,0 +1,441 @@ +//go:build kwiltest + +package tests + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "github.com/trufnetwork/kwil-db/common" + "github.com/trufnetwork/kwil-db/core/crypto" + coreauth "github.com/trufnetwork/kwil-db/core/crypto/auth" + erc20shim "github.com/trufnetwork/kwil-db/node/exts/erc20-bridge/erc20" + kwilTesting "github.com/trufnetwork/kwil-db/testing" + "github.com/trufnetwork/node/internal/migrations" + testutils "github.com/trufnetwork/node/tests/streams/utils" + testerc20 "github.com/trufnetwork/node/tests/streams/utils/erc20" + "github.com/trufnetwork/sdk-go/core/util" +) + +// Test constants for both Hoodi bridges +const ( + // hoodi_tt bridge (first bridge) + testTTChain = "hoodi" + testTTEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7" + testTTERC20 = "0x263ce78fef26600e4e428cebc91c2a52484b4fbf" + testTTExtensionName = "hoodi_tt" + + // hoodi_tt2 bridge (second bridge) + testTT2Chain = "hoodi" + testTT2Escrow = "0x9BD843A3ce718FE639e9968860B933b026784687" + testTT2ERC20 = "0x1591DeAa21710E0BA6CC1b15F49620C9F65B2dEd" + testTT2ExtensionName = "hoodi_tt2" +) + +var ( + ttPointCounter int64 = 7000 // Start from 7000 for TT bridge + tt2PointCounter int64 = 8000 // Start from 8000 for TT2 bridge + ttPrevPoint *int64 // Track previous point for TT deposits + tt2PrevPoint *int64 // Track previous point for TT2 deposits +) + +// TestHoodiBridgeCoexistence verifies that hoodi_tt and hoodi_tt2 can co-exist without interfering +func TestHoodiBridgeCoexistence(t *testing.T) { + testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ + Name: "HOODI_COEXIST01_BridgeCoexistence", + SeedStatements: migrations.GetSeedScriptStatements(), + FunctionTests: []kwilTesting.TestFunc{ + setupBridgeCoexistenceEnvironment(t), + testTTDepositOnlyAffectsTTBalance(t), + testTT2DepositOnlyAffectsTT2Balance(t), + testWithdrawalsAreIsolated(t), + testCrossContamination(t), + }, + }, testutils.GetTestOptionsWithCache()) +} + +// setupBridgeCoexistenceEnvironment sets up both Hoodi bridge instances +func setupBridgeCoexistenceEnvironment(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + // Reset point trackers + ttPrevPoint = nil + tt2PrevPoint = nil + + // Use the system admin address + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + platform.Deployer = systemAdmin.Bytes() + + // Sync hoodi_tt bridge instance + _, err := erc20shim.ForTestingForceSyncInstance( + ctx, + platform, + testTTChain, + testTTEscrow, + testTTERC20, + 18, // decimals + ) + if err != nil { + return fmt.Errorf("failed to sync hoodi_tt instance: %w", err) + } + + // Sync hoodi_tt2 bridge instance + _, err = erc20shim.ForTestingForceSyncInstance( + ctx, + platform, + testTT2Chain, + testTT2Escrow, + testTT2ERC20, + 18, // decimals + ) + if err != nil { + return fmt.Errorf("failed to sync hoodi_tt2 instance: %w", err) + } + + // Initialize extension to load both instances into singleton + err = erc20shim.ForTestingInitializeExtension(ctx, platform) + if err != nil { + return fmt.Errorf("failed to initialize extension: %w", err) + } + + return nil + } +} + +// Test 1: Deposit to TT only affects TT balance +func testTTDepositOnlyAffectsTTBalance(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + err := erc20shim.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err, "failed to re-initialize extension") + + userAddrVal := util.Unsafe_NewEthereumAddressFromString("0xc555555555555555555555555555555555555555") + userAddr := &userAddrVal + + // Give user 100 TT tokens via hoodi_tt bridge + err = giveTTBalance(ctx, platform, userAddr.Address(), "100000000000000000000") + require.NoError(t, err, "failed to give TT balance") + + // Check TT balance (should be 100) + ttBalance, err := getTTBalance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT balance") + require.Equal(t, "100000000000000000000", ttBalance.String(), "TT balance should be 100 tokens") + + // Check TT2 balance (should be 0) + tt2Balance, err := getTT2Balance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT2 balance") + require.Equal(t, "0", tt2Balance.String(), "TT2 balance should be 0 tokens") + + t.Logf("✅ TT deposit correctly affected only TT balance (TT: 100, TT2: 0)") + return nil + } +} + +// Test 2: Deposit to TT2 only affects TT2 balance +func testTT2DepositOnlyAffectsTT2Balance(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + err := erc20shim.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err, "failed to re-initialize extension") + + userAddrVal := util.Unsafe_NewEthereumAddressFromString("0xc666666666666666666666666666666666666666") + userAddr := &userAddrVal + + // Give user 200 TT2 tokens via hoodi_tt2 bridge + err = giveTT2Balance(ctx, platform, userAddr.Address(), "200000000000000000000") + require.NoError(t, err, "failed to give TT2 balance") + + // Check TT2 balance (should be 200) + tt2Balance, err := getTT2Balance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT2 balance") + require.Equal(t, "200000000000000000000", tt2Balance.String(), "TT2 balance should be 200 tokens") + + // Check TT balance (should be 0) + ttBalance, err := getTTBalance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT balance") + require.Equal(t, "0", ttBalance.String(), "TT balance should be 0 tokens") + + t.Logf("✅ TT2 deposit correctly affected only TT2 balance (TT: 0, TT2: 200)") + return nil + } +} + +// Test 3: Withdrawals are isolated between bridges +func testWithdrawalsAreIsolated(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + err := erc20shim.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err, "failed to re-initialize extension") + + userAddrVal := util.Unsafe_NewEthereumAddressFromString("0xc777777777777777777777777777777777777777") + userAddr := &userAddrVal + + // Reset deposit chains + ttPrevPoint = nil + tt2PrevPoint = nil + + // Give user 100 TT and 100 TT2 + err = giveTTBalance(ctx, platform, userAddr.Address(), "100000000000000000000") + require.NoError(t, err, "failed to give TT balance") + + err = giveTT2Balance(ctx, platform, userAddr.Address(), "100000000000000000000") + require.NoError(t, err, "failed to give TT2 balance") + + // Verify initial balances + ttBalance, err := getTTBalance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT balance") + tt2Balance, err := getTT2Balance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT2 balance") + require.Equal(t, "100000000000000000000", ttBalance.String()) + require.Equal(t, "100000000000000000000", tt2Balance.String()) + + // Generate leader + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + require.NoError(t, err) + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + // Withdraw 10 TT tokens + err = executeTTWithdrawal(ctx, platform, userAddr, pub, userAddr.Address(), "10000000000000000000") + require.NoError(t, err, "TT withdrawal should succeed") + + // Verify TT balance decreased by 10, TT2 unchanged + ttBalanceAfter, err := getTTBalance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT balance after withdrawal") + tt2BalanceAfter, err := getTT2Balance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get TT2 balance after withdrawal") + require.Equal(t, "90000000000000000000", ttBalanceAfter.String(), "TT balance should be 90") + require.Equal(t, "100000000000000000000", tt2BalanceAfter.String(), "TT2 balance should remain 100") + + // Withdraw 20 TT2 tokens + err = executeTT2Withdrawal(ctx, platform, userAddr, pub, userAddr.Address(), "20000000000000000000") + require.NoError(t, err, "TT2 withdrawal should succeed") + + // Verify TT2 balance decreased by 20, TT unchanged + ttBalanceFinal, err := getTTBalance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get final TT balance") + tt2BalanceFinal, err := getTT2Balance(ctx, platform, userAddr.Address()) + require.NoError(t, err, "failed to get final TT2 balance") + require.Equal(t, "90000000000000000000", ttBalanceFinal.String(), "TT balance should remain 90") + require.Equal(t, "80000000000000000000", tt2BalanceFinal.String(), "TT2 balance should be 80") + + t.Logf("✅ Withdrawals correctly isolated (TT: 100→90, TT2: 100→80)") + return nil + } +} + +// Test 4: Cross-contamination test - operations on one bridge don't affect the other +func testCrossContamination(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + err := erc20shim.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err, "failed to re-initialize extension") + + user1AddrVal := util.Unsafe_NewEthereumAddressFromString("0xc888888888888888888888888888888888888888") + user1Addr := &user1AddrVal + + user2AddrVal := util.Unsafe_NewEthereumAddressFromString("0xc999999999999999999999999999999999999999") + user2Addr := &user2AddrVal + + // Reset deposit chains + ttPrevPoint = nil + tt2PrevPoint = nil + + // User1: deposit 50 TT + err = giveTTBalance(ctx, platform, user1Addr.Address(), "50000000000000000000") + require.NoError(t, err) + + // User2: deposit 75 TT2 + err = giveTT2Balance(ctx, platform, user2Addr.Address(), "75000000000000000000") + require.NoError(t, err) + + // Verify User1 has 50 TT and 0 TT2 + user1TT, err := getTTBalance(ctx, platform, user1Addr.Address()) + require.NoError(t, err, "failed to get User1 TT balance") + user1TT2, err := getTT2Balance(ctx, platform, user1Addr.Address()) + require.NoError(t, err, "failed to get User1 TT2 balance") + require.Equal(t, "50000000000000000000", user1TT.String()) + require.Equal(t, "0", user1TT2.String()) + + // Verify User2 has 0 TT and 75 TT2 + user2TT, err := getTTBalance(ctx, platform, user2Addr.Address()) + require.NoError(t, err, "failed to get User2 TT balance") + user2TT2, err := getTT2Balance(ctx, platform, user2Addr.Address()) + require.NoError(t, err, "failed to get User2 TT2 balance") + require.Equal(t, "0", user2TT.String()) + require.Equal(t, "75000000000000000000", user2TT2.String()) + + // Generate leader + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + require.NoError(t, err) + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + // User1 withdraws 5 TT + err = executeTTWithdrawal(ctx, platform, user1Addr, pub, user1Addr.Address(), "5000000000000000000") + require.NoError(t, err) + + // Verify User1's TT decreased, User2's TT2 unchanged + user1TTAfter, err := getTTBalance(ctx, platform, user1Addr.Address()) + require.NoError(t, err, "failed to get User1 TT balance after withdrawal") + user2TT2After, err := getTT2Balance(ctx, platform, user2Addr.Address()) + require.NoError(t, err, "failed to get User2 TT2 balance after withdrawal") + require.Equal(t, "45000000000000000000", user1TTAfter.String(), "User1 TT should be 45") + require.Equal(t, "75000000000000000000", user2TT2After.String(), "User2 TT2 should remain 75") + + // User2 withdraws 10 TT2 + err = executeTT2Withdrawal(ctx, platform, user2Addr, pub, user2Addr.Address(), "10000000000000000000") + require.NoError(t, err) + + // Verify User2's TT2 decreased, User1's TT unchanged + user1TTFinal, err := getTTBalance(ctx, platform, user1Addr.Address()) + require.NoError(t, err, "failed to get final User1 TT balance") + user2TT2Final, err := getTT2Balance(ctx, platform, user2Addr.Address()) + require.NoError(t, err, "failed to get final User2 TT2 balance") + require.Equal(t, "45000000000000000000", user1TTFinal.String(), "User1 TT should remain 45") + require.Equal(t, "65000000000000000000", user2TT2Final.String(), "User2 TT2 should be 65") + + t.Logf("✅ No cross-contamination detected (User1 TT: 50→45, User2 TT2: 75→65)") + return nil + } +} + +// ===== HELPER FUNCTIONS ===== + +// TT Bridge helpers +func giveTTBalance(ctx context.Context, platform *kwilTesting.Platform, wallet string, amountStr string) error { + ttPointCounter++ + currentPoint := ttPointCounter + + err := testerc20.InjectERC20Transfer( + ctx, + platform, + testTTChain, + testTTEscrow, + testTTERC20, + wallet, + wallet, + amountStr, + currentPoint, + ttPrevPoint, + ) + + if err == nil { + p := currentPoint + ttPrevPoint = &p + } + + return err +} + +func getTTBalance(ctx context.Context, platform *kwilTesting.Platform, wallet string) (*big.Int, error) { + balanceStr, err := testerc20.GetUserBalance(ctx, platform, testTTExtensionName, wallet) + if err != nil { + return nil, err + } + + balance := new(big.Int) + if _, ok := balance.SetString(balanceStr, 10); !ok { + return nil, fmt.Errorf("invalid balance string: %s", balanceStr) + } + + return balance, nil +} + +func executeTTWithdrawal(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, recipient string, amount string) error { + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{ + Height: 2, + Proposer: leaderPub, + }, + Signer: signer.Bytes(), + Caller: signer.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + } + engineCtx := &common.EngineContext{TxContext: tx} + + res, err := platform.Engine.Call( + engineCtx, + platform.DB, + "", + "hoodi_tt_bridge_tokens", + []any{recipient, amount}, + func(row *common.Row) error { return nil }, + ) + if err != nil { + return err + } + if res != nil && res.Error != nil { + return res.Error + } + return nil +} + +// TT2 Bridge helpers +func giveTT2Balance(ctx context.Context, platform *kwilTesting.Platform, wallet string, amountStr string) error { + tt2PointCounter++ + currentPoint := tt2PointCounter + + err := testerc20.InjectERC20Transfer( + ctx, + platform, + testTT2Chain, + testTT2Escrow, + testTT2ERC20, + wallet, + wallet, + amountStr, + currentPoint, + tt2PrevPoint, + ) + + if err == nil { + p := currentPoint + tt2PrevPoint = &p + } + + return err +} + +func getTT2Balance(ctx context.Context, platform *kwilTesting.Platform, wallet string) (*big.Int, error) { + balanceStr, err := testerc20.GetUserBalance(ctx, platform, testTT2ExtensionName, wallet) + if err != nil { + return nil, err + } + + balance := new(big.Int) + if _, ok := balance.SetString(balanceStr, 10); !ok { + return nil, fmt.Errorf("invalid balance string: %s", balanceStr) + } + + return balance, nil +} + +func executeTT2Withdrawal(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, recipient string, amount string) error { + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{ + Height: 2, + Proposer: leaderPub, + }, + Signer: signer.Bytes(), + Caller: signer.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + } + engineCtx := &common.EngineContext{TxContext: tx} + + res, err := platform.Engine.Call( + engineCtx, + platform.DB, + "", + "hoodi_tt2_bridge_tokens", + []any{recipient, amount}, + func(row *common.Row) error { return nil }, + ) + if err != nil { + return err + } + if res != nil && res.Error != nil { + return res.Error + } + return nil +} diff --git a/tests/streams/hoodi_withdrawal_fee_test.go b/tests/streams/hoodi_withdrawal_fee_test.go index 3db5d1297..8dc9a8056 100644 --- a/tests/streams/hoodi_withdrawal_fee_test.go +++ b/tests/streams/hoodi_withdrawal_fee_test.go @@ -20,19 +20,17 @@ import ( "github.com/trufnetwork/sdk-go/core/util" ) -// Test constants for Hoodi withdrawal fees +// Test constants for Hoodi bridge (no withdrawal fees) const ( testHoodiChain = "hoodi" testHoodiEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7" // Real Hoodi bridge proxy testHoodiERC20 = "0x263ce78fef26600e4e428cebc91c2a52484b4fbf" // Real TRUF token on Hoodi testHoodiExtensionName = "hoodi_tt" // Extension name from migrations - hoodiWithdrawalFee = "40000000000000000000" // 40 TRUF with 18 decimals ) var ( - fortyTRUFHoodiFee = mustParseHoodiBigInt(hoodiWithdrawalFee) // 40 TRUF as big.Int - hoodiPointCounter int64 = 6000 // Start from 6000, increment for each balance injection - hoodiPrevPoint *int64 // Track previous point for deposit chaining + hoodiPointCounter int64 = 6000 // Start from 6000, increment for each balance injection + hoodiPrevPoint *int64 // Track previous point for deposit chaining ) func mustParseHoodiBigInt(s string) *big.Int { @@ -44,18 +42,17 @@ func mustParseHoodiBigInt(s string) *big.Int { return val } -// TestHoodiWithdrawalFees is the main test suite for Hoodi bridge withdrawal fees +// TestHoodiWithdrawalNoFees is the main test suite for Hoodi bridge withdrawals (no fees) // This test validates the hoodi_tt_bridge_tokens action defined in 001-actions.sql -func TestHoodiWithdrawalFees(t *testing.T) { +func TestHoodiWithdrawalNoFees(t *testing.T) { testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ - Name: "HOODI_WITHDRAWAL_FEE01_WithdrawalFees", + Name: "HOODI_WITHDRAWAL_NOFEE01_NoWithdrawalFees", SeedStatements: migrations.GetSeedScriptStatements(), FunctionTests: []kwilTesting.TestFunc{ setupHoodiWithdrawalTestEnvironment(t), - testHoodiWithdrawalPaysFee(t), + testHoodiWithdrawalNoFee(t), testHoodiWithdrawalInsufficientBalance(t), - testHoodiWithdrawalLeaderReceivesFees(t), - testHoodiWithdrawalFeeRecordedInLedger(t), + testHoodiWithdrawalLeaderReceivesNoFees(t), }, }, testutils.GetTestOptionsWithCache()) } @@ -96,8 +93,8 @@ func setupHoodiWithdrawalTestEnvironment(t *testing.T) func(ctx context.Context, } } -// Test 1: hoodi_tt_bridge_tokens pays 40 TRUF fee -func testHoodiWithdrawalPaysFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { +// Test 1: hoodi_tt_bridge_tokens charges no fee +func testHoodiWithdrawalNoFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { return func(ctx context.Context, platform *kwilTesting.Platform) error { // Re-initialize extension in this test (singleton might have been reset) err := erc20shim.ForTestingInitializeExtension(ctx, platform) @@ -116,33 +113,32 @@ func testHoodiWithdrawalPaysFee(t *testing.T) func(ctx context.Context, platform t.Logf("DEBUG: User balance after deposit: %s", initialBalance.String()) require.Equal(t, "100000000000000000000", initialBalance.String(), "initial balance should be 100 TRUF (got %s)", initialBalance.String()) - // Generate leader for fee recipient + // Generate leader _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) require.NoError(t, err, "failed to generate leader key") pub := pubGeneric.(*crypto.Secp256k1PublicKey) - // Withdraw 10 TRUF (should deduct 50 TRUF total: 10 withdrawal + 40 fee) + // Withdraw 10 TRUF (should deduct only 10 TRUF, no fee) withdrawAmount := "10000000000000000000" // 10 TRUF err = executeHoodiWithdrawalWithLeader(ctx, platform, userAddr, pub, userAddr.Address(), withdrawAmount) require.NoError(t, err, "hoodi_tt_bridge_tokens should succeed") - // Verify balance decreased by 50 TRUF (10 TRUF withdrawal + 40 TRUF fee) + // Verify balance decreased by exactly 10 TRUF (no fee) finalBalance, err := getHoodiBalance(ctx, platform, userAddr.Address()) require.NoError(t, err, "failed to get final balance") withdrawAmountBig, _ := new(big.Int).SetString(withdrawAmount, 10) - expectedDeduction := new(big.Int).Add(withdrawAmountBig, fortyTRUFHoodiFee) - expectedBalance := new(big.Int).Sub(initialBalance, expectedDeduction) + expectedBalance := new(big.Int).Sub(initialBalance, withdrawAmountBig) require.Equal(t, 0, expectedBalance.Cmp(finalBalance), - "Balance should decrease by 50 TRUF (10 withdrawal + 40 fee), expected %s but got %s", + "Balance should decrease by exactly 10 TRUF (no fee), expected %s but got %s", expectedBalance, finalBalance) - t.Logf("✅ hoodi_tt_bridge_tokens correctly deducted 50 TRUF (10 withdrawal + 40 fee)") + t.Logf("✅ hoodi_tt_bridge_tokens correctly deducted only 10 TRUF (no fee)") return nil } } -// Test 2: Insufficient balance for withdrawal + fee fails +// Test 2: Insufficient balance for withdrawal fails func testHoodiWithdrawalInsufficientBalance(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { return func(ctx context.Context, platform *kwilTesting.Platform) error { // Re-initialize extension in this test (singleton might have been reset) @@ -152,29 +148,29 @@ func testHoodiWithdrawalInsufficientBalance(t *testing.T) func(ctx context.Conte userAddrVal := util.Unsafe_NewEthereumAddressFromString("0xc222222222222222222222222222222222222222") userAddr := &userAddrVal - // Give user only 30 TRUF (insufficient for 10 TRUF withdrawal + 40 TRUF fee = 50 TRUF needed) - err = giveHoodiBalance(ctx, platform, userAddr.Address(), "30000000000000000000") + // Give user only 5 TRUF (insufficient for 10 TRUF withdrawal) + err = giveHoodiBalance(ctx, platform, userAddr.Address(), "5000000000000000000") require.NoError(t, err, "failed to give balance") - // Generate leader for fee recipient + // Generate leader _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) require.NoError(t, err, "failed to generate leader key") pub := pubGeneric.(*crypto.Secp256k1PublicKey) - // Try to withdraw 10 TRUF (should fail - needs 50 TRUF total) + // Try to withdraw 10 TRUF (should fail - only has 5 TRUF) withdrawAmount := "10000000000000000000" // 10 TRUF err = executeHoodiWithdrawalWithLeader(ctx, platform, userAddr, pub, userAddr.Address(), withdrawAmount) require.Error(t, err, "hoodi_tt_bridge_tokens should fail with insufficient balance") require.Contains(t, err.Error(), "Insufficient balance for withdrawal", "error should mention insufficient balance, got: %v", err) - t.Logf("✅ hoodi_tt_bridge_tokens correctly rejects insufficient balance (30 TRUF < 50 TRUF needed)") + t.Logf("✅ hoodi_tt_bridge_tokens correctly rejects insufficient balance (5 TRUF < 10 TRUF needed)") return nil } } -// Test 3: Leader receives 40 TRUF fee -func testHoodiWithdrawalLeaderReceivesFees(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { +// Test 3: Leader receives no fees +func testHoodiWithdrawalLeaderReceivesNoFees(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { return func(ctx context.Context, platform *kwilTesting.Platform) error { // Re-initialize extension in this test (singleton might have been reset) err := erc20shim.ForTestingInitializeExtension(ctx, platform) @@ -214,95 +210,16 @@ func testHoodiWithdrawalLeaderReceivesFees(t *testing.T) func(ctx context.Contex err = executeHoodiWithdrawalWithLeader(ctx, platform, userAddr, pub, userAddr.Address(), withdrawAmount) require.NoError(t, err, "hoodi_tt_bridge_tokens with leader should succeed") - // Verify leader balance increased by 40 TRUF + // Verify leader balance stays at 0 (no fee transfer) finalLeaderBalance, err := getHoodiBalance(ctx, platform, leaderAddr) require.NoError(t, err, "failed to get final leader balance") - expectedLeaderBalance := new(big.Int).Add(initialLeaderBalance, fortyTRUFHoodiFee) - require.Equal(t, 0, expectedLeaderBalance.Cmp(finalLeaderBalance), - "Leader should receive 40 TRUF fee, expected %s but got %s", - expectedLeaderBalance, finalLeaderBalance) - - t.Logf("✅ Leader correctly received 40 TRUF fee (balance: %s → %s)", + require.Equal(t, 0, initialLeaderBalance.Cmp(finalLeaderBalance), + "Leader should receive no fee, balance should stay %s but got %s", initialLeaderBalance, finalLeaderBalance) - return nil - } -} - -// Test 4: Withdrawal fee is recorded in transaction ledger -func testHoodiWithdrawalFeeRecordedInLedger(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { - return func(ctx context.Context, platform *kwilTesting.Platform) error { - // Re-initialize extension in this test - err := erc20shim.ForTestingInitializeExtension(ctx, platform) - require.NoError(t, err, "failed to re-initialize extension") - - userAddrVal := util.Unsafe_NewEthereumAddressFromString("0xc444444444444444444444444444444444444444") - userAddr := &userAddrVal - // Give user 100 TRUF - // Reset deposit chain for test independence - hoodiPrevPoint = nil - err = giveHoodiBalance(ctx, platform, userAddr.Address(), "100000000000000000000") - require.NoError(t, err, "failed to give balance") - - // Generate leader keys - _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) - require.NoError(t, err, "failed to generate leader key") - pub := pubGeneric.(*crypto.Secp256k1PublicKey) - leaderSigner := crypto.EthereumAddressFromPubKey(pub) - leaderAddr := fmt.Sprintf("0x%x", leaderSigner) - - // Withdraw 10 TRUF - withdrawAmount := "10000000000000000000" // 10 TRUF - err = executeHoodiWithdrawalWithLeader(ctx, platform, userAddr, pub, userAddr.Address(), withdrawAmount) - require.NoError(t, err, "hoodi_tt_bridge_tokens should succeed") - - // Query transaction_events table to verify fee was recorded - // The hoodi_tt_bridge_tokens action calls record_transaction_event(5, $withdrawal_fee, leader, NULL) - // Table schema: method_id (INT), fee_amount (NUMERIC), fee_recipient (TEXT) - query := `SELECT method_id, fee_amount::TEXT, fee_recipient FROM transaction_events - WHERE method_id = 5 AND fee_recipient IS NOT NULL - ORDER BY block_height DESC LIMIT 1` - - var methodID int - var feeAmount string - var feeRecipient string - var found bool - - err = platform.Engine.ExecuteWithoutEngineCtx(ctx, platform.DB, query, map[string]any{}, func(row *common.Row) error { - found = true - if len(row.Values) >= 3 { - // Safe type assertions with comma-ok idiom - if val, ok := row.Values[0].(int64); ok { - methodID = int(val) - } else { - return fmt.Errorf("expected int64 for method_id, got %T", row.Values[0]) - } - - if val, ok := row.Values[1].(string); ok { - feeAmount = val - } else { - return fmt.Errorf("expected string for fee_amount, got %T", row.Values[1]) - } - - if val, ok := row.Values[2].(string); ok { - feeRecipient = val - } else { - return fmt.Errorf("expected string for fee_recipient, got %T", row.Values[2]) - } - } - return nil - }) - require.NoError(t, err, "failed to query transaction_events") - require.True(t, found, "withdrawal fee event should be recorded in transaction_events") - - // Verify event details - require.Equal(t, 5, methodID, "method_id should be 5 (withdrawal fee)") - require.Equal(t, hoodiWithdrawalFee, feeAmount, "fee_amount should be 40 TRUF fee") - require.Equal(t, leaderAddr, feeRecipient, "fee_recipient should be leader address") - - t.Logf("✅ Withdrawal fee correctly recorded in transaction_events (event_type=5, amount=40 TRUF, recipient=%s)", - leaderAddr) + t.Logf("✅ Leader correctly received no fee (balance: %s → %s)", + initialLeaderBalance, finalLeaderBalance) return nil } } @@ -371,7 +288,7 @@ func callHoodiWithdrawalAction(ctx context.Context, platform *kwilTesting.Platfo engineCtx, platform.DB, "", - "hoodi_tt_bridge_tokens", // Hoodi TT-specific action with 40 TRUF fee + "hoodi_tt_bridge_tokens", // Hoodi TT bridge action (no fees) []any{recipient, amount}, func(row *common.Row) error { return nil }, ) @@ -384,7 +301,7 @@ func callHoodiWithdrawalAction(ctx context.Context, platform *kwilTesting.Platfo return nil } -// executeHoodiWithdrawalWithLeader executes a withdrawal with a specific leader (for testing fee recipient) +// executeHoodiWithdrawalWithLeader executes a withdrawal with a specific leader func executeHoodiWithdrawalWithLeader(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, recipient string, amount string) error { return callHoodiWithdrawalAction(ctx, platform, signer, leaderPub, recipient, amount) }