Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .vscode/settings.json

This file was deleted.

168 changes: 161 additions & 7 deletions soroban/contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<PoolStatus, FactoryError> {
require_initialized(&env)?;
bump_instance(&env);
let record = env
.storage()
.persistent()
.get::<DataKey, PoolRecord>(&DataKey::Pool(pool_id))
.ok_or(FactoryError::PoolNotFound)?;
bump_pool(&env, pool_id);

let no_args: Vec<Val> = vec![&env];

// Query is_paused status
let is_paused = match env.try_invoke_contract::<bool, soroban_sdk::Error>(
&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::<i128, soroban_sdk::Error>(
&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
Expand Down Expand Up @@ -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<Val> = 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,
Expand All @@ -1377,6 +1501,36 @@ impl Factory {
)
}

let wasm_key = DataKey::PoolsByWasmHash(wasm_hash.clone());
let mut wasm_pool_ids: Vec<u32> = 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.
Expand Down
12 changes: 12 additions & 0 deletions soroban/contracts/factory/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down