From e0cca2666075fbcd15ca6c9be939358ce39a6af0 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Wed, 29 Jul 2026 10:16:05 +0700 Subject: [PATCH] chore: name the bridge a transfer charged its fee in --- .../002-public-transfer-actions.prod.sql | 14 ++++++-- .../002-public-transfer-actions.sql | 8 ++--- .../streams/transaction_events_ledger_test.go | 35 ++++++++++++++++++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/internal/migrations/erc20-bridge/002-public-transfer-actions.prod.sql b/internal/migrations/erc20-bridge/002-public-transfer-actions.prod.sql index 165942f4..5e0d46b8 100644 --- a/internal/migrations/erc20-bridge/002-public-transfer-actions.prod.sql +++ b/internal/migrations/erc20-bridge/002-public-transfer-actions.prod.sql @@ -15,6 +15,11 @@ -- fee is paid in the SAME bridge as the transfer (not always in TRUF). -- This avoids forcing USDC senders to also hold TRUF. -- +-- Because of that, and because transaction_events carries no token column, both +-- actions record the bridge in the ledger event's metadata. Without it the two +-- fees are indistinguishable in the ledger and a reader has to infer the token +-- from the amount, which only works while the two fees stay different constants. +-- -- Manual-apply mainnet override. The embedded migration loader skips -- *.prod.sql, so apply via: -- @@ -58,11 +63,14 @@ CREATE OR REPLACE ACTION eth_truf_transfer($to_address TEXT, $amount TEXT) PUBLI -- Execute transfer using the bridge extension eth_truf.transfer($to_address, $amount::NUMERIC(78, 0)); + -- The fee is paid in the bridge that moved, not always in $TRUF, and + -- transaction_events has no token column. Naming the bridge here is what lets + -- a reader tell this row's 1 TRUF from eth_usdc_transfer's 1 USDC. record_transaction_event( 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"eth_truf"}' ); }; @@ -100,10 +108,12 @@ CREATE OR REPLACE ACTION eth_usdc_transfer($to_address TEXT, $amount TEXT) PUBLI -- Execute transfer using the bridge extension eth_usdc.transfer($to_address, $amount::NUMERIC(78, 0)); + -- See eth_truf_transfer above: without this the 6-decimal fee is + -- indistinguishable from an 18-decimal one in the ledger. record_transaction_event( 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"eth_usdc"}' ); }; diff --git a/internal/migrations/erc20-bridge/002-public-transfer-actions.sql b/internal/migrations/erc20-bridge/002-public-transfer-actions.sql index 3066cba2..a059982f 100644 --- a/internal/migrations/erc20-bridge/002-public-transfer-actions.sql +++ b/internal/migrations/erc20-bridge/002-public-transfer-actions.sql @@ -37,7 +37,7 @@ CREATE OR REPLACE ACTION sepolia_transfer($to_address TEXT, $amount TEXT) PUBLIC 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"sepolia_bridge"}' ); }; @@ -78,7 +78,7 @@ CREATE OR REPLACE ACTION ethereum_transfer($to_address TEXT, $amount TEXT) PUBLI 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"ethereum_bridge"}' ); }; @@ -119,7 +119,7 @@ CREATE OR REPLACE ACTION hoodi_tt_transfer($to_address TEXT, $amount TEXT) PUBLI 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"hoodi_tt"}' ); }; @@ -160,6 +160,6 @@ CREATE OR REPLACE ACTION hoodi_tt2_transfer($to_address TEXT, $amount TEXT) PUBL 4, $fee, '0x' || $leader_hex, - NULL + '{"bridge":"hoodi_tt2"}' ); }; diff --git a/tests/streams/transaction_events_ledger_test.go b/tests/streams/transaction_events_ledger_test.go index ece358d9..8d2c4aa9 100644 --- a/tests/streams/transaction_events_ledger_test.go +++ b/tests/streams/transaction_events_ledger_test.go @@ -204,6 +204,19 @@ func runTransactionEventsLedgerScenario(t *testing.T) func(ctx context.Context, require.NoError(t, err) height++ + // A second transfer on a different bridge. Two transfer actions booking + // two different bridges under the same method is the whole point of + // recording one: the amount alone cannot tell them apart once a bridge + // with different decimals exists. The actor is already funded on + // hoodi_tt for the write fees above, so this needs no extra setup. + ttTransferLeaderPub, ttTransferLeaderAddr := newLeader(t) + ttTransferTx, err := callActionWithLeader(ctx, platform, actor, ttTransferLeaderPub, height, "hoodi_tt_transfer", []any{ + receiver.Address(), + transferAmount, + }) + require.NoError(t, err) + height++ + withdrawLeaderPub, withdrawLeaderAddr := newLeader(t) withdrawTx, err := callActionWithLeader(ctx, platform, actor, withdrawLeaderPub, height, "sepolia_bridge_tokens", []any{ actor.Address(), @@ -238,6 +251,17 @@ func runTransactionEventsLedgerScenario(t *testing.T) func(ctx context.Context, require.Empty(t, meta, "expected no metadata for ledger event") } + // A transfer is the one method that does not always charge its fee in + // $TRUF: it charges in the bridge it moves. The ledger has no token + // column, so unless each action names its own bridge a reader cannot + // tell a 1 TRUF fee from a 1 USDC one, and the two differ by 1e12. + assertBridge := func(want string) func(meta metadataMap) { + return func(meta metadataMap) { + require.Equal(t, want, meta.String("bridge"), + "a transfer must name the bridge its fee was charged in") + } + } + expected := map[string]ledgerExpectation{ createTx: { method: "deployStream", @@ -274,7 +298,16 @@ func runTransactionEventsLedgerScenario(t *testing.T) func(ctx context.Context, feeDistributions: []string{ buildDistribution(transferLeaderAddr, feeOneTRUF), }, - assertMetadata: assertNoMetadata, + assertMetadata: assertBridge("sepolia_bridge"), + }, + ttTransferTx: { + method: "transferTN", + fee: feeOneTRUF, + feeRecipient: ttTransferLeaderAddr, + feeDistributions: []string{ + buildDistribution(ttTransferLeaderAddr, feeOneTRUF), + }, + assertMetadata: assertBridge("hoodi_tt"), }, withdrawTx: { method: "withdrawTN",