diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 88e78d4..de23edd 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -310,6 +310,14 @@ impl Factory { .instance() .get(&DataKey::PoolCount) .unwrap_or(0); + if start_id >= count { + return Ok(ListPoolsResponse { + records: vec![&env], + next_start_id: count, + total: count, + has_more: false, + }); + } let capped_limit = if limit == 0 { 20 } else { limit.min(20) }; let end = start_id.saturating_add(capped_limit).min(count); let mut records: Vec<(u32, PoolRecord)> = vec![&env]; @@ -352,6 +360,14 @@ impl Factory { .instance() .get(&DataKey::PoolCount) .unwrap_or(0); + if start_id >= count { + return Ok(ListPoolsResponse { + records: vec![&env], + next_start_id: count, + total: count, + has_more: false, + }); + } let capped_limit = if limit == 0 { 20 } else { limit.min(20) }; let end = start_id.saturating_add(capped_limit).min(count); let mut records: Vec<(u32, PoolRecord)> = vec![&env]; @@ -410,6 +426,14 @@ impl Factory { .instance() .get(&DataKey::PoolCount) .unwrap_or(0); + if start_id >= count { + return Ok(ListPoolsResponse { + records: vec![&env], + next_start_id: count, + total: count, + has_more: false, + }); + } let capped_limit = if limit == 0 { 20 } else { limit.min(20) }; let effective_scan = if scan_limit == 0 { MAX_POOL_SCAN_PER_CALL diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index ce2306b..f7d9c62 100644 --- a/soroban/contracts/factory/src/test.rs +++ b/soroban/contracts/factory/src/test.rs @@ -400,6 +400,17 @@ fn test_list_pools_returns_empty_when_start_is_beyond_count() { assert!(!page.has_more); } +#[test] +fn test_list_pools_returns_empty_when_start_equals_count() { + let t = setup_with_pool_records(3); + let page = t.client.list_pools(&3u32, &5u32); + + assert_eq!(page.records.len(), 0); + assert_eq!(page.next_start_id, 3); + assert_eq!(page.total, 3); + assert!(!page.has_more); +} + #[test] fn test_list_pools_caps_limit_at_twenty() { let t = setup_with_pool_records(25); diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 896503d..bcbbc47 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -391,6 +391,44 @@ fn add_total_credits(env: &Env, amount: i128) { ); } +fn read_total_banked_credits(env: &Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalBankedCredits) + .unwrap_or(0) +} + +fn add_total_banked_credits(env: &Env, amount: i128) { + let total = read_total_banked_credits(env); + env.storage().instance().set( + &DataKey::TotalBankedCredits, + &total.checked_add(amount).expect("total banked credits overflow"), + ); +} + +fn subtract_total_banked_credits(env: &Env, amount: i128) { + let total = read_total_banked_credits(env); + env.storage().instance().set( + &DataKey::TotalBankedCredits, + &total.checked_sub(amount).expect("total banked credits underflow"), + ); +} + +fn read_total_credits_earned(env: &Env, user: &Address) -> i128 { + let key = DataKey::TotalCreditsEarned(user.clone()); + env.storage().persistent().get(&key).unwrap_or(0) +} + +fn add_total_credits_earned(env: &Env, user: &Address, amount: i128) { + let key = DataKey::TotalCreditsEarned(user.clone()); + let total = env.storage().persistent().get::(&key).unwrap_or(0); + env.storage().persistent().set( + &key, + &total.checked_add(amount).expect("user lifetime credits overflow"), + ); + bump_user(env, &key); +} + fn read_total_deposits(env: &Env) -> i128 { env.storage() .instance() @@ -633,6 +671,10 @@ fn checkpoint(env: &Env, user: &Address, stake: &mut UserStake) { stake.credits_banked += accrued; add_total_credits(env, accrued); add_total_distributed_credits(env, accrued); + if accrued > 0 { + add_total_banked_credits(env, accrued); + add_total_credits_earned(env, user, accrued); + } stake.start_ledger = current; stake.credit_rate = read_credit_rate(env); stake.multiplier = read_global_multiplier(env); @@ -653,6 +695,10 @@ fn checkpoint_position(env: &Env, user: &Address, position: &mut Position) { position.total_credits += delta; add_total_credits(env, delta); add_total_distributed_credits(env, delta); + if delta > 0 { + add_total_banked_credits(env, delta); + add_total_credits_earned(env, user, delta); + } position.checkpoint_ledger = current; position.credit_rate = read_credit_rate(env); @@ -721,6 +767,9 @@ impl FarmingPool { env.storage().instance().set(&DataKey::TotalStaked, &0i128); env.storage().instance().set(&DataKey::TotalLocked, &0i128); env.storage().instance().set(&DataKey::TotalCredits, &0i128); + env.storage() + .instance() + .set(&DataKey::TotalBankedCredits, &0i128); env.storage() .instance() .set(&DataKey::TotalDeposits, &0i128); @@ -1514,6 +1563,9 @@ impl FarmingPool { let mut stake = get_user_stake(&env, &from).expect("no active stake"); checkpoint(&env, &from, &mut stake); let total_credits = stake.credits_banked; + if total_credits > 0 { + subtract_total_banked_credits(&env, total_credits); + } // Return staked tokens to caller. let stake_token = get_stake_token(&env)?; @@ -1853,6 +1905,21 @@ impl FarmingPool { .unwrap_or(0)) } + /// Return the total credits currently banked across all users. + pub fn total_banked_credits(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_banked_credits(&env)) + } + + /// Return the cumulative credits earned by `user` across their lifetime, + /// including amounts already withdrawn. + pub fn total_credits_earned(env: Env, user: Address) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_credits_earned(&env, &user)) + } + /// Return the running total of all tokens deposited into the pool. /// /// Incremented by `stake` and `lock_assets` with the amount transferred in. diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index 38221d7..3f6f2bd 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -262,6 +262,44 @@ fn test_total_distributed_credits_accumulates_across_users_and_systems() { assert_eq!(t.client.total_distributed_credits(), 10_500); } +#[test] +fn test_total_credits_earned_tracks_lifetime_credits_across_withdrawals() { + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + + advance_ledgers(&t.env, 10); + assert_eq!(t.client.get_credits(&t.user), 10_000); + assert_eq!(t.client.total_credits_earned(&t.user), 0); + + t.client.unstake(&t.user); + assert_eq!(t.client.total_credits_earned(&t.user), 10_000); + + advance_ledgers(&t.env, 5); + t.client.stake(&t.user, &500); + advance_ledgers(&t.env, 5); + t.client.unstake(&t.user); + assert_eq!(t.client.total_credits_earned(&t.user), 12_500); +} + +#[test] +fn test_total_banked_credits_tracks_current_bank_across_users() { + let t = setup(2, 1); + let other = Address::generate(&t.env); + t.token_sac.mint(&other, &1_000_000_000i128); + + t.client.stake(&t.user, &1_000); + advance_ledgers(&t.env, 10); + assert_eq!(t.client.total_banked_credits(), 0); + + t.client.unstake(&t.user); + assert_eq!(t.client.total_banked_credits(), 0); + + t.client.stake(&other, &2_000); + advance_ledgers(&t.env, 5); + t.client.unstake(&other); + assert_eq!(t.client.total_banked_credits(), 0); +} + #[test] fn test_pause_uninitialized_returns_not_initialized() { let (_env, client, _user) = setup_uninitialized(); diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index 31aa969..56bbeb6 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -131,6 +131,10 @@ pub enum DataKey { TotalBoostAlloc, /// Count of users with a non-zero boost allocation currently set. BoostUserCount, + /// Aggregate of credits currently banked across all users. + TotalBankedCredits, + /// Cumulative credits earned by a user across their entire lifetime. + TotalCreditsEarned(Address), } /// Paginated response for `get_whitelisted_users`. diff --git a/soroban/contracts/vesting-wallet/src/lib.rs b/soroban/contracts/vesting-wallet/src/lib.rs index cb390fa..69242f3 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -432,6 +432,13 @@ impl VestingWallet { Ok(()) } + /// Return the token address for the vesting schedule. + pub fn token(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(get_token(&env)) + } + /// Return the current admin address. pub fn admin(env: Env) -> Result { require_initialized(&env)?; diff --git a/soroban/contracts/vesting-wallet/src/test.rs b/soroban/contracts/vesting-wallet/src/test.rs index d621850..b0efe47 100644 --- a/soroban/contracts/vesting-wallet/src/test.rs +++ b/soroban/contracts/vesting-wallet/src/test.rs @@ -150,6 +150,12 @@ fn test_double_initialize_returns_error() { assert!(matches!(result, Err(Ok(VestingError::AlreadyInitialized)))); } +#[test] +fn test_token_getter_returns_initialized_token_address() { + let t = setup(0, 100, 1_000); + assert_eq!(t.client.token(), t.token_address); +} + #[test] #[should_panic(expected = "start must be in the future")] fn test_initialize_rejects_start_ledger_in_the_past() {