@@ -165,6 +165,12 @@ pub enum DataKey {
165165 /// (singleton, `u64`, persistent). Incremented with `saturating_add`
166166 /// so it is monotonic and never panics. Defaults to `0`.
167167 TotalRoutesAllTime ,
168+ /// Protocol-wide lifetime sum of fees collected by `compute_route_fee`
169+ /// (singleton, `i128`, persistent), owed to whoever holds
170+ /// `FeeRecipient` once distributed off-chain. Accumulated with
171+ /// `saturating_add` so it is monotonic and never panics on overflow.
172+ /// Defaults to `0`.
173+ RewardsAccrued ,
168174 /// Ledger timestamp of the most recent `compute_route_fee` for a
169175 /// pair (keyed per-pair, `u64`, persistent). Used by the cooldown
170176 /// rate-limit gate. Absent reads as `None` (`Option`); `get_pair_info`
@@ -994,6 +1000,18 @@ impl StableRouteRouter {
9941000 . unwrap_or ( 0 )
9951001 }
9961002
1003+ /// Read-only view of the protocol-wide lifetime sum of fees accrued by
1004+ /// `compute_route_fee`, owed to `FeeRecipient` once distributed.
1005+ ///
1006+ /// Does not mutate storage. Returns `0` before any fee-bearing route
1007+ /// has been processed.
1008+ pub fn get_rewards_accrued ( env : Env ) -> i128 {
1009+ env. storage ( )
1010+ . persistent ( )
1011+ . get ( & DataKey :: RewardsAccrued )
1012+ . unwrap_or ( 0 )
1013+ }
1014+
9971015 /// Returns the number of successful routes executed for the specified pair.
9981016 ///
9991017 /// Returns `0` if the pair has never been routed.
@@ -1270,6 +1288,22 @@ impl StableRouteRouter {
12701288 . persistent ( )
12711289 . set ( & DataKey :: TotalRoutesAllTime , & total. saturating_add ( 1 ) ) ;
12721290
1291+ // A zero fee is not a rewards-state change: skip the write and
1292+ // the event entirely rather than recording a no-op accrual.
1293+ if fee > 0 {
1294+ let accrued: i128 = env
1295+ . storage ( )
1296+ . persistent ( )
1297+ . get ( & DataKey :: RewardsAccrued )
1298+ . unwrap_or ( 0 ) ;
1299+ let new_accrued = accrued. saturating_add ( fee) ;
1300+ env. storage ( )
1301+ . persistent ( )
1302+ . set ( & DataKey :: RewardsAccrued , & new_accrued) ;
1303+ env. events ( )
1304+ . publish ( ( symbol_short ! ( "rwd_accr" ) , ) , ( fee, new_accrued) ) ;
1305+ }
1306+
12731307 // Construct the route-count key once — shared between read and write.
12741308 let route_count_key = DataKey :: PairRouteCount ( source. clone ( ) , destination. clone ( ) ) ;
12751309 let pair_count: u64 = env
@@ -2338,6 +2372,80 @@ mod test {
23382372 assert_eq ! ( fee, 5_000 ) ;
23392373 }
23402374
2375+ // --- rewards accrual ---
2376+
2377+ #[ test]
2378+ fn test_rewards_accrued_defaults_to_zero ( ) {
2379+ let env = Env :: default ( ) ;
2380+ let ( client, _admin) = setup_initialized ( & env) ;
2381+ assert_eq ! ( client. get_rewards_accrued( ) , 0 ) ;
2382+ }
2383+
2384+ #[ test]
2385+ fn test_rewards_accrued_accumulates_across_routes ( ) {
2386+ let env = Env :: default ( ) ;
2387+ let ( client, _admin) = setup_initialized ( & env) ;
2388+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2389+ client. register_pair ( & s, & d) ;
2390+ client. set_pair_fee_bps ( & s, & d, & 50u32 ) ;
2391+ let fee1 = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2392+ assert_eq ! ( client. get_rewards_accrued( ) , fee1) ;
2393+ let fee2 = client. compute_route_fee ( & s, & d, & 2_000_000_i128 ) ;
2394+ assert_eq ! ( client. get_rewards_accrued( ) , fee1 + fee2) ;
2395+ }
2396+
2397+ #[ test]
2398+ fn test_rewards_accrued_unaffected_by_zero_fee_route ( ) {
2399+ let env = Env :: default ( ) ;
2400+ let ( client, _admin) = setup_initialized ( & env) ;
2401+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2402+ client. register_pair ( & s, & d) ;
2403+ // No fee_bps set — every route is free.
2404+ let fee = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2405+ assert_eq ! ( fee, 0 ) ;
2406+ assert_eq ! ( client. get_rewards_accrued( ) , 0 ) ;
2407+ assert_eq ! (
2408+ event_payloads( & env, symbol_short!( "rwd_accr" ) ) . len( ) ,
2409+ 0 ,
2410+ "a zero-fee route must not emit rwd_accr"
2411+ ) ;
2412+ }
2413+
2414+ #[ test]
2415+ fn test_rewards_accrued_event_carries_fee_and_running_total ( ) {
2416+ let env = Env :: default ( ) ;
2417+ let ( client, _admin) = setup_initialized ( & env) ;
2418+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2419+ client. register_pair ( & s, & d) ;
2420+ client. set_pair_fee_bps ( & s, & d, & 50u32 ) ;
2421+ let fee = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2422+ let payloads = event_payloads ( & env, symbol_short ! ( "rwd_accr" ) ) ;
2423+ assert_eq ! ( payloads. len( ) , 1 , "one rwd_accr event for one fee-bearing route" ) ;
2424+ let ( emitted_fee, emitted_total) : ( i128 , i128 ) =
2425+ soroban_sdk:: TryFromVal :: try_from_val ( & env, & payloads[ 0 ] )
2426+ . expect ( "rwd_accr payload decodes to (fee, running_total)" ) ;
2427+ assert_eq ! ( emitted_fee, fee) ;
2428+ assert_eq ! ( emitted_total, client. get_rewards_accrued( ) ) ;
2429+ }
2430+
2431+ /// Over-limit boundary: accrual must saturate at `i128::MAX` instead of
2432+ /// panicking on overflow.
2433+ #[ test]
2434+ fn test_rewards_accrued_saturates_instead_of_overflowing ( ) {
2435+ let env = Env :: default ( ) ;
2436+ let ( client, _admin, contract_id) = setup_initialized_with_id ( & env) ;
2437+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2438+ client. register_pair ( & s, & d) ;
2439+ client. set_pair_fee_bps ( & s, & d, & 50u32 ) ;
2440+ env. as_contract ( & contract_id, || {
2441+ env. storage ( )
2442+ . persistent ( )
2443+ . set ( & DataKey :: RewardsAccrued , & ( i128:: MAX - 10 ) ) ;
2444+ } ) ;
2445+ client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2446+ assert_eq ! ( client. get_rewards_accrued( ) , i128 :: MAX ) ;
2447+ }
2448+
23412449 #[ test]
23422450 fn test_compute_route_fee_is_zero_when_fee_unset ( ) {
23432451 let env = Env :: default ( ) ;
0 commit comments