@@ -32,6 +32,10 @@ pub enum DataKey {
3232 Dispute ( u64 ) ,
3333 /// The id of the currently active (unresolved) dispute for a stream, if any.
3434 ActiveDispute ( u64 ) ,
35+ /// Configured fee-tier list for volume-based protocol fee discounts (instance storage).
36+ FeeTiers ,
37+ /// Cumulative escrowed volume for a sender address, used for fee-tier eligibility (persistent storage).
38+ SenderVolume ( Address ) ,
3539}
3640
3741/// Stream status enum
@@ -103,18 +107,17 @@ pub struct ProtocolMetrics {
103107 pub total_delegations : u64 , // Total number of delegations across all streams
104108}
105109
106- /// Fee tier threshold and fee rate pair .
110+ /// Fee tier configuration for volume-based protocol fee discounts .
107111///
108- /// Represents: "if the donor's cumulative volume is `>= threshold`, apply
109- /// `fee_rate`". The tier list is sorted by strictly increasing thresholds;
110- /// the first tier must have a threshold of `0` so every donor is covered, and
111- /// fee rates must be monotonically non-increasing across tiers.
112+ /// Tiers are stored sorted ascending by `min_volume`. The highest tier
113+ /// whose `min_volume` is <= the sender's cumulative stream volume determines
114+ /// their protocol fee rate at withdrawal time.
112115#[ contracttype]
113116#[ derive( Clone ) ]
114117pub struct FeeTier {
115- /// Minimum cumulative volume to qualify for this tier.
116- pub threshold : i128 ,
117- /// Fee rate in basis points applied for this tier (e.g., 250 = 2.5 %).
118+ /// Minimum cumulative stream volume (token units) required for this tier
119+ pub min_volume : i128 ,
120+ /// Protocol fee rate in basis points (e.g., 30 = 0.3%, max 500 = 5%)
118121 pub fee_rate : u32 ,
119122}
120123
@@ -309,11 +312,16 @@ pub enum Error {
309312 InvalidTierConfiguration = 32 ,
310313 /// Fee rates across tiers are not monotonically non-increasing
311314 TierFeeNotMonotonic = 33 ,
315+ /// A tier configuration was invalid (rate exceeds max, bad sort order, etc.)
316+ InvalidTier = 34 ,
317+ /// More than MAX_TIERS entries were supplied
318+ TooManyTiers = 35 ,
312319}
313320
314321// Constants
315322const MAX_FEE : u32 = 500 ; // 5% in basis points
316323const MAX_STREAMS_PER_BATCH : u32 = 50 ; // max streams per create_batch_streams call
324+ const MAX_TIERS : u32 = 10 ; // max configurable fee tiers
317325const LEDGER_THRESHOLD : u32 = 518400 ; // ~30 days at 5s/ledger
318326const LEDGER_BUMP : u32 = 535680 ; // ~31 days
319327const DISPUTE_TIMELOCK_DELAY : u64 = 172800 ; // 48 hours in seconds
@@ -359,12 +367,12 @@ impl PaymentStreamContract {
359367 // Tier 2 (500,000+): 100 bps (1.0 %)
360368 let default_tiers: Vec < FeeTier > = {
361369 let mut v = Vec :: new ( & env) ;
362- v. push_back ( FeeTier { threshold : 0 , fee_rate : 500 } ) ;
363- v. push_back ( FeeTier { threshold : 50_000 , fee_rate : 250 } ) ;
364- v. push_back ( FeeTier { threshold : 500_000 , fee_rate : 100 } ) ;
370+ v. push_back ( FeeTier { min_volume : 0 , fee_rate : 500 } ) ;
371+ v. push_back ( FeeTier { min_volume : 50_000 , fee_rate : 250 } ) ;
372+ v. push_back ( FeeTier { min_volume : 500_000 , fee_rate : 100 } ) ;
365373 v
366374 } ;
367- env. storage ( ) . instance ( ) . set ( & Symbol :: new ( & env , "fee_tiers" ) , & default_tiers) ;
375+ env. storage ( ) . instance ( ) . set ( & DataKey :: FeeTiers , & default_tiers) ;
368376
369377 // Initialize protocol metrics
370378 let initial_metrics = ProtocolMetrics {
@@ -392,6 +400,63 @@ impl PaymentStreamContract {
392400 }
393401 }
394402
403+ /// Resolve the applicable fee rate for a sender based on their cumulative
404+ /// stream volume and the configured fee tiers.
405+ ///
406+ /// Tiers are walked in ascending order of `min_volume`; the last qualifying
407+ /// entry wins. Falls back to the flat `FeeRate` when no tiers are configured
408+ /// or when the sender's volume is below all tier thresholds.
409+ fn get_applicable_fee_rate_internal ( env : & Env , sender : & Address ) -> u32 {
410+ let general_rate: u32 = env. storage ( ) . instance ( )
411+ . get ( & DataKey :: FeeRate )
412+ . unwrap_or ( 0 ) ;
413+
414+ let tiers: Vec < FeeTier > = env. storage ( ) . instance ( )
415+ . get ( & DataKey :: FeeTiers )
416+ . unwrap_or_else ( || Vec :: new ( env) ) ;
417+
418+ if tiers. is_empty ( ) {
419+ return general_rate;
420+ }
421+
422+ let sender_vol_key = DataKey :: SenderVolume ( sender. clone ( ) ) ;
423+ let volume: i128 = env. storage ( ) . persistent ( )
424+ . get ( & sender_vol_key)
425+ . unwrap_or ( 0 ) ;
426+ if volume > 0 {
427+ env. storage ( ) . persistent ( ) . extend_ttl ( & sender_vol_key, LEDGER_THRESHOLD , LEDGER_BUMP ) ;
428+ }
429+
430+ // Start with the general rate; upgrade to each qualifying tier
431+ let mut applicable_rate = general_rate;
432+ let len = tiers. len ( ) ;
433+ for i in 0 ..len {
434+ let tier = tiers. get ( i) . unwrap ( ) ;
435+ if volume >= tier. min_volume {
436+ applicable_rate = tier. fee_rate ;
437+ }
438+ }
439+ applicable_rate
440+ }
441+
442+ /// Calculate the protocol fee for a withdrawal amount.
443+ ///
444+ /// Uses the sender's cumulative stream volume to select the applicable
445+ /// fee tier, rewarding high-volume ecosystem donors with lower fees.
446+ fn calculate_protocol_fee ( env : & Env , amount : i128 , sender : & Address ) -> i128 {
447+ let fee_rate = Self :: get_applicable_fee_rate_internal ( env, sender) ;
448+
449+ if fee_rate == 0 {
450+ return 0 ;
451+ }
452+
453+ // fee = (amount * fee_rate) / 10000
454+ // Split to avoid i128 overflow while preserving precision
455+ let rate = fee_rate as i128 ;
456+ let fee = ( amount / 10000 ) * rate + ( ( amount % 10000 ) * rate) / 10000 ;
457+ fee. max ( 0 )
458+ }
459+
395460 /// Activate the global emergency pause switch.
396461 ///
397462 /// When active, all calls to `create_stream`, `deposit`, `withdraw`, and
@@ -735,13 +800,17 @@ impl PaymentStreamContract {
735800 env. storage ( ) . persistent ( ) . extend_ttl ( & DataKey :: Stream ( stream_id) , LEDGER_THRESHOLD , LEDGER_BUMP ) ;
736801 env. storage ( ) . persistent ( ) . extend_ttl ( & DataKey :: Metrics ( stream_id) , LEDGER_THRESHOLD , LEDGER_BUMP ) ;
737802
738- // Update donor cumulative volume for fee tier calculation
739- let donor_volume_key = ( Symbol :: new ( & env, "donor_volume" ) , sender. clone ( ) ) ;
740- let current_volume: i128 = env. storage ( ) . persistent ( ) . get ( & donor_volume_key) . unwrap_or ( 0 ) ;
741- let new_volume = current_volume. checked_add ( total_amount)
742- . unwrap_or_else ( || panic_with_error ! ( & env, Error :: ArithmeticOverflow ) ) ;
743- env. storage ( ) . persistent ( ) . set ( & donor_volume_key, & new_volume) ;
744- env. storage ( ) . persistent ( ) . extend_ttl ( & donor_volume_key, LEDGER_THRESHOLD , LEDGER_BUMP ) ;
803+ // Track sender's cumulative escrowed volume for fee tier eligibility.
804+ // Only funds actually escrowed (initial_amount) are counted here to
805+ // prevent tier-gaming via zero-deposit streams. Additional deposits
806+ // via deposit() also increment this counter.
807+ let sender_vol_key = DataKey :: SenderVolume ( sender. clone ( ) ) ;
808+ let current_vol: i128 = env. storage ( ) . persistent ( )
809+ . get ( & sender_vol_key)
810+ . unwrap_or ( 0 ) ;
811+ let new_vol = current_vol. saturating_add ( initial_amount) ;
812+ env. storage ( ) . persistent ( ) . set ( & sender_vol_key, & new_vol) ;
813+ env. storage ( ) . persistent ( ) . extend_ttl ( & sender_vol_key, LEDGER_THRESHOLD , LEDGER_BUMP ) ;
745814
746815 // Update protocol metrics
747816 let mut protocol_metrics: ProtocolMetrics = env. storage ( ) . instance ( )
@@ -813,6 +882,18 @@ impl PaymentStreamContract {
813882 env. storage ( ) . persistent ( ) . set ( & DataKey :: Metrics ( stream_id) , & metrics) ;
814883 env. storage ( ) . persistent ( ) . extend_ttl ( & DataKey :: Metrics ( stream_id) , LEDGER_THRESHOLD , LEDGER_BUMP ) ;
815884
885+ // Update sender's cumulative volume for fee tier eligibility.
886+ // deposit() actually moves tokens into escrow, so each deposited
887+ // amount counts toward the sender's volume just as initial_amount
888+ // does at stream creation.
889+ let sender_vol_key = DataKey :: SenderVolume ( stream. sender . clone ( ) ) ;
890+ let current_vol: i128 = env. storage ( ) . persistent ( )
891+ . get ( & sender_vol_key)
892+ . unwrap_or ( 0 ) ;
893+ let new_vol = current_vol. saturating_add ( amount) ;
894+ env. storage ( ) . persistent ( ) . set ( & sender_vol_key, & new_vol) ;
895+ env. storage ( ) . persistent ( ) . extend_ttl ( & sender_vol_key, LEDGER_THRESHOLD , LEDGER_BUMP ) ;
896+
816897 // Emit StreamDeposit event
817898 StreamDepositEvent { stream_id, amount } . publish ( & env) ;
818899 }
@@ -1156,48 +1237,6 @@ impl PaymentStreamContract {
11561237 env. storage ( ) . persistent ( ) . get ( & DataKey :: Delegate ( stream_id) )
11571238 }
11581239
1159- /// Calculate the protocol fee for a given withdrawal based on the
1160- /// donor's cumulative volume and the configured fee tiers.
1161- ///
1162- /// Falls back to the flat `FeeRate` if no tier list has been configured
1163- /// (should not happen after `initialize`).
1164- fn calculate_protocol_fee ( env : & Env , donor : & Address , amount : i128 ) -> i128 {
1165- if amount <= 0 {
1166- return 0 ;
1167- }
1168-
1169- // Get donor's cumulative volume
1170- let donor_volume_key = ( Symbol :: new ( env, "donor_volume" ) , donor. clone ( ) ) ;
1171- let cumulative_volume: i128 = env. storage ( ) . persistent ( ) . get ( & donor_volume_key) . unwrap_or ( 0 ) ;
1172-
1173- // Get fee tiers (fallback to flat fee if tiers not configured)
1174- let tiers: Vec < FeeTier > = match env. storage ( ) . instance ( ) . get ( & Symbol :: new ( env, "fee_tiers" ) ) {
1175- Some ( tiers) => tiers,
1176- None => {
1177- // Fallback: use general fee rate if tiers not set up
1178- let fee_rate: u32 = env. storage ( ) . instance ( ) . get ( & DataKey :: FeeRate ) . unwrap_or ( 0 ) ;
1179- let rate = fee_rate as i128 ;
1180- return ( amount / 10000 ) * rate + ( ( amount % 10000 ) * rate) / 10000 ;
1181- }
1182- } ;
1183-
1184- // Determine applicable tier
1185- let mut applicable_fee_rate: u32 = 0 ;
1186- for tier in tiers. iter ( ) {
1187- if cumulative_volume >= tier. threshold {
1188- applicable_fee_rate = tier. fee_rate ;
1189- }
1190- }
1191-
1192- // Calculate fee with the determined rate
1193- if applicable_fee_rate == 0 {
1194- return 0 ;
1195- }
1196-
1197- let rate = applicable_fee_rate as i128 ;
1198- let fee = ( amount / 10000 ) * rate + ( ( amount % 10000 ) * rate) / 10000 ;
1199- fee. max ( 0 )
1200- }
12011240
12021241 /// Calculate withdrawable amount for a stream
12031242 pub fn withdrawable_amount ( env : Env , stream_id : u64 ) -> i128 {
@@ -1263,7 +1302,7 @@ impl PaymentStreamContract {
12631302 }
12641303
12651304 // Calculate protocol fee based on donor's cumulative volume
1266- let fee = Self :: calculate_protocol_fee ( & env, & stream. sender , amount ) ;
1305+ let fee = Self :: calculate_protocol_fee ( & env, amount , & stream. sender ) ;
12671306 let net_amount = amount - fee;
12681307
12691308 stream. withdrawn_amount += amount;
@@ -1465,6 +1504,11 @@ impl PaymentStreamContract {
14651504 }
14661505
14671506 /// Set the protocol fee rate
1507+ ///
1508+ /// # Errors
1509+ /// * [`Error::FeeTooHigh`] — `new_fee_rate > MAX_FEE`.
1510+ /// * [`Error::InvalidTier`] — `new_fee_rate` is below the first configured
1511+ /// tier's fee_rate (would violate the non-increasing invariant).
14681512 pub fn set_protocol_fee_rate ( env : Env , new_fee_rate : u32 ) {
14691513 let admin: Address = env. storage ( ) . instance ( ) . get ( & DataKey :: Admin ) . unwrap ( ) ;
14701514 admin. require_auth ( ) ;
@@ -1473,6 +1517,20 @@ impl PaymentStreamContract {
14731517 panic_with_error ! ( & env, Error :: FeeTooHigh ) ;
14741518 }
14751519
1520+ // Preserve the non-increasing fee-rate invariant: the general rate must
1521+ // be >= every configured tier rate. The first tier has the highest
1522+ // fee_rate among all tiers (since tiers are stored non-increasing), so
1523+ // checking only the first tier is sufficient.
1524+ let tiers: Vec < FeeTier > = env. storage ( ) . instance ( )
1525+ . get ( & DataKey :: FeeTiers )
1526+ . unwrap_or_else ( || Vec :: new ( & env) ) ;
1527+ if !tiers. is_empty ( ) {
1528+ let first_tier = tiers. get ( 0 ) . unwrap ( ) ;
1529+ if new_fee_rate < first_tier. fee_rate {
1530+ panic_with_error ! ( & env, Error :: InvalidTier ) ;
1531+ }
1532+ }
1533+
14761534 env. storage ( ) . instance ( ) . set ( & DataKey :: FeeRate , & new_fee_rate) ;
14771535 env. storage ( ) . instance ( ) . extend_ttl ( LEDGER_THRESHOLD , LEDGER_BUMP ) ;
14781536 }
@@ -1496,17 +1554,18 @@ impl PaymentStreamContract {
14961554 env. storage ( ) . instance ( ) . get ( & DataKey :: FeeCollector ) . unwrap ( )
14971555 }
14981556
1499- /// Replace the fee-tier configuration (admin only).
1557+ /// Set fee tiers for volume-based protocol fee discounts (admin only).
15001558 ///
1501- /// `tiers` must be a non-empty list whose first entry has a threshold of
1502- /// `0`, whose thresholds are strictly increasing, and whose fee rates are
1503- /// monotonically non-increasing and at most [`MAX_FEE`] .
1559+ /// `tiers` must be sorted in **strictly ascending** order by `min_volume`.
1560+ /// Each tier's `fee_rate` must not exceed `MAX_FEE` (500 bps = 5%).
1561+ /// Pass an empty Vec to clear all tiers and revert to the flat `FeeRate` .
15041562 ///
15051563 /// # Errors
1506- /// * [`Error::InvalidTierConfiguration`] — empty list, first threshold not
1507- /// zero, or thresholds not strictly increasing.
1508- /// * [`Error::TierFeeNotMonotonic`] — fee rates increase across tiers.
1509- /// * [`Error::FeeTooHigh`] — a tier fee rate exceeds 500 bps.
1564+ /// * [`Error::Unauthorized`] — caller is not the contract admin.
1565+ /// * [`Error::TooManyTiers`] — `tiers.len() > MAX_TIERS` (10).
1566+ /// * [`Error::InvalidTier`] — a tier's `fee_rate` exceeds `MAX_FEE`, or tiers
1567+ /// are not strictly sorted ascending by `min_volume`, or fee rates are
1568+ /// not monotonically non-increasing.
15101569 pub fn set_fee_tiers ( env : Env , tiers : Vec < FeeTier > ) {
15111570 let admin: Address = env
15121571 . storage ( )
@@ -1515,56 +1574,60 @@ impl PaymentStreamContract {
15151574 . unwrap_or_else ( || panic_with_error ! ( & env, Error :: NotInitialized ) ) ;
15161575 admin. require_auth ( ) ;
15171576
1518- // Validate that tiers is not empty
1519- if tiers. is_empty ( ) {
1520- panic_with_error ! ( & env, Error :: InvalidTierConfiguration ) ;
1521- }
1522-
1523- // Validate first tier has threshold 0
1524- if let Some ( first_tier) = tiers. first ( ) {
1525- if first_tier. threshold != 0 {
1526- panic_with_error ! ( & env, Error :: InvalidTierConfiguration ) ;
1527- }
1577+ if tiers. len ( ) > MAX_TIERS {
1578+ panic_with_error ! ( & env, Error :: TooManyTiers ) ;
15281579 }
15291580
1530- // Validate thresholds are strictly increasing
1531- let mut prev_threshold: i128 = -1 ;
1532- for tier in tiers. iter ( ) {
1533- if tier. threshold <= prev_threshold {
1534- panic_with_error ! ( & env, Error :: InvalidTierConfiguration ) ;
1581+ let general_rate: u32 = env. storage ( ) . instance ( )
1582+ . get ( & DataKey :: FeeRate )
1583+ . unwrap_or ( 0 ) ;
1584+ let mut prev_min_volume: i128 = -1_i128 ;
1585+ let mut prev_fee_rate: u32 = general_rate;
1586+ let len = tiers. len ( ) ;
1587+ for i in 0 ..len {
1588+ let tier = tiers. get ( i) . unwrap ( ) ;
1589+ if tier. fee_rate > MAX_FEE {
1590+ panic_with_error ! ( & env, Error :: InvalidTier ) ;
15351591 }
1536- prev_threshold = tier. threshold ;
1537- }
1538-
1539- // Validate fee rates are monotonically non-increasing and within MAX_FEE
1540- let mut prev_fee_rate: u32 = u32:: MAX ;
1541- for tier in tiers. iter ( ) {
15421592 if tier. fee_rate > prev_fee_rate {
1543- panic_with_error ! ( & env, Error :: TierFeeNotMonotonic ) ;
1593+ panic_with_error ! ( & env, Error :: InvalidTier ) ;
15441594 }
1545- if tier. fee_rate > MAX_FEE {
1546- panic_with_error ! ( & env, Error :: FeeTooHigh ) ;
1595+ if tier. min_volume <= prev_min_volume {
1596+ panic_with_error ! ( & env, Error :: InvalidTier ) ;
15471597 }
1598+ prev_min_volume = tier. min_volume ;
15481599 prev_fee_rate = tier. fee_rate ;
15491600 }
15501601
1551- // Store the new tier configuration
1552- env. storage ( ) . instance ( ) . set ( & Symbol :: new ( & env, "fee_tiers" ) , & tiers) ;
1602+ env. storage ( ) . instance ( ) . set ( & DataKey :: FeeTiers , & tiers) ;
15531603 env. storage ( ) . instance ( ) . extend_ttl ( LEDGER_THRESHOLD , LEDGER_BUMP ) ;
1604+
1605+ env. events ( ) . publish ( ( "FeeTiersUpdated" , ) , ( ) ) ;
15541606 }
15551607
1556- /// Get the current fee-tier configuration.
1608+ /// Return the currently configured fee tiers.
1609+ /// Returns an empty Vec when no tiers have been configured.
15571610 pub fn get_fee_tiers ( env : Env ) -> Vec < FeeTier > {
1558- match env. storage ( ) . instance ( ) . get ( & Symbol :: new ( & env, "fee_tiers" ) ) {
1559- Some ( tiers) => tiers,
1560- None => Vec :: new ( & env) ,
1561- }
1611+ env. storage ( ) . instance ( )
1612+ . get ( & DataKey :: FeeTiers )
1613+ . unwrap_or_else ( || Vec :: new ( & env) )
1614+ }
1615+
1616+ /// Return the cumulative escrowed volume for a sender address.
1617+ ///
1618+ /// This is the sum of all tokens the sender has actually transferred into
1619+ /// escrow: the `initial_amount` at stream creation plus any subsequent
1620+ /// `deposit()` calls. It determines their position in the fee tier
1621+ /// hierarchy and cannot be gamed by declaring a large `total_amount`
1622+ /// without depositing funds.
1623+ pub fn get_sender_volume ( env : Env , sender : Address ) -> i128 {
1624+ let sender_vol_key = DataKey :: SenderVolume ( sender) ;
1625+ env. storage ( ) . persistent ( ) . get ( & sender_vol_key) . unwrap_or ( 0 )
15621626 }
15631627
1564- /// Get a donor's cumulative streamed volume, used to select their fee tier.
1565- pub fn get_donor_cumulative_volume ( env : Env , donor : Address ) -> i128 {
1566- let donor_volume_key = ( Symbol :: new ( & env, "donor_volume" ) , donor) ;
1567- env. storage ( ) . persistent ( ) . get ( & donor_volume_key) . unwrap_or ( 0 )
1628+ /// Return the applicable fee rate for a sender given their current volume.
1629+ pub fn get_applicable_fee_rate ( env : Env , sender : Address ) -> u32 {
1630+ Self :: get_applicable_fee_rate_internal ( & env, & sender)
15681631 }
15691632
15701633 /// Get stream-specific metrics
0 commit comments