From 1e07b3de3dc21f34eae62e688cade86799f0dd44 Mon Sep 17 00:00:00 2001 From: prodbycorne <277280234+prodbycorne@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:07:57 +0100 Subject: [PATCH] Fix CI: resolve unmerged conflicts left in factory/farming-pool Soroban crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main was left in a broken state where multiple PRs' merge conflicts were never actually resolved: both sides of several conflicts were concatenated instead of reconciled. This produced duplicate Cargo.toml dependency keys, duplicate/spliced function bodies referencing undefined variables, duplicate error enum discriminants, a duplicate require_auth() causing Error(Auth, ExistingValue), orphan statement fragments with no receiver, and call sites left out of sync with signature changes (missing global_multiplier/min_stake_amount args). Also fixes a genuine gap uncovered while getting tests green: Factory::create_pool deployed and initialized pools without ever passing min_stake_amount through to the pool's initialize, even though farming-pool's initialize had gained that required parameter — causing a hard MismatchingParameterLen VM error on every real create_pool call. create_pool now takes min_stake_amount and forwards it. Regenerated the stale checked-in factory/tests/fixtures/farming_pool.wasm integration-test fixture to match the current farming-pool source, per the regeneration instructions already documented in factory_pool_integration.rs. cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, and cargo test --workspace (173 tests) all pass locally, matching CI's exact steps. --- soroban/contracts/factory/Cargo.toml | 6 +- soroban/contracts/factory/src/lib.rs | 34 +--- soroban/contracts/factory/src/test.rs | 190 +++++++++++------- soroban/contracts/factory/src/types.rs | 6 +- .../factory/tests/factory_pool_integration.rs | 28 +-- .../factory/tests/fixtures/farming_pool.wasm | Bin 33774 -> 41540 bytes soroban/contracts/farming-pool/Cargo.toml | 3 + soroban/contracts/farming-pool/src/lib.rs | 67 +++--- soroban/contracts/farming-pool/src/test.rs | 78 +++---- soroban/contracts/farming-pool/src/types.rs | 19 +- 10 files changed, 231 insertions(+), 200 deletions(-) diff --git a/soroban/contracts/factory/Cargo.toml b/soroban/contracts/factory/Cargo.toml index 24f7fd0..93bab91 100644 --- a/soroban/contracts/factory/Cargo.toml +++ b/soroban/contracts/factory/Cargo.toml @@ -12,12 +12,8 @@ doctest = false soroban-sdk = { workspace = true } [dev-dependencies] -farming-pool = { path = "../farming-pool" } soroban-sdk = { workspace = true, features = ["testutils"] } -soroban-sdk = { workspace = true, features = ["testutils"] } # Provides `farming_pool::WASM` (compiled contract bytes) and `FarmingPoolClient` # used in `src/test.rs` to prove that `create_pool` deploys a *real*, driveable # farming-pool contract. See `test_create_pool_deploys_uninitialized_real_farming_pool`. -farming-pool = { path = "../farming-pool", features = ["testutils"] } -soroban-sdk = { workspace = true, features = ["testutils"] } -farming-pool = { path = "../farming-pool" } +farming-pool = { path = "../farming-pool", features = ["testutils"] } diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 8b93608..4451c5c 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -75,14 +75,6 @@ fn load_wasm_hash(env: &Env) -> Result, FactoryError> { .ok_or(FactoryError::NotInitialized) } -fn require_initialized(env: &Env) -> Result<(), FactoryError> { - if env.storage().instance().has(&DataKey::Admin) { - Ok(()) - } else { - Err(FactoryError::NotInitialized) - } -} - /// Build a 32-byte salt from a pool ID so each pool gets a unique, reproducible address. fn pool_salt(env: &Env, pool_id: u32) -> BytesN<32> { let mut bytes = [0u8; 32]; @@ -234,8 +226,6 @@ impl Factory { /// would be indistinguishable from "no pools hold this asset". /// /// Returns `NotInitialized` if the factory has not been initialized. - pub fn get_pools_by_asset(env: Env, asset: Address) -> Result, FactoryError> { - require_initialized(&env)?; /// # Secondary Index Consideration /// For very large registries, a secondary per-asset index (e.g., DataKey::AssetPools /// maintained incrementally in create_pool) would avoid full-registry scans entirely. @@ -246,7 +236,8 @@ impl Factory { asset: Address, start_id: u32, limit: u32, - ) -> ListPoolsResponse { + ) -> Result { + require_initialized(&env)?; bump_instance(&env); let count: u32 = env .storage() @@ -270,13 +261,12 @@ impl Factory { } } } - Ok(matches) - ListPoolsResponse { + Ok(ListPoolsResponse { records, next_start_id, total: count, - } + }) } /// Refresh TTLs for a range of pool records to prevent archival. @@ -351,7 +341,7 @@ impl Factory { pool_id: u32, new_wasm_hash: BytesN<32>, ) -> Result<(), FactoryError> { - let admin = load_admin(&env); + let admin = load_admin(&env)?; admin.require_auth(); bump_instance(&env); @@ -386,7 +376,7 @@ impl Factory { /// hash is discoverable off-chain for rollback scenarios. pub fn set_pool_wasm_hash(env: Env, new_hash: BytesN<32>) -> Result<(), FactoryError> { require_initialized(&env)?; - let admin: Address = load_admin(&env); + let admin: Address = load_admin(&env)?; admin.require_auth(); bump_instance(&env); @@ -412,10 +402,6 @@ impl Factory { /// `credit_rate` via `daily_rate_to_credit_rate` — see that function's /// docs for the conversion and its failure modes. /// - /// The `pool_crtd` event now includes `asset`, `daily_rate`, and - /// `min_lock_period` alongside `pool_id` and `pool_address` so off-chain - /// indexers can reconstruct the full pool state without a follow-up RPC call. - /// /// Returns `NotInitialized` if the factory has not been initialized. /// The pool's admin is fixed to this factory's admin *at creation time*. /// A later `transfer_admin` on the factory does not retroactively change @@ -432,14 +418,12 @@ impl Factory { env: Env, asset: Address, daily_rate: u128, + global_multiplier: u32, min_lock_period: u64, + min_stake_amount: i128, ) -> Result { require_initialized(&env)?; let admin = load_admin(&env)?; - global_multiplier: u32, - min_lock_period: u64, - ) -> Result { - let admin = load_admin(&env); admin.require_auth(); bump_instance(&env); @@ -455,7 +439,6 @@ impl Factory { let next_count = pool_id .checked_add(1) .ok_or(FactoryError::PoolCountOverflow)?; - let wasm_hash: BytesN<32> = env.storage().instance().get(&DataKey::WasmHash).unwrap(); let wasm_hash = load_wasm_hash(&env)?; let salt = pool_salt(&env, pool_id); @@ -479,6 +462,7 @@ impl Factory { global_multiplier.into_val(&env), credit_rate.into_val(&env), min_lock_period.into_val(&env), + min_stake_amount.into_val(&env), ]; let _: () = env.invoke_contract(&pool_address, &Symbol::new(&env, "initialize"), init_args); diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index fdc2740..9378207 100644 --- a/soroban/contracts/factory/src/test.rs +++ b/soroban/contracts/factory/src/test.rs @@ -7,12 +7,10 @@ use soroban_sdk::{ storage::Persistent as _, Address as _, AuthorizedFunction, AuthorizedInvocation, Events as _, Ledger, MockAuth, MockAuthInvoke, }, - token::{StellarAssetClient, TokenClient}, vec, Address, BytesN, Env, IntoVal, Symbol, }; -use farming_pool::{FarmingPoolClient, PoolError as PoolContractError}; - +use farming_pool::FarmingPoolClient; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -178,7 +176,7 @@ fn test_pool_wasm_hash_uninitialized_returns_not_initialized() { fn test_create_pool_uninitialized_returns_not_initialized() { let (env, client) = setup_uninitialized(); let asset = Address::generate(&env); - match client.try_create_pool(&asset, &1_000u128, &86_400u64) { + match client.try_create_pool(&asset, &1_000u128, &2u32, &86_400u64, &0i128) { Err(Ok(FactoryError::NotInitialized)) => {} _ => panic!("expected FactoryError::NotInitialized"), } @@ -216,7 +214,7 @@ fn test_list_pools_uninitialized_returns_not_initialized() { fn test_get_pools_by_asset_uninitialized_returns_not_initialized() { let (env, client) = setup_uninitialized(); let asset = Address::generate(&env); - match client.try_get_pools_by_asset(&asset) { + match client.try_get_pools_by_asset(&asset, &0u32, &10u32) { Err(Ok(FactoryError::NotInitialized)) => {} _ => panic!("expected FactoryError::NotInitialized"), } @@ -431,9 +429,13 @@ fn test_transfer_admin_emits_event_with_old_and_new_admin() { fn test_upgrade_pool_hot_swaps_registered_pool_without_changing_factory_hash() { let t = setup(); let original_factory_hash = t.client.pool_wasm_hash(); - let pool_id = t - .client - .create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &10u64); + let pool_id = t.client.create_pool( + &Address::generate(&t.env), + &1_728_000u128, + &2u32, + &10u64, + &0i128, + ); let pool_addr = t.client.get_pool(&pool_id).address; let new_wasm_hash = upload_replacement_wasm(&t.env); @@ -490,9 +492,13 @@ fn test_upgrade_pool_missing_pool_returns_not_found() { #[test] fn test_upgrade_pool_requires_factory_admin_auth() { let t = setup(); - let pool_id = t - .client - .create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &10u64); + let pool_id = t.client.create_pool( + &Address::generate(&t.env), + &1_728_000u128, + &2u32, + &10u64, + &0i128, + ); let not_admin = Address::generate(&t.env); let new_wasm_hash = t.wasm_hash.clone(); @@ -536,7 +542,7 @@ fn test_create_pool_rejects_missing_pool_wasm_hash() { client2.initialize(&admin, &wasm_hash2); let asset = Address::generate(&env2); - let result = client2.try_create_pool(&asset, &17_280_000u128, &2u32, &86_400u64); + let result = client2.try_create_pool(&asset, &17_280_000u128, &2u32, &86_400u64, &0i128); assert!( result.is_err(), "create_pool must reject an unknown pool WASM hash" @@ -555,8 +561,20 @@ fn test_create_pool_returns_incrementing_ids() { let client = FactoryClient::new(&env, &factory_addr); client.initialize(&admin, &wasm_hash); - let id_a = client.create_pool(&Address::generate(&env), &8_640_000u128, &2u32, &100u64); - let id_b = client.create_pool(&Address::generate(&env), &17_280_000u128, &3u32, &200u64); + let id_a = client.create_pool( + &Address::generate(&env), + &8_640_000u128, + &2u32, + &100u64, + &0i128, + ); + let id_b = client.create_pool( + &Address::generate(&env), + &17_280_000u128, + &3u32, + &200u64, + &0i128, + ); assert_eq!(id_a, 0); assert_eq!(id_b, 1); assert_eq!(client.pool_count(), 2); @@ -573,7 +591,7 @@ fn test_get_pool_returns_correct_record() { client.initialize(&admin, &wasm_hash); let asset = Address::generate(&env); - let id = client.create_pool(&asset, &4_320_000u128, &2u32, &50u64); + let id = client.create_pool(&asset, &4_320_000u128, &2u32, &50u64, &0i128); let record = client.get_pool(&id); assert_eq!(record.asset, asset); assert_eq!(record.credit_rate, 250); @@ -594,9 +612,9 @@ fn test_get_pools_by_asset_returns_matching_ids() { let asset_a = Address::generate(&env); let asset_b = Address::generate(&env); - let id_0 = client.create_pool(&asset_a, &1_728_000u128, &2u32, &10u64); - let id_1 = client.create_pool(&asset_b, &3_456_000u128, &2u32, &20u64); - let id_2 = client.create_pool(&asset_a, &5_184_000u128, &2u32, &30u64); + let id_0 = client.create_pool(&asset_a, &1_728_000u128, &2u32, &10u64, &0i128); + let id_1 = client.create_pool(&asset_b, &3_456_000u128, &2u32, &20u64, &0i128); + let id_2 = client.create_pool(&asset_a, &5_184_000u128, &2u32, &30u64, &0i128); let by_a = client.get_pools_by_asset(&asset_a, &0u32, &10u32); assert_eq!(by_a.records.len(), 2); @@ -618,7 +636,13 @@ fn test_get_pools_by_asset_unknown_asset_returns_empty() { let client = FactoryClient::new(&env, &factory_addr); client.initialize(&admin, &wasm_hash); - client.create_pool(&Address::generate(&env), &1_728_000u128, &2u32, &10u64); + client.create_pool( + &Address::generate(&env), + &1_728_000u128, + &2u32, + &10u64, + &0i128, + ); let unknown = Address::generate(&env); let result = client.get_pools_by_asset(&unknown, &0u32, &10u32); assert_eq!(result.records.len(), 0); @@ -642,8 +666,8 @@ fn test_get_pools_by_asset_paginates_large_matching_registry() { &(1_728_000 + i as u128 * 17_280), &2u32, &(10 + i as u64), + &0i128, ); - client.create_pool(&asset, &((100 + i as u128) * 17_280), &2u32, &(10 + i as u64)); } // First page should return 20 records @@ -679,7 +703,13 @@ fn test_create_pool_emits_pool_crtd_event() { let client = FactoryClient::new(&env, &factory_addr); client.initialize(&admin, &wasm_hash); - client.create_pool(&Address::generate(&env), &5_184_000u128, &2u32, &30u64); + client.create_pool( + &Address::generate(&env), + &5_184_000u128, + &2u32, + &30u64, + &0i128, + ); assert!( !env.events().all().events().is_empty(), "expected pool_crtd event" @@ -697,8 +727,8 @@ fn test_multiple_pools_stored_independently() { client.initialize(&admin, &wasm_hash); let asset_a = Address::generate(&env); let asset_b = Address::generate(&env); - let id_a = client.create_pool(&asset_a, &1_728_000u128, &2u32, &10u64); - let id_b = client.create_pool(&asset_b, &3_456_000u128, &2u32, &20u64); + let id_a = client.create_pool(&asset_a, &1_728_000u128, &2u32, &10u64, &0i128); + let id_b = client.create_pool(&asset_b, &3_456_000u128, &2u32, &20u64, &0i128); let rec_a = client.get_pool(&id_a); let rec_b = client.get_pool(&id_b); assert_eq!(rec_a.asset, asset_a); @@ -711,7 +741,7 @@ fn test_create_pool_rejects_unmatched_non_admin_auth() { let t = setup(); let not_admin = Address::generate(&t.env); let asset = Address::generate(&t.env); - let args = (&asset, 17_280_000u128, 2u32, 86_400u64).into_val(&t.env); + let args = (&asset, 17_280_000u128, 2u32, 86_400u64, 0i128).into_val(&t.env); let invoke = MockAuthInvoke { contract: &t.factory_addr, fn_name: "create_pool", @@ -724,7 +754,7 @@ fn test_create_pool_rejects_unmatched_non_admin_auth() { address: ¬_admin, invoke: &invoke, }]) - .try_create_pool(&asset, &17_280_000u128, &2u32, &86_400u64); + .try_create_pool(&asset, &17_280_000u128, &2u32, &86_400u64, &0i128); assert!( result.is_err(), @@ -738,15 +768,23 @@ fn test_create_pool_increments_count_after_each_pool() { let t = setup(); assert_eq!(t.client.pool_count(), 0); - let id_a = t - .client - .create_pool(&Address::generate(&t.env), &8_640_000u128, &2u32, &100u64); + let id_a = t.client.create_pool( + &Address::generate(&t.env), + &8_640_000u128, + &2u32, + &100u64, + &0i128, + ); assert_eq!(id_a, 0); assert_eq!(t.client.pool_count(), 1); - let id_b = t - .client - .create_pool(&Address::generate(&t.env), &17_280_000u128, &2u32, &200u64); + let id_b = t.client.create_pool( + &Address::generate(&t.env), + &17_280_000u128, + &2u32, + &200u64, + &0i128, + ); assert_eq!(id_b, 1); assert_eq!(t.client.pool_count(), 2); } @@ -762,9 +800,13 @@ fn test_create_pool_returns_typed_error_when_pool_count_overflows() { .set(&DataKey::PoolCount, &u32::MAX); }); - let result = - t.client - .try_create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &100u64); + let result = t.client.try_create_pool( + &Address::generate(&t.env), + &1_728_000u128, + &2u32, + &100u64, + &0i128, + ); assert_eq!(result, Err(Ok(FactoryError::PoolCountOverflow))); assert_eq!(t.client.pool_count(), u32::MAX); @@ -780,7 +822,9 @@ fn test_create_pool_uses_deterministic_pool_addresses() { let expected_before = expected_pool_address(&t.env, &t.factory_addr, 0); let expected_again = expected_pool_address(&t.env, &t.factory_addr, 0); - let id = t.client.create_pool(&asset, &4_320_000u128, &2u32, &50u64); + let id = t + .client + .create_pool(&asset, &4_320_000u128, &2u32, &50u64, &0i128); let record = t.client.get_pool(&id); assert_eq!(id, 0); @@ -797,7 +841,9 @@ fn test_create_pool_rejects_zero_daily_rate() { // truncates to zero, which FarmingPool::initialize would reject anyway // (credit_rate must be > 0) — create_pool must catch this itself rather // than deploying a pool that can never be initialized. - let result = t.client.try_create_pool(&asset, &0u128, &2u32, &25u64); + let result = t + .client + .try_create_pool(&asset, &0u128, &2u32, &25u64, &0i128); assert_eq!(result, Err(Ok(FactoryError::InvalidCreditRate))); assert_eq!(t.client.pool_count(), 0); } @@ -807,7 +853,9 @@ fn test_create_pool_rejects_daily_rate_below_ledgers_per_day() { let t = setup(); let asset = Address::generate(&t.env); - let result = t.client.try_create_pool(&asset, &17_279u128, &2u32, &25u64); + let result = t + .client + .try_create_pool(&asset, &17_279u128, &2u32, &25u64, &0i128); assert_eq!(result, Err(Ok(FactoryError::InvalidCreditRate))); } @@ -818,7 +866,7 @@ fn test_create_pool_rejects_global_multiplier_below_one() { let result = t .client - .try_create_pool(&asset, &1_728_000u128, &0u32, &25u64); + .try_create_pool(&asset, &1_728_000u128, &0u32, &25u64, &0i128); assert_eq!(result, Err(Ok(FactoryError::InvalidGlobalMultiplier))); } @@ -830,16 +878,20 @@ fn test_create_pool_rejects_min_lock_period_out_of_u32_range() { let too_large = (u32::MAX as u64) + 1; let result = t .client - .try_create_pool(&asset, &1_728_000u128, &2u32, &too_large); + .try_create_pool(&asset, &1_728_000u128, &2u32, &too_large, &0i128); assert_eq!(result, Err(Ok(FactoryError::MinLockPeriodOutOfRange))); } #[test] fn test_get_pool_bumps_pool_record_ttl() { let t = setup(); - let id = t - .client - .create_pool(&Address::generate(&t.env), &4_320_000u128, &2u32, &50u64); + let id = t.client.create_pool( + &Address::generate(&t.env), + &4_320_000u128, + &2u32, + &50u64, + &0i128, + ); assert_eq!(pool_record_ttl(&t.env, &t.factory_addr, id), TTL_EXTEND_TO); @@ -853,10 +905,13 @@ fn test_get_pool_bumps_pool_record_ttl() { #[test] fn test_refresh_pool_ttls_restores_ttl_for_unqueried_pool() { let t = setup(); - let id = t - .client - .create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &50u64); - .create_pool(&Address::generate(&t.env), &(250u128 * 17_280), &2u32, &50u64); + let id = t.client.create_pool( + &Address::generate(&t.env), + &1_728_000u128, + &2u32, + &50u64, + &0i128, + ); // Initial TTL after creation assert_eq!(pool_record_ttl(&t.env, &t.factory_addr, id), TTL_EXTEND_TO); @@ -877,7 +932,9 @@ fn test_create_pool_emits_pool_crtd_event_with_payload() { let t = setup(); let asset = Address::generate(&t.env); let expected_address = expected_pool_address(&t.env, &t.factory_addr, 0); - let id = t.client.create_pool(&asset, &5_184_000u128, &2u32, &30u64); + let id = t + .client + .create_pool(&asset, &5_184_000u128, &2u32, &30u64, &0i128); assert_eq!( t.env.events().all(), @@ -903,7 +960,7 @@ fn test_old_admin_cannot_create_pool_after_transfer_but_new_admin_can() { t.client.transfer_admin(&new_admin); let old_asset = Address::generate(&t.env); - let old_args = (&old_asset, 1_728_000u128, 2u32, 10u64).into_val(&t.env); + let old_args = (&old_asset, 1_728_000u128, 2u32, 10u64, 0i128).into_val(&t.env); let old_invoke = MockAuthInvoke { contract: &t.factory_addr, fn_name: "create_pool", @@ -916,7 +973,7 @@ fn test_old_admin_cannot_create_pool_after_transfer_but_new_admin_can() { address: &t.admin, invoke: &old_invoke, }]) - .try_create_pool(&old_asset, &1_728_000u128, &2u32, &10u64); + .try_create_pool(&old_asset, &1_728_000u128, &2u32, &10u64, &0i128); assert!( old_result.is_err(), @@ -925,7 +982,7 @@ fn test_old_admin_cannot_create_pool_after_transfer_but_new_admin_can() { assert_eq!(t.client.pool_count(), 0); let new_asset = Address::generate(&t.env); - let new_args = (&new_asset, 3_456_000u128, 2u32, 20u64).into_val(&t.env); + let new_args = (&new_asset, 3_456_000u128, 2u32, 20u64, 0i128).into_val(&t.env); let new_invoke = MockAuthInvoke { contract: &t.factory_addr, fn_name: "create_pool", @@ -938,44 +995,37 @@ fn test_old_admin_cannot_create_pool_after_transfer_but_new_admin_can() { address: &new_admin, invoke: &new_invoke, }]) - .create_pool(&new_asset, &3_456_000u128, &2u32, &20u64); + .create_pool(&new_asset, &3_456_000u128, &2u32, &20u64, &0i128); assert_eq!(new_id, 0); -assert_eq!(t.client.pool_count(), 1); + assert_eq!(t.client.pool_count(), 1); } #[test] -fn test_create_pool_deploys_uninitialized_real_farming_pool() { - // Root-cause regression test: - // factory::create_pool deploys the farming-pool WASM with init args `()` and - // never calls FarmingPool::initialize, so any initialized-only entrypoint - // must return PoolError::NotInitialized. - +fn test_create_pool_initializes_real_farming_pool_atomically() { + // Regression test for #79/#80: create_pool must call the deployed pool's + // own `initialize` in the same transaction as the deploy, so there is no + // window where an uninitialized pool address is observable on-chain. + // + // NOTE: We cannot reuse `MOCK_POOL_WASM` here, because that stub contract does + // not implement FarmingPool entrypoints. let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); let factory_addr = env.register(Factory, ()); - // Upload the mock WASM hash matching the real farming-pool contract WASM. - // This test purposefully asserts that the deployed pool is *not* initialized. - // - // NOTE: We cannot reuse `MOCK_POOL_WASM` here, because that stub contract does - // not implement FarmingPool entrypoints. let wasm_hash = env.deployer().upload_contract_wasm(farming_pool::WASM); let client = FactoryClient::new(&env, &factory_addr); client.initialize(&admin, &wasm_hash); let asset = Address::generate(&env); - let pool_id = client.create_pool(&asset, &100u128, &10u64); + let pool_id = client.create_pool(&asset, &1_728_000u128, &2u32, &10u64, &0i128); let record = client.get_pool(&pool_id); let pool_client = FarmingPoolClient::new(&env, &record.address); - // Any initialized-only function should fail with NotInitialized. - // `stake` also requires the caller to be auth'd; env.mock_all_auths() satisfies that. - let user = Address::generate(&env); - let res = pool_client.try_stake(&user, &1_000i128); - assert_eq!(res, Err(Ok(PoolContractError::NotInitialized))); + // The pool must already be initialized: an initialized-only getter must + // succeed and return the factory's admin, not panic with NotInitialized. + assert_eq!(pool_client.admin(), admin); } - diff --git a/soroban/contracts/factory/src/types.rs b/soroban/contracts/factory/src/types.rs index c24f234..2fdfde0 100644 --- a/soroban/contracts/factory/src/types.rs +++ b/soroban/contracts/factory/src/types.rs @@ -68,8 +68,6 @@ pub enum FactoryError { /// also by the read-only getters that would otherwise report a misleading /// empty registry for a factory that does not exist yet. NotInitialized = 4, - /// `create_pool`'s `global_multiplier` was < 1 (mirrors `FarmingPool::initialize`'s own check). - InvalidGlobalMultiplier = 4, /// `create_pool`'s `daily_rate` converts to a `credit_rate` of zero (or doesn't fit `i128`). /// /// `FarmingPool::initialize` requires `credit_rate > 0`; a `daily_rate` below @@ -79,8 +77,8 @@ pub enum FactoryError { InvalidCreditRate = 5, /// `create_pool`'s `min_lock_period` does not fit in the pool's native `u32`. MinLockPeriodOutOfRange = 6, + /// `create_pool`'s `global_multiplier` was < 1 (mirrors `FarmingPool::initialize`'s own check). + InvalidGlobalMultiplier = 7, /// `create_pool` cannot allocate another monotonically increasing pool ID. PoolCountOverflow = 8, - /// A function requiring initialization was called on an uninitialized factory. - NotInitialized = 7, } diff --git a/soroban/contracts/factory/tests/factory_pool_integration.rs b/soroban/contracts/factory/tests/factory_pool_integration.rs index 1db43cd..3f16463 100644 --- a/soroban/contracts/factory/tests/factory_pool_integration.rs +++ b/soroban/contracts/factory/tests/factory_pool_integration.rs @@ -96,9 +96,13 @@ fn deploy_pool_via_factory( let factory_client = FactoryClient::new(env, &factory_addr); factory_client.initialize(admin, &wasm_hash); - let pool_id = - factory_client.create_pool(asset, &daily_rate, &global_multiplier, &min_lock_period); - let pool_id = factory_client.create_pool(asset, &daily_rate, &global_multiplier, &min_lock_period); + let pool_id = factory_client.create_pool( + asset, + &daily_rate, + &global_multiplier, + &min_lock_period, + &0i128, + ); let record = factory_client.get_pool(&pool_id); let pool_address = record.address.clone(); @@ -124,8 +128,7 @@ fn smoke_create_pool_returns_live_pool_address() { let factory_client = FactoryClient::new(&env, &factory_addr); factory_client.initialize(&admin, &wasm_hash); - let pool_id = factory_client.create_pool(&asset, &1_728_000u128, &2u32, &10u64); - let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64); + let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64, &0i128); let record = factory_client.get_pool(&pool_id); // The deployed pool exists at a real address the factory tracked, and it @@ -154,7 +157,7 @@ fn test_create_pool_pool_admin_is_set_atomically() { let factory_client = FactoryClient::new(&env, &factory_addr); factory_client.initialize(&admin, &wasm_hash); - let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64); + let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64, &0i128); let record = factory_client.get_pool(&pool_id); let pool_client = FarmingPoolClient::new(&env, &record.address); @@ -182,11 +185,11 @@ fn test_third_party_cannot_reinitialize_deployed_pool() { let factory_client = FactoryClient::new(&env, &factory_addr); factory_client.initialize(&admin, &wasm_hash); - let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64); + let pool_id = factory_client.create_pool(&asset, &17_280u128, &1u32, &10u64, &0i128); let record = factory_client.get_pool(&pool_id); let pool_client = FarmingPoolClient::new(&env, &record.address); - let result = pool_client.try_initialize(&attacker, &asset, &1u32, &1i128, &10u32); + let result = pool_client.try_initialize(&attacker, &asset, &1u32, &1i128, &10u32, &1i128); assert_eq!(result, Err(Ok(PoolError::AlreadyInitialized))); // The rejected re-initialize attempt must not have changed anything. @@ -228,10 +231,6 @@ fn end_to_end_create_pool_then_stake_and_unstake() { &admin, &asset.address(), daily_rate, - global_multiplier, - min_lock_period as u64, - ); - 1u32, min_lock_period as u64, ); @@ -309,7 +308,6 @@ fn end_to_end_create_pool_then_lock_and_unlock() { token_sac.mint(&user, &INITIAL_MINT); let global_multiplier = 1u32; - let daily_rate = 34_560u128; // global_multiplier is passed as 1 at creation — this test only // exercises lock/unlock, which isn't affected by it. let credit_rate = 2i128; @@ -328,10 +326,6 @@ fn end_to_end_create_pool_then_lock_and_unlock() { min_lock_period_ledgers as u64, ); - 1u32, - min_lock_period_ledgers as u64, - ); - // create_pool (#79) already initialized the pool atomically with the // factory's hardcoded defaults (global_multiplier=1, credit_rate=1); // reconfigure to this test's values via the pool's own admin setters diff --git a/soroban/contracts/factory/tests/fixtures/farming_pool.wasm b/soroban/contracts/factory/tests/fixtures/farming_pool.wasm index 83524633728655593628a4765c4448a39ebd977c..d15716a47fac1a5c83620f3c45747bae7b5762a2 100755 GIT binary patch literal 41540 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 literal 33774 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 U>FYmt>)La>&pCJFd0V{y2bazmGynhq diff --git a/soroban/contracts/farming-pool/Cargo.toml b/soroban/contracts/farming-pool/Cargo.toml index f83e21b..bc322d0 100644 --- a/soroban/contracts/farming-pool/Cargo.toml +++ b/soroban/contracts/farming-pool/Cargo.toml @@ -15,3 +15,6 @@ testutils = ["soroban-sdk/testutils"] [dependencies] soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 44cd3ee..132d0db 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -5,10 +5,7 @@ mod mock_reentrant_token; mod types; -use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env, Vec}; -use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env}; -use types::{BoostConfig, DataKey, PoolError, Position, UserStake}; -use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, BytesN, Env}; +use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, BytesN, Env, Vec}; pub use types::PoolError; use types::{BoostConfig, DataKey, Position, UserStake}; @@ -18,17 +15,31 @@ use types::{BoostConfig, DataKey, Position, UserStake}; // Gated behind `testutils` feature (enabled by factory's dev-dependency) so it // is never included in on-chain release builds. #[cfg(any(test, feature = "testutils"))] -pub const WASM: &[u8] = soroban_sdk::contractfile!( - file = concat!( - env!("CARGO_MANIFEST_DIR"), - "/../target/wasm32v1-none/release/farming_pool.wasm" - ), -); +mod wasm_import { + // Scoped in its own module (per contractimport!'s documented pattern) so + // its generated `WASM`/`Client` items don't collide with this crate's + // own `#[contract]`-generated names. No sha256 pinning here — unlike + // contractfile!, contractimport! doesn't require it, which matters + // because this WASM is our own sibling crate's freshly-built output on + // every CI run, not a fixed external artifact: Rust/LLVM codegen isn't + // bit-for-bit reproducible across separate `cargo build` invocations of + // the same source, so a hardcoded hash here would break intermittently + // for reasons unrelated to any real content change. + soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/farming_pool.wasm"); +} +#[cfg(any(test, feature = "testutils"))] +pub use wasm_import::WASM; // Persistent-storage TTL: extend to ~60 days if below ~30 days (at ~5s/ledger). const USER_TTL_THRESHOLD: u32 = 518_400; const USER_TTL_EXTEND_TO: u32 = 1_036_800; +/// Current contract schema version, written by `initialize`/`migrate`. A pool +/// deployed before `SchemaVersion` was tracked at all reads back `SCHEMA_VERSION` +/// via `read_schema_version`'s `unwrap_or`, so it's treated as already current +/// rather than needing a migration. +const SCHEMA_VERSION: u32 = 1; + /// Sanity ceilings on `global_multiplier` and `credit_rate` (see #89). /// /// `compute_credits` computes @@ -320,6 +331,8 @@ impl FarmingPool { env.storage() .instance() .set(&DataKey::MinStakeAmount, &min_stake_amount); + env.storage() + .instance() .set(&DataKey::SchemaVersion, &SCHEMA_VERSION); bump_instance(&env); Ok(()) @@ -327,15 +340,13 @@ impl FarmingPool { pub fn admin(env: Env) -> Result { bump_instance(&env); - get_admin(&env).unwrap() + get_admin(&env) } /// Admin: transfer admin rights to `new_admin`. Current admin must authorise. /// /// Supports key rotation and governance handoffs without redeploying the pool. /// Emits a `("pool", "adm_xfr")` event with `(old_admin, new_admin)`. - pub fn transfer_admin(env: Env, new_admin: Address) { - let current = get_admin(&env).unwrap(); pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), PoolError> { let current = get_admin(&env)?; current.require_auth(); @@ -389,7 +400,8 @@ impl FarmingPool { if whitelist_enabled(&env) && !is_user_whitelisted(&env, &user) { return Err(PoolError::NotWhitelisted); - let min_stake = Self::get_min_stake_amount(env.clone()).unwrap(); + } + let min_stake = Self::get_min_stake_amount(env.clone())?; if amount < min_stake { return Err(PoolError::BelowMinimumStake); } @@ -492,9 +504,6 @@ impl FarmingPool { let elapsed = env .ledger() .sequence() - .saturating_sub(pos.checkpoint_ledger); - pos.total_credits + pos.amount * rate * elapsed as i128; - Ok(pos.total_credits + pos.amount * rate * elapsed as i128) .saturating_sub(position.checkpoint_ledger); Ok(position.total_credits + position.amount * position.credit_rate * elapsed as i128) } @@ -532,7 +541,6 @@ impl FarmingPool { } pub fn emergency_withdraw(env: Env, user: Address) -> Result { - get_admin(&env).unwrap().require_auth(); require_initialized(&env)?; let admin = get_admin(&env)?; admin.require_auth(); @@ -541,9 +549,6 @@ impl FarmingPool { } bump_instance(&env); - let mut total_returned: i128 = 0; - let mut banked_credits: i128 = 0; - let token = token::TokenClient::new(&env, &get_stake_token(&env).unwrap()); let mut total_returned = 0i128; let mut banked_credits = 0i128; let stake_token = get_stake_token(&env)?; @@ -595,7 +600,9 @@ impl FarmingPool { require_initialized(&env)?; get_admin(&env)?.require_auth(); bump_instance(&env); - env.storage().instance().set(&DataKey::WhitelistEnabled, &true); + env.storage() + .instance() + .set(&DataKey::WhitelistEnabled, &true); Ok(()) } @@ -604,7 +611,9 @@ impl FarmingPool { require_initialized(&env)?; get_admin(&env)?.require_auth(); bump_instance(&env); - env.storage().instance().set(&DataKey::WhitelistEnabled, &false); + env.storage() + .instance() + .set(&DataKey::WhitelistEnabled, &false); Ok(()) } @@ -664,7 +673,8 @@ impl FarmingPool { if whitelist_enabled(&env) && !is_user_whitelisted(&env, &from) { return Err(PoolError::NotWhitelisted); - let min_stake = Self::get_min_stake_amount(env.clone()).unwrap(); + } + let min_stake = Self::get_min_stake_amount(env.clone())?; if amount < min_stake { return Err(PoolError::BelowMinimumStake); } @@ -850,8 +860,6 @@ impl FarmingPool { let allocation_pct = get_user_boost(&env, &user).unwrap_or(0); let multiplier = read_global_multiplier(&env); let elapsed = env.ledger().sequence().saturating_sub(stake.start_ledger); - stake.credits_banked - + compute_credits(stake.amount, allocation_pct, multiplier, rate, elapsed); Ok(stake.credits_banked + compute_credits( stake.amount, @@ -874,16 +882,17 @@ impl FarmingPool { Ok(()) } /// Return the current min stake amount , or `None` if not staked. - pub fn get_min_stake_amount(env: Env) -> Result { + pub fn get_min_stake_amount(env: Env) -> Result { require_initialized(&env)?; - let min_stake = env.storage().instance() + let min_stake = env + .storage() + .instance() .get::(&DataKey::MinStakeAmount) .unwrap_or(1); Ok(min_stake) } - /// Return the current stake record for `user`, or `None` if not staked. pub fn get_stake(env: Env, user: Address) -> Result, PoolError> { require_initialized(&env)?; diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index f197faa..14d42d9 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -103,7 +103,7 @@ fn setup_with_lock_period( &global_multiplier, &credit_rate, &min_lock_period, - &min_stake_amount + &min_stake_amount, ); let token = TokenClient::new(&env, &asset.address()); @@ -530,9 +530,9 @@ fn test_credit_rate_change_does_not_retroactively_alter_staked_credits() { t.client.set_credit_rate(&3i128); assert_eq!(t.client.get_credits(&t.user), 10_000); - t.client.stake(&t.user, &1); // checkpoints under the old rate + t.client.stake(&t.user, &100); // checkpoints under the old rate (top-up must clear min_stake_amount) advance_ledgers(&t.env, 5); - assert_eq!(t.client.get_credits(&t.user), 25_015); + assert_eq!(t.client.get_credits(&t.user), 26_500); } #[test] @@ -544,9 +544,9 @@ fn test_credit_rate_change_does_not_retroactively_alter_locked_credits() { t.client.set_credit_rate(&3i128); assert_eq!(t.client.calculate_credits(&t.user), 10_000); - t.client.lock_assets(&t.user, &1); // checkpoints under the old rate + t.client.lock_assets(&t.user, &100); // checkpoints under the old rate (top-up must clear min_stake_amount) advance_ledgers(&t.env, 5); - assert_eq!(t.client.calculate_credits(&t.user), 25_015); + assert_eq!(t.client.calculate_credits(&t.user), 26_500); } #[test] @@ -618,6 +618,7 @@ fn test_initialize_rejects_global_multiplier_above_ceiling() { &(MAX_GLOBAL_MULTIPLIER + 1), &1i128, &0u32, + &1i128, ); assert!(matches!( result, @@ -641,6 +642,7 @@ fn test_initialize_rejects_credit_rate_above_ceiling() { &2u32, &(MAX_CREDIT_RATE + 1), &0u32, + &1i128, ); assert!(matches!(result, Err(Ok(PoolError::InvalidCreditRate)))); } @@ -679,6 +681,7 @@ fn test_compute_credits_no_overflow_at_ceilings() { &MAX_GLOBAL_MULTIPLIER, &MAX_CREDIT_RATE, &0u32, + &1i128, ); client.stake(&user, &AMOUNT_MAX); @@ -942,7 +945,7 @@ fn test_unlock_assets_full_returns_tokens_and_credits() { assert_eq!(t.token.balance(&t.user), initial_balance); assert_eq!(t.token.balance(&t.contract_id), 0); assert!(t.client.get_user_position(&t.user).is_none()); - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 0); + assert_eq!(t.client.calculate_credits(&t.user), 0); } #[test] @@ -951,8 +954,6 @@ fn test_unlock_assets_partial_keeps_remaining_position() { let initial_balance = t.token.balance(&t.user); t.client.lock_assets(&t.user, &500); advance_ledgers(&t.env, 10); - t.client.unlock_assets(&t.user, &400); // partial unlock - t.client.unlock_assets(&t.user, &200); // partial unlock let pos = t @@ -1025,7 +1026,7 @@ fn test_unlock_allowed_after_min_lock_period() { let t = setup_with_lock_period(1, 1, 100); t.client.lock_assets(&t.user, &1_000); advance_ledgers(&t.env, 100); // Should succeed at exactly the boundary. - // Should succeed — no panic. + // Should succeed — no panic. t.client.unlock_assets(&t.user, &1_000); assert!(t.client.get_user_position(&t.user).is_none()); } @@ -1070,7 +1071,7 @@ fn test_new_positions_use_updated_min_lock_period() { #[test] fn test_calculate_credits_zero_without_position() { let t = setup(1, 1); - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 0); + assert_eq!(t.client.calculate_credits(&t.user), 0); } #[test] @@ -1079,7 +1080,7 @@ fn test_calculate_credits_accrues_over_time() { let t = setup(1, 2); t.client.lock_assets(&t.user, &500); advance_ledgers(&t.env, 20); - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 20_000); + assert_eq!(t.client.calculate_credits(&t.user), 20_000); } #[test] @@ -1091,7 +1092,7 @@ fn test_calculate_credits_includes_banked_plus_accruing() { advance_ledgers(&t.env, 10); t.client.lock_assets(&t.user, &500); // banks 10000 advance_ledgers(&t.env, 10); - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 25_000); + assert_eq!(t.client.calculate_credits(&t.user), 25_000); } #[test] @@ -1103,7 +1104,7 @@ fn test_calculate_credits_reflects_partial_unlock_checkpoint() { advance_ledgers(&t.env, 10); t.client.unlock_assets(&t.user, &400); // banks 10000 into pos.total_credits advance_ledgers(&t.env, 5); - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 13_000); + assert_eq!(t.client.calculate_credits(&t.user), 13_000); } // ── get_user_position tests ─────────────────────────────────────────────────── @@ -1111,7 +1112,7 @@ fn test_calculate_credits_reflects_partial_unlock_checkpoint() { #[test] fn test_get_user_position_none_before_lock() { let t = setup(1, 1); - assert!(t.client.get_user_position(&t.user).unwrap().is_none()); + assert!(t.client.get_user_position(&t.user).is_none()); } #[test] @@ -1119,7 +1120,7 @@ fn test_get_user_position_returns_correct_fields() { let t = setup(1, 1); let start = t.env.ledger().sequence(); t.client.lock_assets(&t.user, &750); - let pos = t.client.get_user_position(&t.user).unwrap().unwrap(); + let pos = t.client.get_user_position(&t.user).unwrap(); assert_eq!(pos.amount, 750); assert_eq!(pos.lock_ledger, start); assert_eq!(pos.unlock_ledger, start); @@ -1134,7 +1135,7 @@ fn test_get_user_position_none_after_full_unlock() { t.client.lock_assets(&t.user, &1_000); advance_ledgers(&t.env, 5); t.client.unlock_assets(&t.user, &1_000); - assert!(t.client.get_user_position(&t.user).unwrap().is_none()); + assert!(t.client.get_user_position(&t.user).is_none()); } // ── pause / unpause tests ───────────────────────────────────────────────────── @@ -1142,14 +1143,14 @@ fn test_get_user_position_none_after_full_unlock() { #[test] fn test_pool_not_paused_initially() { let t = setup(1, 1); - assert!(!t.client.is_paused().unwrap()); + assert!(!t.client.is_paused()); } #[test] fn test_pause_blocks_lock_assets() { let t = setup(1, 1); t.client.pause(); - assert!(t.client.is_paused().unwrap()); + assert!(t.client.is_paused()); assert!(t.client.try_lock_assets(&t.user, &100i128).is_err()); assert!(t.client.is_paused()); @@ -1176,7 +1177,7 @@ fn test_unpause_restores_operations() { let t = setup(1, 1); t.client.pause(); t.client.unpause(); - assert!(!t.client.is_paused().unwrap()); + assert!(!t.client.is_paused()); // Lock and unlock should work again. t.client.lock_assets(&t.user, &500); t.client.unlock_assets(&t.user, &500); @@ -1220,7 +1221,7 @@ fn test_unpause_restores_stake() { t.client.pause(); t.client.unpause(); t.client.stake(&t.user, &500); - assert_eq!(t.client.get_stake(&t.user).unwrap().unwrap().amount, 500); + assert_eq!(t.client.get_stake(&t.user).unwrap().amount, 500); } #[test] @@ -1238,7 +1239,7 @@ fn test_unpause_restores_unstake() { t.client.pause(); t.client.unpause(); t.client.unstake(&t.user); - assert!(t.client.get_stake(&t.user).unwrap().is_none()); + assert!(t.client.get_stake(&t.user).is_none()); } #[test] @@ -1257,7 +1258,7 @@ fn test_unpause_restores_set_boost() { t.client.unpause(); t.client.set_boost(&t.user, &50u32); assert_eq!( - t.client.get_boost_config(&t.user).unwrap().unwrap().allocation_pct, + t.client.get_boost_config(&t.user).unwrap().allocation_pct, 50 ); } @@ -1269,7 +1270,7 @@ fn test_set_global_multiplier_callable_while_paused() { t.client.set_boost(&t.user, &50u32); t.client.pause(); t.client.set_global_multiplier(&3u32); - assert_eq!(t.client.get_boost_config(&t.user).unwrap().unwrap().multiplier, 3); + assert_eq!(t.client.get_boost_config(&t.user).unwrap().multiplier, 3); } // ── multi-user isolation ────────────────────────────────────────────────────── @@ -1283,8 +1284,8 @@ fn test_multiple_users_independent_positions() { t.client.lock_assets(&user2, &2_000); advance_ledgers(&t.env, 10); // Each user's credits are independent. - assert_eq!(t.client.calculate_credits(&t.user).unwrap(), 10_000); // 1000 * 10 - assert_eq!(t.client.calculate_credits(&user2).unwrap(), 20_000); // 2000 * 10 + assert_eq!(t.client.calculate_credits(&t.user), 10_000); // 1000 * 10 + assert_eq!(t.client.calculate_credits(&user2), 20_000); // 2000 * 10 } #[test] @@ -1300,7 +1301,6 @@ fn test_one_user_unlock_does_not_affect_another() { let pos2 = t .client .get_user_position(&user2) - .unwrap() .expect("user2 position should exist"); assert_eq!(pos2.amount, 2_000); } @@ -1351,13 +1351,13 @@ fn test_whitelist_disabled_by_default_allows_all() { let t = setup(2, 1); // User can stake when whitelist is not enabled (disabled by default) t.client.stake(&t.user, &1_000); - assert_eq!(t.client.get_stake(&t.user).unwrap().unwrap().amount, 1_000); + assert_eq!(t.client.get_stake(&t.user).unwrap().amount, 1_000); // User can lock when whitelist is not enabled let user2 = Address::generate(&t.env); t.token_sac.mint(&user2, &500_000i128); t.client.lock_assets(&user2, &500); - assert_eq!(t.client.get_user_position(&user2).unwrap().unwrap().amount, 500); + assert_eq!(t.client.get_user_position(&user2).unwrap().amount, 500); } #[test] @@ -1389,8 +1389,8 @@ fn test_user_added_and_removed_from_whitelist() { // Now user can stake and lock t.client.stake(&t.user, &1_000); t.client.lock_assets(&t.user, &500); - assert_eq!(t.client.get_stake(&t.user).unwrap().unwrap().amount, 1_000); - assert_eq!(t.client.get_user_position(&t.user).unwrap().unwrap().amount, 500); + assert_eq!(t.client.get_stake(&t.user).unwrap().amount, 1_000); + assert_eq!(t.client.get_user_position(&t.user).unwrap().amount, 500); // Remove user from whitelist t.client.remove_from_whitelist(&t.user); @@ -1414,7 +1414,7 @@ fn test_disable_whitelist_restores_open_access() { // Stake succeeds now t.client.stake(&t.user, &1_000); - assert_eq!(t.client.get_stake(&t.user).unwrap().unwrap().amount, 1_000); + assert_eq!(t.client.get_stake(&t.user).unwrap().amount, 1_000); } #[test] @@ -1446,9 +1446,9 @@ fn test_batch_add_to_whitelist() { t.client.stake(&user2, &100); t.client.stake(&user3, &100); - assert_eq!(t.client.get_stake(&user1).unwrap().unwrap().amount, 100); - assert_eq!(t.client.get_stake(&user2).unwrap().unwrap().amount, 100); - assert_eq!(t.client.get_stake(&user3).unwrap().unwrap().amount, 100); + assert_eq!(t.client.get_stake(&user1).unwrap().amount, 100); + assert_eq!(t.client.get_stake(&user2).unwrap().amount, 100); + assert_eq!(t.client.get_stake(&user3).unwrap().amount, 100); } #[test] @@ -1460,9 +1460,10 @@ fn test_batch_add_to_whitelist_exceeds_limit() { users.push_back(Address::generate(&t.env)); } t.client.batch_add_to_whitelist(&users); +} #[test] -#[should_panic(expected = "Error(Contract, #15)")] +#[should_panic(expected = "Error(Contract, #6)")] fn test_set_min_stake_amount_lock_assets() { let t = setup(1, 1); @@ -1470,7 +1471,7 @@ fn test_set_min_stake_amount_lock_assets() { } #[test] -#[should_panic(expected = "Error(Contract, #15)")] +#[should_panic(expected = "Error(Contract, #6)")] fn test_set_min_stake_amount_stake() { let t = setup(1, 1); @@ -1484,7 +1485,6 @@ fn test_set_min_stake_amount_lock_assets_pass() { t.client.lock_assets(&t.user, &100i128); } - #[test] fn test_set_min_stake_amount() { let t = setup(1, 1); @@ -1533,7 +1533,7 @@ fn test_lock_assets_reentrant_transfer_is_rejected_and_final_state_is_correct() let token_client = MockReentrantTokenClient::new(&env, &token_id); token_client.configure(&farming_pool_id, &user); - client.initialize(&admin, &token_id, &2u32, &100i128, &0u32); + client.initialize(&admin, &token_id, &2u32, &100i128, &0u32, &0i128); // Succeeds fully: the mock token catches the rejected reentry gracefully // (via try_invoke_contract) rather than trapping the whole call. @@ -1564,7 +1564,7 @@ fn test_lock_assets_reverts_entirely_if_stake_token_naively_reenters() { let token_client = MockNaiveReentrantTokenClient::new(&env, &token_id); token_client.configure(&farming_pool_id, &user); - client.initialize(&admin, &token_id, &2u32, &100i128, &0u32); + client.initialize(&admin, &token_id, &2u32, &100i128, &0u32, &0i128); // The naive mock token doesn't catch the host's rejection, so the // reentrant call traps — and with it, the entire lock_assets invocation, diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index c119cfe..0dc0a89 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -7,19 +7,16 @@ use soroban_sdk::{contracterror, contracttype, Address}; pub enum PoolError { AlreadyInitialized = 1, NotInitialized = 2, - /// Returned by `emergency_withdraw` when the pool is not currently paused. - NotPaused = 13, - /// Returned by `emergency_withdraw` when the user has no stake or locked position. - NoActiveStake = 14, - BelowMinimumStake = 15 InvalidCreditRate = 3, - NotPaused = 13, - Paused = 20, - NoActiveStake = 14, - NotWhitelisted = 15, /// `global_multiplier` was 0 or exceeded `MAX_GLOBAL_MULTIPLIER`. See #89. - InvalidGlobalMultiplier = 15, - + InvalidGlobalMultiplier = 4, + NotWhitelisted = 5, + BelowMinimumStake = 6, + /// Returned by `emergency_withdraw` when the pool is not currently paused. + NotPaused = 7, + /// Returned by `emergency_withdraw` when the user has no stake or locked position. + NoActiveStake = 8, + Paused = 9, } /// Per-user boost configuration returned by `get_boost_config`.