diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b242572..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "githubPullRequests.ignoredPullRequestBranches": [ - "main" - ] -} \ No newline at end of file diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index f7f3750..2f5e7a2 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -6,7 +6,7 @@ use soroban_sdk::{ contract, contractimpl, symbol_short, vec, Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec, }; -use types::{DataKey, ListPoolsResponse, PoolRecord, PoolSort}; +use types::{DataKey, ListPoolsResponse, PoolRecord, PoolSort, PoolStatus}; pub use types::FactoryError; pub use types::PoolParams; @@ -471,14 +471,16 @@ impl Factory { if env.storage().instance().has(&DataKey::Admin) { return Err(FactoryError::AlreadyInitialized); } - if admin - == Address::from_string(&soroban_sdk::String::from_str( - &env, - "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", - )) - { + + // Issue #374: Validate admin address before storing + let zero_admin = Address::from_string(&soroban_sdk::String::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + )); + if admin == zero_admin { return Err(FactoryError::InvalidAdmin); } + if pool_wasm_hash == BytesN::from_array(&env, &[0u8; 32]) { return Err(FactoryError::InvalidWasmHash); } @@ -1230,6 +1232,54 @@ impl Factory { Ok(end) } + /// Query the health status of an individual pool (Issue #375). + /// + /// Returns a `PoolStatus` struct containing the pool's address, pause status, + /// and total staked amount. This provides a centralized way to check pool + /// health without querying each pool contract directly. + /// + /// Returns `NotInitialized` if the factory has not been initialized, + /// `PoolNotFound` for an unknown `pool_id`, or `PoolQueryFailed` if the + /// deployed pool does not respond to the status queries. + pub fn pool_status(env: Env, pool_id: u32) -> Result { + require_initialized(&env)?; + bump_instance(&env); + let record = env + .storage() + .persistent() + .get::(&DataKey::Pool(pool_id)) + .ok_or(FactoryError::PoolNotFound)?; + bump_pool(&env, pool_id); + + let no_args: Vec = vec![&env]; + + // Query is_paused status + let is_paused = match env.try_invoke_contract::( + &record.address, + &Symbol::new(&env, "is_paused"), + no_args.clone(), + ) { + Ok(Ok(v)) => v, + _ => return Err(FactoryError::PoolQueryFailed), + }; + + // Query total_staked + let total_staked = match env.try_invoke_contract::( + &record.address, + &Symbol::new(&env, "total_staked"), + no_args, + ) { + Ok(Ok(v)) => v, + _ => return Err(FactoryError::PoolQueryFailed), + }; + + Ok(PoolStatus { + address: record.address, + is_paused, + total_staked, + }) + } + /// Update the WASM hash used for future `create_pool` deployments. Admin-only. /// /// Allows the admin to point future pool deployments at a corrected or upgraded @@ -1366,6 +1416,80 @@ impl Factory { let admin = load_admin(&env)?; validate_factory_admin(&env, &admin)?; admin.require_auth(); + bump_instance(&env); + + let paused: bool = env + .storage() + .instance() + .get(&DataKey::PoolCreationPaused) + .unwrap_or(false); + if paused { + return Err(FactoryError::PoolCreationPaused); + } + + if min_stake_amount <= 0 { + return Err(FactoryError::InvalidMinStakeAmount); + } + + validate_asset(&env, &asset)?; + + if global_multiplier < 1 { + return Err(FactoryError::InvalidGlobalMultiplier); + } + let credit_rate = daily_rate_to_credit_rate(daily_rate)?; + let min_lock_period: u32 = min_lock_period + .try_into() + .map_err(|_| FactoryError::MinLockPeriodOutOfRange)?; + if min_lock_period < MIN_LOCK_PERIOD { + return Err(FactoryError::MinLockPeriodTooShort); + } + let effective_min_stake = min_stake_amount; + // The requested amount must lie within [MIN_STAKE_AMOUNT, + // MAX_STAKE_AMOUNT] so the pool is usable. + if effective_min_stake < MIN_STAKE_AMOUNT || effective_min_stake > MAX_STAKE_AMOUNT { + return Err(FactoryError::InvalidMinStakeAmount); + } + + let pool_id: u32 = env.storage().instance().get(&DataKey::PoolCount).unwrap(); + let next_count = pool_id + .checked_add(1) + .ok_or(FactoryError::PoolCountOverflow)?; + env.storage().instance().set(&DataKey::PoolCount, &next_count); + let wasm_hash = load_wasm_hash(&env)?; + let salt = pool_salt(&env, pool_id); + + // Deploy a fresh farming-pool instance. The resulting address is + // deterministic: keccak256(factory_address || salt). + let pool_address = env + .deployer() + .with_current_contract(salt) + .deploy_v2(wasm_hash.clone(), ()); + + // Call the freshly deployed pool's `initialize` directly via + // `invoke_contract` rather than depending on the `farming-pool` + // crate's generated Client: pulling that crate in as a normal + // dependency causes its own `#[contractimpl]`-exported WASM symbols + // (e.g. `admin`, `transfer_admin`) to collide with the factory's own + // exports of the same names when both are linked into one cdylib. + let init_args: Vec = vec![ + &env, + admin.into_val(&env), + asset.into_val(&env), + global_multiplier.into_val(&env), + credit_rate.into_val(&env), + min_lock_period.into_val(&env), + effective_min_stake.into_val(&env), + ]; + + // Issue #373: Initialize the pool before emitting pool_crtd event to ensure + // the event is only emitted if pool creation succeeds. If initialization + // fails, the entire transaction reverts and no event is published. + let _: () = env.invoke_contract(&pool_address, &Symbol::new(&env, "initialize"), init_args); + + let record = PoolRecord { + address: pool_address.clone(), + asset: asset.clone(), + credit_rate, create_pool_inner( env, admin, @@ -1377,6 +1501,36 @@ impl Factory { ) } + 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); + + // Issue #373: Emit enriched event AFTER successful pool initialization. + // This ensures indexers only record pools that were fully created. + // `effective_min_stake` is the resolved dust-thresholded value actually + // passed to the pool's `initialize` (#330), not the raw caller input. + #[allow(deprecated)] + env.events().publish( + (symbol_short!("factory"), symbol_short!("pool_crtd")), + ( + pool_id, + pool_address, + admin, + asset, + credit_rate, + global_multiplier, + min_lock_period, + daily_rate, + wasm_hash, + effective_min_stake, + ), + ); /// Create multiple pools atomically using the same validation and setup as /// `create_pool`. If any item is invalid, the transaction rolls back every /// pool created earlier in this batch. diff --git a/soroban/contracts/factory/src/types.rs b/soroban/contracts/factory/src/types.rs index 367a7dd..93868ab 100644 --- a/soroban/contracts/factory/src/types.rs +++ b/soroban/contracts/factory/src/types.rs @@ -100,6 +100,18 @@ pub struct ListPoolsResponse { pub has_more: bool, } +/// Pool health status returned by `pool_status` (Issue #375). +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct PoolStatus { + /// Address of the pool contract. + pub address: Address, + /// Whether staking is currently paused in the pool. + pub is_paused: bool, + /// Current total amount staked in the pool. + pub total_staked: i128, +} + /// Typed errors returned by the factory contract. /// /// Using `#[contracterror]` exposes these as a stable on-chain error code so diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 4e6b71e..c02dc80 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -1233,6 +1233,10 @@ impl FarmingPool { let mut position = if let Some(mut existing) = get_position(&env, &user) { checkpoint_position(&env, &user, &mut existing); existing.amount += amount; + + // Issue #376: Update lock_ledger to current ledger for additional deposits + // This ensures the lock period is based on the most recent deposit, not the original + existing.lock_ledger = current; let fresh_unlock = current.saturating_add(read_min_lock_period(&env)); existing.unlock_ledger = existing.unlock_ledger.max(fresh_unlock); existing