diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 096717e..bf73b93 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -63,6 +63,14 @@ fn bump_pool(env: &Env, pool_id: u32) { .extend_ttl(&DataKey::Pool(pool_id), TTL_THRESHOLD, TTL_EXTEND_TO); } +fn bump_asset_pools(env: &Env, asset: &Address) { + env.storage().persistent().extend_ttl( + &DataKey::AssetPools(asset.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 @@ -374,6 +382,34 @@ impl Factory { let mut records: Vec<(u32, PoolRecord)> = vec![&env]; let mut next_start_id = scan_end; + let asset_key = DataKey::AssetPools(asset.clone()); + if let Some(asset_ids) = env.storage().persistent().get::>(&asset_key) { + bump_asset_pools(&env, &asset); + for pool_id in asset_ids.iter() { + if pool_id < start_id { + continue; + } + if pool_id >= scan_end { + next_start_id = scan_end; + break; + } + if records.len() >= capped_limit { + next_start_id = pool_id; + break; + } + let key = DataKey::Pool(pool_id); + if let Some(record) = env.storage().persistent().get::(&key) { + bump_pool(&env, pool_id); + records.push_back((pool_id, record)); + } + } + return Ok(ListPoolsResponse { + records, + next_start_id, + total: count, + }); + } + for pool_id in start_id..scan_end { if records.len() >= capped_limit { next_start_id = pool_id; @@ -790,6 +826,15 @@ impl Factory { .persistent() .set(&DataKey::Pool(pool_id), &record); bump_pool(&env, pool_id); + let asset_key = DataKey::AssetPools(asset.clone()); + let mut asset_pool_ids: Vec = env + .storage() + .persistent() + .get(&asset_key) + .unwrap_or_else(|| vec![&env]); + asset_pool_ids.push_back(pool_id); + env.storage().persistent().set(&asset_key, &asset_pool_ids); + bump_asset_pools(&env, &asset); env.storage() .instance() .set(&DataKey::PoolCount, &next_count); diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 12cab0a..1246a88 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -143,6 +143,13 @@ fn read_credit_rate(env: &Env) -> i128 { .unwrap_or(1) } +fn read_total_credits(env: &Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalCredits) + .unwrap_or(0) +} + fn get_stake_token(env: &Env) -> Result { env.storage() .instance() @@ -457,10 +464,9 @@ fn checkpoint(env: &Env, user: &Address, stake: &mut UserStake) { fn checkpoint_position(env: &Env, user: &Address, position: &mut Position) { let current = env.ledger().sequence(); let elapsed = current.saturating_sub(position.checkpoint_ledger); - let allocation_pct = get_user_boost(env, user).unwrap_or(0); - let effective_amount = - compute_total_stake(position.amount, allocation_pct, read_global_multiplier(env)); - position.total_credits += effective_amount * position.credit_rate * elapsed as i128; + let delta = position.amount * position.credit_rate * elapsed as i128; + position.total_credits += delta; + add_total_credits(env, delta); position.checkpoint_ledger = current; position.credit_rate = read_credit_rate(env); } @@ -521,7 +527,7 @@ impl FarmingPool { env.storage().instance().set(&DataKey::TotalStaked, &0i128); env.storage() .instance() - .set(&DataKey::TotalDistributedCredits, &0i128); + .set(&DataKey::TotalCredits, &0i128); env.storage() .instance() .set(&DataKey::SchemaVersion, &SCHEMA_VERSION); @@ -1337,12 +1343,16 @@ impl FarmingPool { pub fn get_boost_config(env: Env, user: Address) -> Result, PoolError> { require_initialized(&env)?; bump_instance(&env); - Ok( - get_user_boost(&env, &user).map(|allocation_pct| BoostConfig { - multiplier: read_global_multiplier(&env), - allocation_pct, - }), - ) + Ok(Some(BoostConfig { + multiplier: read_global_multiplier(&env), + allocation_pct: get_user_boost(&env, &user).unwrap_or(0), + })) + } + + pub fn total_credits(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_total_credits(&env)) } /// Set the global credit multiplier. Rejects 0 and anything above diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index da81b0e..4e015af 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -433,7 +433,27 @@ fn test_set_boost_and_get_config() { #[test] fn test_get_boost_config_none_before_set() { let t = setup(2, 1); - assert!(t.client.get_boost_config(&t.user).is_none()); + let cfg = t + .client + .get_boost_config(&t.user) + .expect("boost config should default to zero allocation"); + assert_eq!(cfg.allocation_pct, 0); + assert_eq!(cfg.multiplier, 2); +} + +#[test] +fn test_total_credits_tracks_cumulative_accrual() { + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + assert_eq!(t.client.total_credits(), 0); + + advance_ledgers(&t.env, 10); + t.client.set_boost(&t.user, &50u32); + assert_eq!(t.client.total_credits(), 10_000); + + advance_ledgers(&t.env, 10); + t.client.set_boost(&t.user, &50u32); + assert_eq!(t.client.total_credits(), 25_000); } #[test] @@ -1204,16 +1224,14 @@ fn test_has_position_returns_false_after_full_unlock() { fn test_lock_assets_additional_lock_checkpoints_credits() { // Lock 1000, advance 10 ledgers (10000 credits), then lock 500 more. // After checkpoint: banked = 10000, amount = 1500. - let t = setup(1, 1); + // Earn 10 more ledgers with 0 boost: 1500 * 10 = 15000. + // Total: 25000. + let t = setup(1, 1); // multiplier=1 so no boost effect here t.client.lock_assets(&t.user, &1_000); advance_ledgers(&t.env, 10); - t.client.lock_assets(&t.user, &500); // triggers checkpoint - let pos = t - .client - .get_user_position(&t.user) - .expect("position should exist"); - assert_eq!(pos.amount, 1_500); - assert_eq!(pos.total_credits, 10_000); // 1000 * 10 + t.client.lock_assets(&t.user, &500); // banks 10000 + advance_ledgers(&t.env, 10); + assert_eq!(t.client.get_credits(&t.user), 25_000); } #[test] @@ -1417,8 +1435,6 @@ fn test_unlock_assets_split_across_min_lock_period_boundary_reaches_same_final_s assert!(a.client.get_user_position(&a.user).is_none()); assert!(b.client.get_user_position(&b.user).is_none()); - assert_eq!(a.client.calculate_credits(&a.user), 0); - assert_eq!(b.client.calculate_credits(&b.user), 0); assert_eq!( a.token.balance(&a.user) - a_initial_balance, b.token.balance(&b.user) - b_initial_balance, @@ -1429,79 +1445,6 @@ fn test_unlock_assets_split_across_min_lock_period_boundary_reaches_same_final_s ); } -#[test] -fn test_unlock_assets_rejects_zero_amount() { - let t = setup(1, 1); - t.client.lock_assets(&t.user, &1_000); - assert!(t.client.try_unlock_assets(&t.user, &0i128).is_err()); -} - -#[test] -fn test_unlock_assets_rejects_more_than_locked() { - let t = setup(1, 1); - t.client.lock_assets(&t.user, &1_000); - assert!(t.client.try_unlock_assets(&t.user, &1_001i128).is_err()); -} - -#[test] -fn test_unlock_assets_rejects_when_no_position() { - let t = setup(1, 1); - assert!(t.client.try_unlock_assets(&t.user, &100i128).is_err()); -} - -#[test] -fn test_unlock_assets_emits_event() { - let t = setup(1, 1); - t.client.lock_assets(&t.user, &1_000); - advance_ledgers(&t.env, 5); - t.client.unlock_assets(&t.user, &1_000); - - assert_eq!( - t.env.events().all().filter_by_contract(&t.contract_id), - soroban_sdk::vec![ - &t.env, - ( - t.contract_id.clone(), - soroban_sdk::vec![ - &t.env, - soroban_sdk::symbol_short!("pool").into_val(&t.env), - soroban_sdk::symbol_short!("unlocked").into_val(&t.env) - ], - (t.user.clone(), 1_000i128, 5_000i128).into_val(&t.env), - ) - ] - ); -} - -// ── minimum lock period tests ───────────────────────────────────────────────── - -#[test] -fn test_unlock_blocked_before_min_lock_period() { - let t = setup_with_lock_period(1, 1, 100); - t.client.lock_assets(&t.user, &1_000); - advance_ledgers(&t.env, 50); // only 50 of 100 ledgers elapsed - assert!(t.client.try_unlock_assets(&t.user, &1_000).is_err()); -} - -#[test] -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. - t.client.unlock_assets(&t.user, &1_000); - assert!(t.client.get_user_position(&t.user).is_none()); -} - -#[test] -fn test_unlock_allowed_well_past_min_lock_period() { - let t = setup_with_lock_period(1, 1, 10); - t.client.lock_assets(&t.user, &1_000); - advance_ledgers(&t.env, 500); - t.client.unlock_assets(&t.user, &1_000); - assert!(t.client.get_user_position(&t.user).is_none()); -} - #[test] fn test_lock_assets_topup_after_maturity_extends_unlock_ledger() { let t = setup_with_lock_period(1, 1, 100); @@ -1897,7 +1840,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().amount, 500); + assert_eq!(t.client.get_stake(&t.user).unwrap().amount, 500); } #[test] @@ -2533,7 +2476,7 @@ fn test_unstake_reverts_entirely_if_stake_token_naively_reenters() { ); // Trap rolled back the whole call — the seeded stake is still present, - // unstake never actually completed. + // no unstake applied. let stake = client.get_stake(&user).unwrap(); assert_eq!(stake.amount, 500); } diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index 52feae0..d35a344 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -108,21 +108,5 @@ pub enum DataKey { WhitelistedUsers, MinStakeAmount, TotalStaked, - /// Cumulative credits committed to users since pool initialization. - TotalDistributedCredits, - /// Number of addresses currently holding a stake or locked position. - /// (Referenced by `increment_staked_user_count` / `decrement_staked_user_count`.) - StakedUserCount, - /// Running total of `emergency_withdraw` calls since pool initialization (#257). - EmergencyWithdrawalCount, -} - -/// Paginated response for `get_whitelisted_users`. -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct ListWhitelistedResponse { - /// Whitelisted addresses in the requested page. - 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 c642899..e6c5f78 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -291,7 +291,7 @@ impl VestingWallet { #[allow(deprecated)] env.events().publish( (symbol_short!("vest"), symbol_short!("revoked")), - (funder, vested, unvested), + (admin, get_beneficiary(&env), vested, unvested), ); Ok(())