Skip to content

Commit bfedbf0

Browse files
committed
feat(rewards): accrue protocol fee rewards with view, event, and boundary tests
Adds a minimal rewards-accrual model: every fee-bearing compute_route_fee call adds its fee to a protocol-wide RewardsAccrued counter (saturating, owed to FeeRecipient once distributed), exposed via get_rewards_accrued and a rwd_accr event carrying (fee, running_total). Zero-fee routes are not a state change and neither write nor emit. Closes #419 Closes #420 Closes #421
1 parent 3eaff98 commit bfedbf0

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)