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
34 changes: 19 additions & 15 deletions extensions/tn_settlement/settlement_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ func testFindUnsettledMarkets(t *testing.T) func(context.Context, *kwilTesting.P
require.NoError(t, err)

// Give TRUF balance for market creation fee
// Cover one market's worth of fees: create_stream (1) + insert_records
// (1) + request_attestation (40) = 42 TRUF. Fund 100 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "100000000000000000000") // 100 TRUF
// Cover one market's worth of fees: create_stream (100, #3971) +
// insert_records (1) + request_attestation (40) = 141 TRUF.
// Fund 200 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "200000000000000000000") // 200 TRUF
require.NoError(t, err)

// Create stream and attestation - returns query_components (ABI-encoded)
Expand Down Expand Up @@ -165,9 +166,10 @@ func testAttestationExists(t *testing.T) func(context.Context, *kwilTesting.Plat
require.NoError(t, err)

// Give TRUF balance for market creation fee
// Cover one market's worth of fees: create_stream (1) + insert_records
// (1) + request_attestation (40) = 42 TRUF. Fund 100 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "100000000000000000000") // 100 TRUF
// Cover one market's worth of fees: create_stream (100, #3971) +
// insert_records (1) + request_attestation (40) = 141 TRUF.
// Fund 200 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "200000000000000000000") // 200 TRUF
require.NoError(t, err)

streamID := "stattexists000000000000000000000"
Expand Down Expand Up @@ -242,9 +244,10 @@ func testSettleMarketViaAction(t *testing.T) func(context.Context, *kwilTesting.
require.NoError(t, err)

// Give TRUF balance for market creation fee
// Cover one market's worth of fees: create_stream (1) + insert_records
// (1) + request_attestation (40) = 42 TRUF. Fund 100 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "100000000000000000000") // 100 TRUF
// Cover one market's worth of fees: create_stream (100, #3971) +
// insert_records (1) + request_attestation (40) = 141 TRUF.
// Fund 200 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "200000000000000000000") // 200 TRUF
require.NoError(t, err)

streamID := "stsettleaction000000000000000000"
Expand Down Expand Up @@ -356,9 +359,10 @@ func testSkipMarketWithoutAttestation(t *testing.T) func(context.Context, *kwilT
require.NoError(t, err)

// Give TRUF balance for market creation fee
// Cover one market's worth of fees: create_stream (1) + insert_records
// (1) + request_attestation (40) = 42 TRUF. Fund 100 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "100000000000000000000") // 100 TRUF
// Cover one market's worth of fees: create_stream (100, #3971) +
// insert_records (1) + request_attestation (40) = 141 TRUF.
// Fund 200 TRUF for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "200000000000000000000") // 200 TRUF
require.NoError(t, err)

// Create stream and attestation WITHOUT signing (skip the SignAttestation step)
Expand Down Expand Up @@ -443,10 +447,10 @@ func testMultipleMarketsProcessing(t *testing.T) func(context.Context, *kwilTest
err = erc20bridge.ForTestingInitializeExtension(ctx, platform)
require.NoError(t, err)

// Cover three markets' worth of fees: 3 × (create_stream 1 +
// insert_records 1 + request_attestation 40) = 126 TRUF. Fund 500 TRUF
// Cover three markets' worth of fees: 3 × (create_stream 100, #3971 +
// insert_records 1 + request_attestation 40) = 423 TRUF. Fund 600 TRUF
// for headroom.
err = giveTrufBalance(ctx, platform, deployer.Address(), "500000000000000000000") // 500 TRUF
err = giveTrufBalance(ctx, platform, deployer.Address(), "600000000000000000000") // 600 TRUF
require.NoError(t, err)

// Create 3 markets (settleTime in future relative to BlockContext)
Expand Down
38 changes: 15 additions & 23 deletions internal/migrations/001-common-actions.prod.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,26 @@ CREATE OR REPLACE ACTION create_streams(
}

-- ===== FEE COLLECTION =====
-- Flat 1 TRUF per transaction (write-fee policy per issue #3805).
-- Phased rollout: only wallets enrolled in `system:fee_required`
-- are charged. Empty role => no caller is charged. Once every
-- active write wallet is enrolled, drop this gate in a follow-up
-- migration so universal charging resumes.
$total_fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TRUF with 18 decimals
-- Per-stream write fee per issue #3971. Charged universally — no role gate.
-- Streams aren't truncated by the daily digest, so per-stream pricing
-- ensures storage cost scales with what the caller actually creates.
$per_stream_fee NUMERIC(78, 0) := '100000000000000000000'::NUMERIC(78, 0); -- 100 TRUF (10^20)
$total_fee NUMERIC(78, 0) := $per_stream_fee * array_length($stream_ids)::NUMERIC(78, 0);

$fee_required BOOL := FALSE;
for $r in are_members_of('system', 'fee_required', ARRAY[$lower_caller]) {
$fee_required := $r.is_member;
IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex := encode(@leader_sender, 'hex')::TEXT;

IF $fee_required {
IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex := encode(@leader_sender, 'hex')::TEXT;

$caller_balance := eth_truf.balance(@caller);
$caller_balance := eth_truf.balance(@caller);

IF $caller_balance < $total_fee {
ERROR('Insufficient balance for stream creation. Required: 1 TRUF');
}

eth_truf.transfer($leader_hex, $total_fee);
$fee_total := $total_fee;
$fee_recipient := '0x' || $leader_hex;
IF $caller_balance < $total_fee {
ERROR('Insufficient balance for stream creation. Required: 100 TRUF per stream');
}

eth_truf.transfer($leader_hex, $total_fee);
$fee_total := $total_fee;
$fee_recipient := '0x' || $leader_hex;
-- ===== END FEE COLLECTION =====

-- ===== STREAM CREATION LOGIC =====
Expand Down
45 changes: 20 additions & 25 deletions internal/migrations/001-common-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ CREATE OR REPLACE ACTION create_stream(

/**
* create_streams: Creates multiple streams at once.
* Fee: 1 TRUF flat per transaction (charged to every caller, no exemptions)
* Fee: 100 TRUF per stream (issue #3971). Charged on every caller —
* no role exemption. The total transferred is
* 100 TRUF × array_length($stream_ids), bounded only by the caller's
* TRUF balance.
* Validates stream_id format, data provider address, and stream type.
* Sets default metadata including type, owner, visibility, and readonly keys.
*
Expand Down Expand Up @@ -90,34 +93,26 @@ CREATE OR REPLACE ACTION create_streams(
}

-- ===== FEE COLLECTION =====
-- Flat 1 TRUF per transaction (write-fee policy per issue #3805).
-- Phased rollout: only wallets enrolled in `system:fee_required`
-- are charged. Empty role => no caller is charged. Once every
-- active write wallet is enrolled, drop this gate in a follow-up
-- migration so universal charging resumes.
$total_fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TRUF with 18 decimals

$fee_required BOOL := FALSE;
for $r in are_members_of('system', 'fee_required', ARRAY[$lower_caller]) {
$fee_required := $r.is_member;
-- Per-stream write fee per issue #3971. Charged universally — no role gate.
-- Streams aren't truncated by the daily digest, so per-stream pricing
-- ensures storage cost scales with what the caller actually creates.
$per_stream_fee NUMERIC(78, 0) := '100000000000000000000'::NUMERIC(78, 0); -- 100 TRUF (10^20)
$total_fee NUMERIC(78, 0) := $per_stream_fee * array_length($stream_ids)::NUMERIC(78, 0);

IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex := encode(@leader_sender, 'hex')::TEXT;

IF $fee_required {
IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex := encode(@leader_sender, 'hex')::TEXT;

$caller_balance := hoodi_tt.balance(@caller);
$caller_balance := hoodi_tt.balance(@caller);

IF $caller_balance < $total_fee {
ERROR('Insufficient balance for stream creation. Required: 1 TRUF');
}

hoodi_tt.transfer($leader_hex, $total_fee);
$fee_total := $total_fee;
$fee_recipient := '0x' || $leader_hex;
IF $caller_balance < $total_fee {
ERROR('Insufficient balance for stream creation. Required: 100 TRUF per stream');
}

hoodi_tt.transfer($leader_hex, $total_fee);
$fee_total := $total_fee;
$fee_recipient := '0x' || $leader_hex;
-- ===== END FEE COLLECTION =====

-- ===== STREAM CREATION LOGIC =====
Expand Down
5 changes: 3 additions & 2 deletions tests/streams/allow_zeros_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,9 @@ func createStreamWithAllowZeros(ctx context.Context, platform *kwilTesting.Platf
return errors.Wrap(err, "invalid data provider address")
}

// Fund for the universal create_stream fee — mirror setup.UntypedCreateStream.
if err := feefund.EnsureWalletFunded(ctx, platform, addr.Address(), feefund.WriteFeeWei); err != nil {
// Fund for the universal create_stream per-stream fee (issue #3971) —
// mirror setup.UntypedCreateStream.
if err := feefund.EnsureWalletFunded(ctx, platform, addr.Address(), feefund.StreamCreationFeeWei); err != nil {
return errors.Wrap(err, "fund wallet for create_stream fee")
}

Expand Down
6 changes: 3 additions & 3 deletions tests/streams/order_book/settlement_payout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ func testWinnerReceivesFullPayout(t *testing.T) func(context.Context, *kwilTesti
require.NoError(t, err)

// Fund DP on the sepolia_bridge (the dev TRUF substitute) for
// create_stream (6 TRUF), insert_records (6 TRUF), request_attestation
// (40 TRUF). Give 100 TRUF to leave headroom.
err = feefund.EnsureWalletFunded(ctx, platform, dpAddr.Address(), "100000000000000000000")
// create_stream (100 TRUF, #3971), insert_records (1 TRUF), and
// request_attestation (40 TRUF). 200 TRUF leaves headroom.
err = feefund.EnsureWalletFunded(ctx, platform, dpAddr.Address(), "200000000000000000000")
require.NoError(t, err)

// Get USDC balance before market operations
Expand Down
Loading
Loading