From 41787170d666e02ea3285b4cb7bafcb99cd5c7c0 Mon Sep 17 00:00:00 2001 From: Alex Benjamin Date: Sat, 29 Aug 2026 09:35:55 +0100 Subject: [PATCH] fixed all issues --- soroban/contracts/factory/src/lib.rs | 114 ++++++++++++++ soroban/contracts/factory/src/types.rs | 6 +- soroban/contracts/farming-pool/src/lib.rs | 164 ++++++++++++++++++++ soroban/contracts/farming-pool/src/types.rs | 13 +- soroban/contracts/vesting-wallet/src/lib.rs | 7 + 5 files changed, 302 insertions(+), 2 deletions(-) diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 899fc9e..5f1cd31 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_wasm_pools(env: &Env, wasm_hash: &BytesN<32>) { + env.storage().persistent().extend_ttl( + &DataKey::PoolsByWasmHash(wasm_hash.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,76 @@ impl Factory { Self::get_pools_by_asset_range(env, asset, start_id, MAX_POOL_SCAN_PER_CALL, limit) } + /// Return a page of pool records deployed or upgraded to the given `wasm_hash`. + /// + /// Uses the `PoolsByWasmHash` secondary index populated by `create_pool` and + /// updated by `upgrade_pool`, so this is a direct O(m) lookup in the indexed + /// list — callers do not need to scan the full registry. `start_idx` is a + /// 0-based offset *within the matching ID list* (not a pool ID window), and + /// `limit` is capped at 20 matching records per call. + /// + /// Pools whose WASM was changed via `upgrade_pool` are removed from their + /// old hash's list and added to the new hash's list, so each pool ID appears + /// in exactly one index entry at a time. + /// + /// Returns `NotInitialized` if the factory has not been initialized. An + /// empty `records` list with `has_more = false` indicates that no pools + /// match (or the start_idx is past the end of the list). + pub fn get_pools_by_wasm_hash( + env: Env, + wasm_hash: BytesN<32>, + start_idx: u32, + limit: u32, + ) -> Result { + require_initialized(&env)?; + bump_instance(&env); + let count: u32 = env + .storage() + .instance() + .get(&DataKey::PoolCount) + .unwrap_or(0); + let capped_limit = if limit == 0 { 20 } else { limit.min(20) }; + + let wasm_key = DataKey::PoolsByWasmHash(wasm_hash.clone()); + let matching_ids: Vec = env + .storage() + .persistent() + .get(&wasm_key) + .unwrap_or_else(|| vec![&env]); + bump_wasm_pools(&env, &wasm_hash); + + let mut records: Vec<(u32, PoolRecord)> = vec![&env]; + let total_matches = matching_ids.len(); + let mut next_start_idx = total_matches; + let mut collected = 0u32; + let mut i = start_idx; + while i < total_matches && collected < capped_limit { + let pool_id = matching_ids.get(i).unwrap(); + let pool_key = DataKey::Pool(pool_id); + if let Some(record) = env + .storage() + .persistent() + .get::(&pool_key) + { + bump_pool(&env, pool_id); + records.push_back((pool_id, record)); + collected += 1; + } + i += 1; + } + if i < total_matches { + next_start_idx = i; + } + + let has_more = next_start_idx < total_matches; + Ok(ListPoolsResponse { + records, + next_start_id: next_start_idx, + total: count, + has_more, + }) + } + /// Refresh TTLs for a range of pool records to prevent archival. /// /// This permissionless function allows keepers or any caller to proactively @@ -638,6 +716,31 @@ impl Factory { record.wasm_hash = new_wasm_hash.clone(); env.storage().persistent().set(&key, &record); + let old_wasm_key = DataKey::PoolsByWasmHash(old_hash.clone()); + if let Some(mut old_pool_ids) = env + .storage() + .persistent() + .get::>(&old_wasm_key) + { + let mut new_old_ids: Vec = vec![&env]; + for id in old_pool_ids.iter() { + if id != pool_id { + new_old_ids.push_back(id); + } + } + env.storage().persistent().set(&old_wasm_key, &new_old_ids); + } + + let new_wasm_key = DataKey::PoolsByWasmHash(new_wasm_hash.clone()); + let mut new_pool_ids: Vec = env + .storage() + .persistent() + .get(&new_wasm_key) + .unwrap_or_else(|| vec![&env]); + new_pool_ids.push_back(pool_id); + env.storage().persistent().set(&new_wasm_key, &new_pool_ids); + bump_wasm_pools(&env, &new_wasm_hash); + env.storage() .instance() .set(&DataKey::UpgradeCount, &read_upgrade_count(&env).saturating_add(1)); @@ -863,6 +966,17 @@ impl Factory { asset_pool_ids.push_back(pool_id); env.storage().persistent().set(&asset_key, &asset_pool_ids); bump_asset_pools(&env, &asset); + + let wasm_key = DataKey::PoolsByWasmHash(wasm_hash.clone()); + let mut wasm_pool_ids: Vec = env + .storage() + .persistent() + .get(&wasm_key) + .unwrap_or_else(|| vec![&env]); + wasm_pool_ids.push_back(pool_id); + env.storage().persistent().set(&wasm_key, &wasm_pool_ids); + bump_wasm_pools(&env, &wasm_hash); + env.storage() .instance() .set(&DataKey::PoolCount, &next_count); diff --git a/soroban/contracts/factory/src/types.rs b/soroban/contracts/factory/src/types.rs index 9ad9824..857a8a0 100644 --- a/soroban/contracts/factory/src/types.rs +++ b/soroban/contracts/factory/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracterror, contracttype, Address, Vec}; +use soroban_sdk::{contracterror, contracttype, Address, BytesN, Vec}; /// Storage keys used by the factory contract. #[contracttype] @@ -17,6 +17,10 @@ pub enum DataKey { AdminTransferCount, /// Running total of successful `upgrade_pool` calls, for version tracking (#258). UpgradeCount, + /// Secondary index: list of pool IDs whose staking asset matches. + AssetPools(Address), + /// Secondary index: list of pool IDs deployed or upgraded to a given WASM hash. + PoolsByWasmHash(BytesN<32>), } /// 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..22dd658 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -339,6 +339,102 @@ fn add_total_distributed_credits(env: &Env, amount: i128) { ); } +fn add_total_credits(env: &Env, amount: i128) { + let total = env + .storage() + .instance() + .get::(&DataKey::TotalCredits) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::TotalCredits, + &total.checked_add(amount).expect("total credits overflow"), + ); +} + +fn read_total_deposits(env: &Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalDeposits) + .unwrap_or(0) +} + +fn add_total_deposits(env: &Env, amount: i128) { + let total = env + .storage() + .instance() + .get::(&DataKey::TotalDeposits) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::TotalDeposits, + &total.checked_add(amount).expect("total deposits overflow"), + ); +} + +fn read_total_withdrawals(env: &Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalWithdrawals) + .unwrap_or(0) +} + +fn add_total_withdrawals(env: &Env, amount: i128) { + let total = env + .storage() + .instance() + .get::(&DataKey::TotalWithdrawals) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::TotalWithdrawals, + &total.checked_add(amount).expect("total withdrawals overflow"), + ); +} + +fn read_total_boost_allocations(env: &Env) -> u64 { + env.storage() + .instance() + .get(&DataKey::TotalBoostAlloc) + .unwrap_or(0) +} + +fn add_total_boost_allocation(env: &Env, delta: i64) { + let total = read_total_boost_allocations(env); + if delta >= 0 { + env.storage().instance().set( + &DataKey::TotalBoostAlloc, + &total.checked_add(delta as u64).expect("total boost alloc overflow"), + ); + } else { + let sub = (-delta) as u64; + env.storage().instance().set( + &DataKey::TotalBoostAlloc, + &total.checked_sub(sub).expect("total boost alloc underflow"), + ); + } +} + +fn read_boost_user_count(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::BoostUserCount) + .unwrap_or(0) +} + +fn increment_boost_user_count(env: &Env) { + let count = read_boost_user_count(env); + env.storage() + .instance() + .set(&DataKey::BoostUserCount, &(count + 1)); +} + +fn decrement_boost_user_count(env: &Env) { + let count = read_boost_user_count(env); + if count > 0 { + env.storage() + .instance() + .set(&DataKey::BoostUserCount, &(count - 1)); + } +} + fn get_position(env: &Env, user: &Address) -> Option { let key = DataKey::UserPosition(user.clone()); let value: Option = env.storage().persistent().get(&key); @@ -554,6 +650,12 @@ impl FarmingPool { env.storage() .instance() .set(&DataKey::TotalCredits, &0i128); + env.storage() + .instance() + .set(&DataKey::TotalDeposits, &0i128); + env.storage() + .instance() + .set(&DataKey::TotalWithdrawals, &0i128); env.storage() .instance() .set(&DataKey::SchemaVersion, &SCHEMA_VERSION); @@ -738,6 +840,7 @@ impl FarmingPool { } increment_lock_count(&env); add_total_staked(&env, amount); + add_total_deposits(&env, amount); let stake_token = get_stake_token(&env)?; token::TokenClient::new(&env, &stake_token).transfer( @@ -794,6 +897,7 @@ impl FarmingPool { decrement_staked_user_count(&env); } subtract_total_staked(&env, amount); + add_total_withdrawals(&env, amount); let stake_token = get_stake_token(&env)?; token::TokenClient::new(&env, &stake_token).transfer( @@ -1011,6 +1115,8 @@ impl FarmingPool { return Err(PoolError::NoActiveStake); } + add_total_withdrawals(&env, total_returned); + // Bank the position and stake credits as separate totals so each staking // system's accrual history survives even when a user held both (#145). if position_credits > 0 || stake_credits > 0 { @@ -1285,6 +1391,7 @@ impl FarmingPool { increment_staked_user_count(&env); } add_total_staked(&env, amount); + add_total_deposits(&env, amount); // Pull tokens from caller into the contract. let stake_token = get_stake_token(&env)?; @@ -1338,6 +1445,7 @@ impl FarmingPool { } increment_unstake_count(&env); subtract_total_staked(&env, stake.amount); + add_total_withdrawals(&env, stake.amount); Ok(total_credits) } @@ -1356,6 +1464,17 @@ impl FarmingPool { set_user_stake(&env, &user, &stake); } + let old_alloc: u32 = get_user_boost(&env, &user).unwrap_or(0); + if old_alloc == 0 { + increment_boost_user_count(&env); + add_total_boost_allocation(&env, allocation_pct as i64); + } else { + let delta = allocation_pct as i64 - old_alloc as i64; + if delta != 0 { + add_total_boost_allocation(&env, delta); + } + } + let key = DataKey::UserBoost(user.clone()); env.storage().persistent().set(&key, &allocation_pct); bump_user(&env, &key); @@ -1628,6 +1747,30 @@ impl FarmingPool { .unwrap_or(0)) } + /// Return the running total of all tokens deposited into the pool. + /// + /// Incremented by `stake` and `lock_assets` with the amount transferred in. + /// Tracks cumulative inflow for protocol flow analytics; compare with + /// `total_withdrawals` to derive net flow and with `total_staked` to + /// reconcile current TVL against historical turnover. + pub fn total_deposits(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_deposits(&env)) + } + + /// Return the running total of all tokens withdrawn from the pool. + /// + /// Incremented by `unstake`, `unlock_assets`, and `emergency_withdraw` + /// with the amount transferred out. Tracks cumulative outflow for + /// protocol flow analytics; compare with `total_deposits` to derive + /// net flow. + pub fn total_withdrawals(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_withdrawals(&env)) + } + /// Return the count of currently staked unique users in the pool. pub fn staked_user_count(env: Env) -> Result { require_initialized(&env)?; @@ -1664,6 +1807,27 @@ impl FarmingPool { pub fn get_unstake_count(env: Env) -> Result { Self::unstake_count(env) } + + /// Return the sum of all active user boost allocation percentages. + /// + /// Each user's `allocation_pct` (1-100) is captured in this running total + /// so that the protocol-wide average boost allocation can be derived + /// off-chain as `total_boost_allocations / boost_user_count`. + pub fn total_boost_allocations(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_boost_allocations(&env)) + } + + /// Return the count of users currently with a non-zero boost allocation set. + /// + /// Pair with `total_boost_allocations` to compute the average boost + /// allocation across boosted users. + pub fn boost_user_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_boost_user_count(&env)) + } } mod test; diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index f97da6b..7ea8700 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -115,6 +115,18 @@ pub enum DataKey { LockCount, /// Running count of total unstake operations performed. UnstakeCount, + /// Running total of all tokens deposited into the pool (stake + lock). + TotalDeposits, + /// Running total of all tokens withdrawn from the pool (unstake + unlock + emergency). + TotalWithdrawals, + /// Total credits accrued in the position (lock) system. + TotalCredits, + /// Running count of successful emergency_withdraw calls. + EmergencyWithdrawalCount, + /// Sum of all active user boost allocation percentages (for average computation). + TotalBoostAlloc, + /// Count of users currently with a non-zero boost allocation set. + BoostUserCount, } /// Paginated response for `get_whitelisted_users`. @@ -125,5 +137,4 @@ pub struct ListWhitelistedResponse { pub users: Vec
, /// Total number of whitelisted addresses. pub total: u32, - TotalCredits, } diff --git a/soroban/contracts/vesting-wallet/src/lib.rs b/soroban/contracts/vesting-wallet/src/lib.rs index 143255d..835a034 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -334,6 +334,13 @@ impl VestingWallet { Ok(compute_vested(&env)? - get_released(&env)) } + /// Return whether the vesting schedule is revocable by admin. + pub fn revocable(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(is_revocable(&env)) + } + /// Return the full vesting schedule parameters in a single call. /// /// Frontends need `beneficiary`, `token`, `total_amount`, `start_ledger`,