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
32 changes: 32 additions & 0 deletions soroban/contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,18 @@ fn validate_asset(env: &Env, asset: &Address) -> Result<(), FactoryError> {
asset,
&Symbol::new(env, "symbol"),
vec![env],
) {
Ok(Ok(_)) => {}
_ => return Err(FactoryError::InvalidAsset),
}
// Validation 49: Also require the SEP-41 `balance` entry point. A contract
// that answers `symbol` but not `balance` cannot support staking, because
// `stake`/`lock_assets` read the caller's token balance before transferring
// (#542).
match env.try_invoke_contract::<i128, soroban_sdk::Error>(
asset,
&Symbol::new(env, "balance"),
(env.current_contract_address(),).into_val(env),
) {
Ok(Ok(_)) => Ok(()),
_ => Err(FactoryError::InvalidAsset),
Expand Down Expand Up @@ -351,6 +363,14 @@ fn create_pool_inner(
return Err(FactoryError::InvalidAsset);
}

// Validation 48: Ensure the staking asset is not also the pool admin. A
// pool administered by its own staking token cannot authorize admin
// actions (pause, upgrade, multiplier changes), leaving the pool
// unmanageable (#541).
if asset == admin {
return Err(FactoryError::InvalidAsset);
}

// Validation 24: Ensure global_multiplier is strictly within bounds
// Must be at least 1 and at most MAX_GLOBAL_MULTIPLIER (1000)
if global_multiplier < 1 {
Expand Down Expand Up @@ -395,6 +415,13 @@ fn create_pool_inner(
if min_stake_amount > MAX_STAKE_AMOUNT {
return Err(FactoryError::InvalidMinStakeAmount);
}
// Validation 47: Reject an undersized raw min_stake_amount (magnitude
// beyond MAX_STAKE_AMOUNT on the negative side) before the non-positive
// defaulting logic. A value like `i128::MIN` is corrupt input, not a
// request to use the dust default (#540).
if min_stake_amount < -MAX_STAKE_AMOUNT {
return Err(FactoryError::InvalidMinStakeAmount);
}
let effective_min_stake = if min_stake_amount <= 0 {
MIN_STAKE_AMOUNT
} else {
Expand Down Expand Up @@ -1572,6 +1599,11 @@ impl Factory {
validate_factory_admin(&env, &admin)?;
admin.require_auth();

// Validation 50: Reject an empty batch before any work is done. A
// zero-length request consumes admin authorization and bumps instance
// TTL while creating nothing (#543).
if pools.is_empty() {
return Err(FactoryError::EmptyBatch);
// Validation 35: Cap the batch size before any pool is deployed. Each
// entry triggers a WASM deploy inside `create_pool_inner`, so an
// unbounded batch can exhaust the transaction's resource budget and
Expand Down
81 changes: 81 additions & 0 deletions soroban/contracts/factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ use soroban_sdk::{

use farming_pool::FarmingPoolClient;

/// A test-only contract that answers SEP-41 `symbol` but has no `balance`
/// entry point. Used to prove `validate_asset` rejects half-implemented token
/// interfaces rather than treating any `symbol` responder as a stake asset
/// (#542).
#[contract]
pub struct SymbolOnlyToken;

#[contractimpl]
impl SymbolOnlyToken {
pub fn symbol(env: Env) -> soroban_sdk::String {
soroban_sdk::String::from_str(&env, "FAKE")
}
}

// The contract crate is `#![no_std]`; these tests assert on `std` collection
// types, so the shim is declared here (same as farming-pool's tests).
extern crate std;
Expand Down Expand Up @@ -1467,6 +1481,73 @@ fn test_create_pool_rejects_factory_address_as_asset() {
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pool_rejects_min_stake_amount_below_negative_max() {
let t = setup();
let asset = test_asset(&t.env);

let result = t.client.try_create_pool(
&asset,
&1_728_000u128,
&2u32,
&25u64,
&(0i128 - MAX_STAKE_AMOUNT - 1),
);
assert_eq!(result, Err(Ok(FactoryError::InvalidMinStakeAmount)));
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pool_rejects_i128_min_min_stake_amount() {
let t = setup();
let asset = test_asset(&t.env);

let result = t
.client
.try_create_pool(&asset, &1_728_000u128, &2u32, &25u64, &i128::MIN);
assert_eq!(result, Err(Ok(FactoryError::InvalidMinStakeAmount)));
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pool_rejects_asset_equal_to_admin() {
let t = setup();
let asset = test_asset(&t.env);

// Point the factory admin at the token contract so `asset == admin`, then
// confirm the pool is refused rather than deployed with a token-controlled
// admin (#541).
t.client.transfer_admin(&asset);
t.env.mock_all_auths();

let result = t
.client
.try_create_pool(&asset, &1_728_000u128, &2u32, &25u64, &0i128);
assert_eq!(result, Err(Ok(FactoryError::InvalidAsset)));
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pool_rejects_asset_without_balance_entry_point() {
let t = setup();
let asset = t.env.register(SymbolOnlyToken, ());

let result = t
.client
.try_create_pool(&asset, &1_728_000u128, &2u32, &25u64, &0i128);
assert_eq!(result, Err(Ok(FactoryError::InvalidAsset)));
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pools_batch_rejects_empty_batch() {
let t = setup();

let result = t.client.try_create_pools_batch(&vec![&t.env]);
assert_eq!(result, Err(Ok(FactoryError::EmptyBatch)));
assert_eq!(t.client.pool_count(), 0);
}

#[test]
fn test_create_pool_rejects_non_token_asset() {
let t = setup();
Expand Down
4 changes: 4 additions & 0 deletions soroban/contracts/factory/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ pub enum FactoryError {
/// threshold, which would trap staked funds for an effectively permanent
/// window.
MinLockPeriodAboveCeiling = 19,
/// `create_pools_batch` was called with an empty `pools` vector, which
/// would consume admin authorization and bump instance TTL while creating
/// nothing.
EmptyBatch = 20,
/// `create_pools_batch` was called with more entries than `MAX_BATCH_SIZE`.
BatchTooLarge = 20,
}
Loading