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
18 changes: 16 additions & 2 deletions internal/migrations/erc20-bridge/000-extension.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,23 @@ USE erc20 {
distribution_period: '10m'
} AS sepolia_bridge;

-- The following is for test environments where the hoodi bridge is used. Please comment for production and use the above.
-- The following is for test environments where the hoodi bridges are used. Please comment for production and use the above.
-- First hoodi bridge: Test Token (TT)
-- Token: 0x263ce78fef26600e4e428cebc91c2a52484b4fbf
-- Proxy: 0x878d6aaeb6e746033f50b8dc268d54b4631554e7
-- Explorer: https://hoodi.etherscan.io/address/0x878d6aaeb6e746033f50b8dc268d54b4631554e7
USE erc20 {
chain: 'hoodi',
escrow: '0x878d6aaeb6e746033f50b8dc268d54b4631554e7',
distribution_period: '10m'
} AS hoodi_bridge;
} AS hoodi_tt;

-- Second hoodi bridge: Test Token 2 (TT2)
-- Token: 0x1591DeAa21710E0BA6CC1b15F49620C9F65B2dEd
-- Proxy: 0x9BD843A3ce718FE639e9968860B933b026784687
-- Explorer: https://hoodi.etherscan.io/address/0x9BD843A3ce718FE639e9968860B933b026784687
USE erc20 {
chain: 'hoodi',
escrow: '0x9BD843A3ce718FE639e9968860B933b026784687',
distribution_period: '10m'
} AS hoodi_tt2;
76 changes: 67 additions & 9 deletions internal/migrations/erc20-bridge/001-actions.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- HOODI TESTNET
CREATE OR REPLACE ACTION hoodi_get_erc20_bridge_info()
-- HOODI TESTNET - Test Token (TT)
CREATE OR REPLACE ACTION hoodi_tt_get_erc20_bridge_info()
PUBLIC VIEW RETURNS (
chain TEXT,
escrow TEXT,
Expand All @@ -11,23 +11,23 @@ PUBLIC VIEW RETURNS (
synced_at INT8,
enabled BOOLEAN
) {
FOR $row IN hoodi_bridge.info() {
FOR $row IN hoodi_tt.info() {
RETURN $row.chain, $row.escrow, $row.epoch_period, $row.erc20, $row.decimals, $row.balance, $row.synced, $row.synced_at, $row.enabled;
}
};

CREATE OR REPLACE ACTION hoodi_wallet_balance($wallet_address TEXT) PUBLIC VIEW RETURNS (balance NUMERIC(78, 0)) {
$balance := hoodi_bridge.balance($wallet_address);
CREATE OR REPLACE ACTION hoodi_tt_wallet_balance($wallet_address TEXT) PUBLIC VIEW RETURNS (balance NUMERIC(78, 0)) {
$balance := hoodi_tt.balance($wallet_address);
RETURN $balance;
};

CREATE OR REPLACE ACTION hoodi_bridge_tokens($recipient TEXT DEFAULT NULL, $amount TEXT) PUBLIC {
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_bridge.balance(@caller), 0::NUMERIC(78, 0));
$caller_balance := COALESCE(hoodi_tt.balance(@caller), 0::NUMERIC(78, 0));

IF $caller_balance < $total_required {
ERROR('Insufficient balance for withdrawal. Required: ' ||
Expand All @@ -40,13 +40,71 @@ CREATE OR REPLACE ACTION hoodi_bridge_tokens($recipient TEXT DEFAULT NULL, $amou
ERROR('Leader address not available for fee transfer');
}
$leader_hex TEXT := encode(@leader_sender, 'hex')::TEXT;
hoodi_bridge.transfer($leader_hex, $withdrawal_fee);
hoodi_tt.transfer($leader_hex, $withdrawal_fee);
-- ===== END FEE COLLECTION =====

$bridge_recipient TEXT := LOWER(COALESCE($recipient, @caller));

-- Execute withdrawal using the bridge extension
hoodi_bridge.bridge($bridge_recipient, $withdrawal_amount);
hoodi_tt.bridge($bridge_recipient, $withdrawal_amount);

record_transaction_event(
5,
$withdrawal_fee,
'0x' || $leader_hex,
NULL
);
};

-- HOODI TESTNET - Test Token 2 (TT2)
CREATE OR REPLACE ACTION hoodi_tt2_get_erc20_bridge_info()
PUBLIC VIEW RETURNS (
chain TEXT,
escrow TEXT,
epoch_period TEXT,
erc20 TEXT,
decimals INT,
balance NUMERIC(78, 0),
synced BOOLEAN,
synced_at INT8,
enabled BOOLEAN
) {
FOR $row IN hoodi_tt2.info() {
RETURN $row.chain, $row.escrow, $row.epoch_period, $row.erc20, $row.decimals, $row.balance, $row.synced, $row.synced_at, $row.enabled;
}
};

CREATE OR REPLACE ACTION hoodi_tt2_wallet_balance($wallet_address TEXT) PUBLIC VIEW RETURNS (balance NUMERIC(78, 0)) {
$balance := hoodi_tt2.balance($wallet_address);
RETURN $balance;
};

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 {
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)');
}

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,
Expand Down
8 changes: 4 additions & 4 deletions internal/migrations/erc20-bridge/003-disable-hoodi.prod.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- Disable and remove Hoodi bridge instance
-- This migration removes the hoodi_bridge extension to allow fresh deployment with new escrow
-- Disable and remove Hoodi bridge instance (Test Token)
-- This migration removes the hoodi_tt extension to allow fresh deployment with new escrow
--
-- UNUSE will properly clean up the namespace even if the instance is already disabled.
-- Use this migration to rollback to a clean state before re-adding the hoodi_bridge with new parameters.
-- Use this migration to rollback to a clean state before re-adding the hoodi_tt with new parameters.

UNUSE hoodi_bridge;
UNUSE hoodi_tt;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
-- Withdrawal Proof Action for Hoodi Non-Custodial Bridge
-- Withdrawal Proof Action for Hoodi Non-Custodial Bridge (Test Token)
-- This action exposes the list_wallet_rewards precompile as a public action.
-- Returns merkle proofs AND validator signatures - everything needed for withdrawal.
-- Returns ALL confirmed epochs, not just first one (uses RETURN NEXT)
-- Epochs filtered by withdrawals table (only unclaimed epochs returned)
CREATE OR REPLACE ACTION hoodi_get_withdrawal_proof($wallet_address TEXT)
CREATE OR REPLACE ACTION hoodi_tt_get_withdrawal_proof($wallet_address TEXT)
PUBLIC VIEW RETURNS TABLE (
chain TEXT,
chain_id TEXT,
Expand All @@ -18,7 +18,7 @@ PUBLIC VIEW RETURNS TABLE (
) {
-- with_pending = false means only return confirmed epochs (ready for withdrawal)
-- Returns ALL confirmed epochs ordered by height DESC (newest first)
FOR $row IN hoodi_bridge.list_wallet_rewards($wallet_address, false) {
FOR $row IN hoodi_tt.list_wallet_rewards($wallet_address, false) {
-- Return each row (don't exit loop!)
RETURN NEXT $row.chain, $row.chain_id, $row.contract, $row.created_at,
$row.param_recipient, $row.param_amount, $row.param_block_hash,
Expand Down
34 changes: 17 additions & 17 deletions tests/extensions/erc20/erc20_bridge_withdrawal_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,29 @@ const (
// Real Hoodi chain and addresses (from migrations)
MigrationHoodiChain = "hoodi"
MigrationHoodiEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7"
MigrationHoodiAlias = "hoodi_bridge" // Alias from migration 000-extension.sql
MigrationHoodiAlias = "hoodi_tt" // Alias from migration 000-extension.sql
)

// TestHoodiGetWithdrawalProofAction tests the public hoodi_get_withdrawal_proof action.
// This test uses the migration-registered hoodi_bridge instance (not a test-specific instance).
// TestHoodiGetWithdrawalProofAction tests the public hoodi_tt_get_withdrawal_proof action.
// This test uses the migration-registered hoodi_tt instance (not a test-specific instance).
//
// Test flow:
// 1) Seed data into migration-registered hoodi_bridge instance
// 1) Seed data into migration-registered hoodi_tt instance
// 2) User deposits and withdraws
// 3) Finalize and confirm epoch
// 4) Call public action hoodi_get_withdrawal_proof
// 4) Call public action hoodi_tt_get_withdrawal_proof
// 5) Verify returned merkle proof structure
Comment thread
coderabbitai[bot] marked this conversation as resolved.
//
// This validates the full end-to-end public API that users will call.
func TestHoodiGetWithdrawalProofAction(t *testing.T) {
seedAndRun(t, "hoodi_get_withdrawal_proof_action", func(ctx context.Context, platform *kwilTesting.Platform) error {
// The hoodi_bridge instance is already created by migrations (000-extension.sql)
seedAndRun(t, "hoodi_tt_get_withdrawal_proof_action", func(ctx context.Context, platform *kwilTesting.Platform) error {
// The hoodi_tt instance is already created by migrations (000-extension.sql)
// We just need to seed it with test data

testUser := "0xf9820f9143699cac6f662b19a4b29e13c9393783"
testAmount := "100000000000000000000" // 100 tokens

// The hoodi_bridge instance already exists from migrations
// The hoodi_tt instance already exists from migrations
// Need to sync it to database AND load into singleton
_, err := erc20shim.ForTestingForceSyncInstance(
ctx, platform,
Expand All @@ -52,7 +52,7 @@ func TestHoodiGetWithdrawalProofAction(t *testing.T) {
"0x0000000000000000000000000000000000000001", // Fake ERC20 for testing
18,
)
require.NoError(t, err, "failed to sync hoodi_bridge instance")
require.NoError(t, err, "failed to sync hoodi_tt instance")

// Load DB instances into singleton
err = erc20shim.ForTestingInitializeExtension(ctx, platform)
Expand Down Expand Up @@ -106,7 +106,7 @@ func TestHoodiGetWithdrawalProofAction(t *testing.T) {
)
require.NoError(t, err, "failed to add validator signature")

// Call the PUBLIC ACTION hoodi_get_withdrawal_proof
// Call the PUBLIC ACTION hoodi_tt_get_withdrawal_proof
// This is the action users will call in production
engineCtx = engCtx(ctx, platform, "0x0000000000000000000000000000000000000000", 3, false)

Expand All @@ -117,7 +117,7 @@ func TestHoodiGetWithdrawalProofAction(t *testing.T) {
var proofs [][]byte
var signatures [][]byte

r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_get_withdrawal_proof",
r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_tt_get_withdrawal_proof",
[]any{testUser}, // Just wallet address parameter
func(row *common.Row) error {
proofRows++
Expand Down Expand Up @@ -216,7 +216,7 @@ func TestHoodiGetWithdrawalProofAction(t *testing.T) {
}
t.Logf("Total validator signatures: %d", len(signatures))

t.Logf("✅ Public action hoodi_get_withdrawal_proof works correctly")
t.Logf("✅ Public action hoodi_tt_get_withdrawal_proof works correctly")
t.Logf(" Chain: %s", chain)
t.Logf(" ChainID: %s", chainID)
t.Logf(" Contract: %s", contract)
Expand All @@ -230,7 +230,7 @@ func TestHoodiGetWithdrawalProofAction(t *testing.T) {

// TestHoodiGetWithdrawalProofNoPending tests that pending epochs are not returned.
func TestHoodiGetWithdrawalProofNoPending(t *testing.T) {
seedAndRun(t, "hoodi_get_withdrawal_proof_no_pending", func(ctx context.Context, platform *kwilTesting.Platform) error {
seedAndRun(t, "hoodi_tt_get_withdrawal_proof_no_pending", func(ctx context.Context, platform *kwilTesting.Platform) error {
testUser := "0xabc0000000000000000000000000000000000001"
testAmount := "50000000000000000000"

Expand Down Expand Up @@ -280,7 +280,7 @@ func TestHoodiGetWithdrawalProofNoPending(t *testing.T) {
// Call public action
engineCtx = engCtx(ctx, platform, "0x0000000000000000000000000000000000000000", 3, false)
var proofRows int
r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_get_withdrawal_proof",
r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_tt_get_withdrawal_proof",
[]any{testUser},
func(row *common.Row) error {
proofRows++
Expand All @@ -304,7 +304,7 @@ func TestHoodiGetWithdrawalProofNoPending(t *testing.T) {
// Note: Testing multiple EPOCHS requires complex timing logic not yet in test infrastructure.
// This test validates that multiple users can each retrieve their withdrawal proofs correctly.
func TestHoodiGetWithdrawalProofMultipleUsers(t *testing.T) {
seedAndRun(t, "hoodi_get_withdrawal_proof_multiple_users", func(ctx context.Context, platform *kwilTesting.Platform) error {
seedAndRun(t, "hoodi_tt_get_withdrawal_proof_multiple_users", func(ctx context.Context, platform *kwilTesting.Platform) error {
userA := "0xabc0000000000000000000000000000000000001"
userB := "0xabc0000000000000000000000000000000000002"
amount100 := "100000000000000000000"
Expand Down Expand Up @@ -405,7 +405,7 @@ func TestHoodiGetWithdrawalProofMultipleUsers(t *testing.T) {
var proofRowsA int
var amountA string

r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_get_withdrawal_proof",
r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_tt_get_withdrawal_proof",
[]any{userA},
func(row *common.Row) error {
proofRowsA++
Expand All @@ -424,7 +424,7 @@ func TestHoodiGetWithdrawalProofMultipleUsers(t *testing.T) {
var proofRowsB int
var amountB string

r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_get_withdrawal_proof",
r, err = platform.Engine.Call(engineCtx, platform.DB, "", "hoodi_tt_get_withdrawal_proof",
[]any{userB},
func(row *common.Row) error {
proofRowsB++
Expand Down
Loading
Loading