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
45 changes: 45 additions & 0 deletions soroban/contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::<DataKey, Vec<u32>>(&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::<DataKey, PoolRecord>(&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;
Expand Down Expand Up @@ -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<u32> = 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);
Expand Down
32 changes: 21 additions & 11 deletions soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address, PoolError> {
env.storage()
.instance()
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1337,12 +1343,16 @@ impl FarmingPool {
pub fn get_boost_config(env: Env, user: Address) -> Result<Option<BoostConfig>, 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<i128, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
Ok(read_total_credits(&env))
}

/// Set the global credit multiplier. Rejects 0 and anything above
Expand Down
115 changes: 29 additions & 86 deletions soroban/contracts/farming-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 1 addition & 17 deletions soroban/contracts/farming-pool/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address>,
/// Total number of whitelisted addresses.
pub total: u32,
TotalCredits,
}
2 changes: 1 addition & 1 deletion soroban/contracts/vesting-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down