@@ -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`
@@ -1020,6 +1026,18 @@ impl StableRouteRouter {
10201026 . unwrap_or ( 0 )
10211027 }
10221028
1029+ /// Read-only view of the protocol-wide lifetime sum of fees accrued by
1030+ /// `compute_route_fee`, owed to `FeeRecipient` once distributed.
1031+ ///
1032+ /// Does not mutate storage. Returns `0` before any fee-bearing route
1033+ /// has been processed.
1034+ pub fn get_rewards_accrued ( env : Env ) -> i128 {
1035+ env. storage ( )
1036+ . persistent ( )
1037+ . get ( & DataKey :: RewardsAccrued )
1038+ . unwrap_or ( 0 )
1039+ }
1040+
10231041 /// Returns the number of successful routes executed for the specified pair.
10241042 ///
10251043 /// Returns `0` if the pair has never been routed.
@@ -1296,6 +1314,22 @@ impl StableRouteRouter {
12961314 . persistent ( )
12971315 . set ( & DataKey :: TotalRoutesAllTime , & total. saturating_add ( 1 ) ) ;
12981316
1317+ // A zero fee is not a rewards-state change: skip the write and
1318+ // the event entirely rather than recording a no-op accrual.
1319+ if fee > 0 {
1320+ let accrued: i128 = env
1321+ . storage ( )
1322+ . persistent ( )
1323+ . get ( & DataKey :: RewardsAccrued )
1324+ . unwrap_or ( 0 ) ;
1325+ let new_accrued = accrued. saturating_add ( fee) ;
1326+ env. storage ( )
1327+ . persistent ( )
1328+ . set ( & DataKey :: RewardsAccrued , & new_accrued) ;
1329+ env. events ( )
1330+ . publish ( ( symbol_short ! ( "rwd_accr" ) , ) , ( fee, new_accrued) ) ;
1331+ }
1332+
12991333 // Construct the route-count key once — shared between read and write.
13001334 let route_count_key = DataKey :: PairRouteCount ( source. clone ( ) , destination. clone ( ) ) ;
13011335 let pair_count: u64 = env
@@ -2319,6 +2353,80 @@ mod test {
23192353 assert_eq ! ( fee, 5_000 ) ;
23202354 }
23212355
2356+ // --- rewards accrual ---
2357+
2358+ #[ test]
2359+ fn test_rewards_accrued_defaults_to_zero ( ) {
2360+ let env = Env :: default ( ) ;
2361+ let ( client, _admin) = setup_initialized ( & env) ;
2362+ assert_eq ! ( client. get_rewards_accrued( ) , 0 ) ;
2363+ }
2364+
2365+ #[ test]
2366+ fn test_rewards_accrued_accumulates_across_routes ( ) {
2367+ let env = Env :: default ( ) ;
2368+ let ( client, _admin) = setup_initialized ( & env) ;
2369+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2370+ client. register_pair ( & s, & d) ;
2371+ client. set_pair_fee_bps ( & s, & d, & 50u32 ) ;
2372+ let fee1 = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2373+ assert_eq ! ( client. get_rewards_accrued( ) , fee1) ;
2374+ let fee2 = client. compute_route_fee ( & s, & d, & 2_000_000_i128 ) ;
2375+ assert_eq ! ( client. get_rewards_accrued( ) , fee1 + fee2) ;
2376+ }
2377+
2378+ #[ test]
2379+ fn test_rewards_accrued_unaffected_by_zero_fee_route ( ) {
2380+ let env = Env :: default ( ) ;
2381+ let ( client, _admin) = setup_initialized ( & env) ;
2382+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2383+ client. register_pair ( & s, & d) ;
2384+ // No fee_bps set — every route is free.
2385+ let fee = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2386+ assert_eq ! ( fee, 0 ) ;
2387+ assert_eq ! ( client. get_rewards_accrued( ) , 0 ) ;
2388+ assert_eq ! (
2389+ event_payloads( & env, symbol_short!( "rwd_accr" ) ) . len( ) ,
2390+ 0 ,
2391+ "a zero-fee route must not emit rwd_accr"
2392+ ) ;
2393+ }
2394+
2395+ #[ test]
2396+ fn test_rewards_accrued_event_carries_fee_and_running_total ( ) {
2397+ let env = Env :: default ( ) ;
2398+ let ( client, _admin) = setup_initialized ( & env) ;
2399+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2400+ client. register_pair ( & s, & d) ;
2401+ client. set_pair_fee_bps ( & s, & d, & 50u32 ) ;
2402+ let fee = client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2403+ let payloads = event_payloads ( & env, symbol_short ! ( "rwd_accr" ) ) ;
2404+ assert_eq ! ( payloads. len( ) , 1 , "one rwd_accr event for one fee-bearing route" ) ;
2405+ let ( emitted_fee, emitted_total) : ( i128 , i128 ) =
2406+ soroban_sdk:: TryFromVal :: try_from_val ( & env, & payloads[ 0 ] )
2407+ . expect ( "rwd_accr payload decodes to (fee, running_total)" ) ;
2408+ assert_eq ! ( emitted_fee, fee) ;
2409+ assert_eq ! ( emitted_total, client. get_rewards_accrued( ) ) ;
2410+ }
2411+
2412+ /// Over-limit boundary: accrual must saturate at `i128::MAX` instead of
2413+ /// panicking on overflow.
2414+ #[ test]
2415+ fn test_rewards_accrued_saturates_instead_of_overflowing ( ) {
2416+ let env = Env :: default ( ) ;
2417+ let ( client, _admin, contract_id) = setup_initialized_with_id ( & 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+ env. as_contract ( & contract_id, || {
2422+ env. storage ( )
2423+ . persistent ( )
2424+ . set ( & DataKey :: RewardsAccrued , & ( i128:: MAX - 10 ) ) ;
2425+ } ) ;
2426+ client. compute_route_fee ( & s, & d, & 1_000_000_i128 ) ;
2427+ assert_eq ! ( client. get_rewards_accrued( ) , i128 :: MAX ) ;
2428+ }
2429+
23222430 #[ test]
23232431 fn test_compute_route_fee_is_zero_when_fee_unset ( ) {
23242432 let env = Env :: default ( ) ;
0 commit comments