From 7e3e506b3380b8a076d231acf1f4f1c7b5ed6fca Mon Sep 17 00:00:00 2001 From: James AkpaMgbo Date: Sat, 29 Aug 2026 09:51:32 +0100 Subject: [PATCH] feat: add admin pool query, boost count, total locked, and vesting event cumulative total --- soroban/contracts/factory/src/lib.rs | 33 ++++++++++ soroban/contracts/factory/src/test.rs | 19 ++++++ soroban/contracts/factory/src/types.rs | 4 ++ soroban/contracts/farming-pool/src/lib.rs | 66 ++++++++++++++++++++ soroban/contracts/farming-pool/src/test.rs | 43 +++++++++++++ soroban/contracts/farming-pool/src/types.rs | 4 ++ soroban/contracts/vesting-wallet/src/lib.rs | 2 +- soroban/contracts/vesting-wallet/src/test.rs | 13 ++++ 8 files changed, 183 insertions(+), 1 deletion(-) diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 899fc9e..dc14a80 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -71,6 +71,14 @@ fn bump_asset_pools(env: &Env, asset: &Address) { ); } +fn bump_admin_pools(env: &Env, admin: &Address) { + env.storage().persistent().extend_ttl( + &DataKey::PoolsByAdmin(admin.clone()), + TTL_THRESHOLD, + TTL_EXTEND_TO, + ); +} + /// Reject any call that lands on a factory whose state was never seeded. /// /// `initialize` is the only writer of `DataKey::Admin`, so its presence is the @@ -463,6 +471,21 @@ impl Factory { Self::get_pools_by_asset_range(env, asset, start_id, MAX_POOL_SCAN_PER_CALL, limit) } + /// Return the list of pool IDs created by `admin`. + pub fn get_pools_by_admin(env: Env, admin: Address) -> Result, FactoryError> { + require_initialized(&env)?; + bump_instance(&env); + let admin_key = DataKey::PoolsByAdmin(admin.clone()); + if env.storage().persistent().has(&admin_key) { + bump_admin_pools(&env, &admin); + } + Ok(env + .storage() + .persistent() + .get(&admin_key) + .unwrap_or_else(|| vec![&env])) + } + /// Refresh TTLs for a range of pool records to prevent archival. /// /// This permissionless function allows keepers or any caller to proactively @@ -863,6 +886,16 @@ impl Factory { asset_pool_ids.push_back(pool_id); env.storage().persistent().set(&asset_key, &asset_pool_ids); bump_asset_pools(&env, &asset); + + let admin_key = DataKey::PoolsByAdmin(admin.clone()); + let mut admin_pool_ids: Vec = env + .storage() + .persistent() + .get(&admin_key) + .unwrap_or_else(|| vec![&env]); + admin_pool_ids.push_back(pool_id); + env.storage().persistent().set(&admin_key, &admin_pool_ids); + bump_admin_pools(&env, &admin); env.storage() .instance() .set(&DataKey::PoolCount, &next_count); diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index 4f27d0f..4516abe 100644 --- a/soroban/contracts/factory/src/test.rs +++ b/soroban/contracts/factory/src/test.rs @@ -1459,3 +1459,22 @@ fn test_admin_transfer_count_increments_on_transfer() { assert!(result.is_err()); assert_eq!(t.client.admin_transfer_count(), 2); } + +#[test] +fn test_get_pools_by_admin_returns_created_pools() { + let t = setup(); + assert_eq!(t.client.get_pools_by_admin(&t.admin), vec![&t.env]); + + let asset1 = Address::generate(&t.env); + let id1 = t + .client + .create_pool(&asset1, &1_728_000u128, &2u32, &10u64, &0i128); + + let asset2 = Address::generate(&t.env); + let id2 = t + .client + .create_pool(&asset2, &3_456_000u128, &2u32, &20u64, &0i128); + + let pools = t.client.get_pools_by_admin(&t.admin); + assert_eq!(pools, vec![&t.env, id1, id2]); +} diff --git a/soroban/contracts/factory/src/types.rs b/soroban/contracts/factory/src/types.rs index 9ad9824..dd76af4 100644 --- a/soroban/contracts/factory/src/types.rs +++ b/soroban/contracts/factory/src/types.rs @@ -17,6 +17,10 @@ pub enum DataKey { AdminTransferCount, /// Running total of successful `upgrade_pool` calls, for version tracking (#258). UpgradeCount, + /// List of pool IDs created for a specific asset. + AssetPools(Address), + /// List of pool IDs created by a specific admin (#229). + PoolsByAdmin(Address), } /// On-chain record for a registered farming pool. diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 9f78ebb..727a655 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -253,6 +253,39 @@ fn subtract_total_staked(env: &Env, amount: i128) { ); } +fn add_total_locked(env: &Env, amount: i128) { + let total = env + .storage() + .instance() + .get::(&DataKey::TotalLocked) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::TotalLocked, + &total.checked_add(amount).expect("total locked overflow"), + ); +} + +fn subtract_total_locked(env: &Env, amount: i128) { + let total = env + .storage() + .instance() + .get::(&DataKey::TotalLocked) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::TotalLocked, + &total.checked_sub(amount).expect("total locked underflow"), + ); +} + +fn read_boost_count(env: &Env) -> u32 { + env.storage().instance().get(&DataKey::BoostCount).unwrap_or(0) +} + +fn increment_boost_count(env: &Env) { + let count = read_boost_count(env); + env.storage().instance().set(&DataKey::BoostCount, &(count + 1)); +} + fn is_user_staked(env: &Env, user: &Address) -> bool { get_position(env, user).is_some() || get_user_stake(env, user).is_some() } @@ -551,6 +584,7 @@ impl FarmingPool { .instance() .set(&DataKey::MinStakeAmount, &min_stake); env.storage().instance().set(&DataKey::TotalStaked, &0i128); + env.storage().instance().set(&DataKey::TotalLocked, &0i128); env.storage() .instance() .set(&DataKey::TotalCredits, &0i128); @@ -738,6 +772,7 @@ impl FarmingPool { } increment_lock_count(&env); add_total_staked(&env, amount); + add_total_locked(&env, amount); let stake_token = get_stake_token(&env)?; token::TokenClient::new(&env, &stake_token).transfer( @@ -794,6 +829,7 @@ impl FarmingPool { decrement_staked_user_count(&env); } subtract_total_staked(&env, amount); + subtract_total_locked(&env, amount); let stake_token = get_stake_token(&env)?; token::TokenClient::new(&env, &stake_token).transfer( @@ -991,6 +1027,7 @@ impl FarmingPool { token.transfer(&env.current_contract_address(), &user, &position.amount); total_returned += position.amount; subtract_total_staked(&env, position.amount); + subtract_total_locked(&env, position.amount); position_credits = position.total_credits; remove_position(&env, &user); } @@ -1357,6 +1394,9 @@ impl FarmingPool { } let key = DataKey::UserBoost(user.clone()); + if !env.storage().persistent().has(&key) { + increment_boost_count(&env); + } env.storage().persistent().set(&key, &allocation_pct); bump_user(&env, &key); @@ -1664,6 +1704,32 @@ impl FarmingPool { pub fn get_unstake_count(env: Env) -> Result { Self::unstake_count(env) } + + /// Return the total number of boost configurations configured across users (#230). + pub fn boost_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_boost_count(&env)) + } + + pub fn get_boost_count(env: Env) -> Result { + Self::boost_count(env) + } + + /// Return the total tokens locked across all position locking positions (#232). + pub fn total_locked(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(env + .storage() + .instance() + .get(&DataKey::TotalLocked) + .unwrap_or(0)) + } + + pub fn get_total_locked(env: Env) -> Result { + Self::total_locked(env) + } } mod test; diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index fe3e584..71fd103 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -2792,4 +2792,47 @@ fn test_unstake_count_increments_on_every_unstake_operation() { assert_eq!(t.client.unstake_count(), 2); } +#[test] +fn test_boost_count_increments_on_new_boost_config() { + let t = setup(1, 10); + assert_eq!(t.client.boost_count(), 0); + assert_eq!(t.client.get_boost_count(), 0); + + let user2 = Address::generate(&t.env); + t.token_sac.mint(&user2, &10_000); + + t.client.set_boost(&t.user, &50u32); + assert_eq!(t.client.boost_count(), 1); + assert_eq!(t.client.get_boost_count(), 1); + + // Updating existing boost does not increment boost_count + t.client.set_boost(&t.user, &75u32); + assert_eq!(t.client.boost_count(), 1); + + // Setting boost for user2 increments boost_count to 2 + t.client.set_boost(&user2, &25u32); + assert_eq!(t.client.boost_count(), 2); +} + +#[test] +fn test_total_locked_tracks_locked_positions() { + let t = setup(1, 10); + assert_eq!(t.client.total_locked(), 0); + assert_eq!(t.client.get_total_locked(), 0); + + let user2 = Address::generate(&t.env); + t.token_sac.mint(&user2, &10_000); + + t.client.lock_assets(&t.user, &1_000); + assert_eq!(t.client.total_locked(), 1_000); + assert_eq!(t.client.get_total_locked(), 1_000); + + t.client.lock_assets(&user2, &2_000); + assert_eq!(t.client.total_locked(), 3_000); + + advance_ledgers(&t.env, 10); + t.client.unlock_assets(&t.user, &1_000); + assert_eq!(t.client.total_locked(), 2_000); +} + diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index f97da6b..828beb0 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -115,6 +115,10 @@ pub enum DataKey { LockCount, /// Running count of total unstake operations performed. UnstakeCount, + /// Running count of user boost configurations set. + BoostCount, + /// Total tokens currently locked in positions. + TotalLocked, } /// 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 143255d..6db978a 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -258,7 +258,7 @@ impl VestingWallet { #[allow(deprecated)] env.events().publish( (symbol_short!("vest"), symbol_short!("released")), - (beneficiary, releasable), + (beneficiary, releasable, released + releasable), ); Ok(releasable) diff --git a/soroban/contracts/vesting-wallet/src/test.rs b/soroban/contracts/vesting-wallet/src/test.rs index b4a27e0..7b06f7e 100644 --- a/soroban/contracts/vesting-wallet/src/test.rs +++ b/soroban/contracts/vesting-wallet/src/test.rs @@ -369,6 +369,19 @@ fn test_release_emits_event() { ); } +#[test] +fn test_release_emits_event_with_cumulative_total() { + let t = setup(0, 100, 1_000); + advance_ledgers(&t.env, 50); + t.client.release(); // 500 released + + advance_ledgers(&t.env, 25); + t.client.release(); // 250 releasable, cumulative total 750 + + let events = t.env.events().all(); + assert!(!events.events().is_empty()); +} + // ── revoke tests ────────────────────────────────────────────────────────────── #[test]