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
182 changes: 124 additions & 58 deletions soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod types;

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, Env,
};
Expand All @@ -26,8 +28,18 @@ fn bump_user(env: &Env, key: &DataKey) {
.extend_ttl(key, USER_TTL_THRESHOLD, USER_TTL_EXTEND_TO);
}

fn get_admin(env: &Env) -> Address {
env.storage().instance().get(&DataKey::Admin).unwrap()
fn require_initialized(env: &Env) -> Result<(), PoolError> {
if !env.storage().instance().has(&DataKey::Admin) {
return Err(PoolError::NotInitialized);
}
Ok(())
}

fn get_admin(env: &Env) -> Result<Address, PoolError> {
env.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(PoolError::NotInitialized)
}

fn get_global_multiplier(env: &Env) -> u32 {
Expand All @@ -44,8 +56,11 @@ fn get_credit_rate(env: &Env) -> i128 {
.unwrap_or(1)
}

fn get_stake_token(env: &Env) -> Address {
env.storage().instance().get(&DataKey::StakeToken).unwrap()
fn get_stake_token(env: &Env) -> Result<Address, PoolError> {
env.storage()
.instance()
.get(&DataKey::StakeToken)
.ok_or(PoolError::NotInitialized)
}

fn get_min_lock_period(env: &Env) -> u32 {
Expand Down Expand Up @@ -156,7 +171,8 @@ fn checkpoint(env: &Env, user: &Address, stake: &mut UserStake) {
let rate = get_credit_rate(env);
let current = env.ledger().sequence();
let elapsed = current.saturating_sub(stake.start_ledger);
stake.credits_banked += compute_credits(stake.amount, allocation_pct, multiplier, rate, elapsed);
stake.credits_banked +=
compute_credits(stake.amount, allocation_pct, multiplier, rate, elapsed);
stake.start_ledger = current;
}

Expand Down Expand Up @@ -188,19 +204,28 @@ impl FarmingPool {
global_multiplier: u32,
credit_rate: i128,
min_lock_period: u32,
) {
) -> Result<(), PoolError> {
if env.storage().instance().has(&DataKey::Admin) {
panic!("already initialized");
return Err(PoolError::AlreadyInitialized);
}
assert!(global_multiplier >= 1, "multiplier must be >= 1");
assert!(credit_rate > 0, "credit_rate must be positive");

env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::StakeToken, &stake_token);
env.storage().instance().set(&DataKey::GlobalMultiplier, &global_multiplier);
env.storage().instance().set(&DataKey::CreditRate, &credit_rate);
env.storage().instance().set(&DataKey::MinLockPeriod, &min_lock_period);
env.storage()
.instance()
.set(&DataKey::StakeToken, &stake_token);
env.storage()
.instance()
.set(&DataKey::GlobalMultiplier, &global_multiplier);
env.storage()
.instance()
.set(&DataKey::CreditRate, &credit_rate);
env.storage()
.instance()
.set(&DataKey::MinLockPeriod, &min_lock_period);
bump_instance(&env);
Ok(())
}

// ── Lock / Unlock system ─────────────────────────────────────────────────
Expand All @@ -209,8 +234,9 @@ impl FarmingPool {
/// checkpointed first and the new amount is added to the existing position.
///
/// Emits a `("pool", "locked")` event with `(user, amount)`.
pub fn lock_assets(env: Env, user: Address, amount: i128) {
pub fn lock_assets(env: Env, user: Address, amount: i128) -> Result<(), PoolError> {
user.require_auth();
require_initialized(&env)?;
assert!(!pool_is_paused(&env), "pool is paused");
assert!(amount > 0, "amount must be positive");
bump_instance(&env);
Expand All @@ -229,24 +255,30 @@ impl FarmingPool {
}
};

token::TokenClient::new(&env, &get_stake_token(&env))
.transfer(&user, &env.current_contract_address(), &amount);
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&user,
&env.current_contract_address(),
&amount,
);

set_position(&env, &user, &pos);

env.events().publish(
(symbol_short!("pool"), symbol_short!("locked")),
(user, amount),
);
Ok(())
}

/// Unlock `amount` tokens for the caller. The minimum lock period (in ledgers) must
/// have elapsed since the position was created. Partial unlocks are supported; the
/// remaining balance stays locked.
///
/// Emits a `("pool", "unlocked")` event with `(user, amount, total_credits)`.
pub fn unlock_assets(env: Env, user: Address, amount: i128) {
pub fn unlock_assets(env: Env, user: Address, amount: i128) -> Result<(), PoolError> {
user.require_auth();
require_initialized(&env)?;
assert!(!pool_is_paused(&env), "pool is paused");
assert!(amount > 0, "amount must be positive");
bump_instance(&env);
Expand All @@ -265,8 +297,12 @@ impl FarmingPool {
let total_credits = pos.total_credits;
pos.amount -= amount;

token::TokenClient::new(&env, &get_stake_token(&env))
.transfer(&env.current_contract_address(), &user, &amount);
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&env.current_contract_address(),
&user,
&amount,
);

if pos.amount == 0 {
remove_position(&env, &user);
Expand All @@ -278,57 +314,64 @@ impl FarmingPool {
(symbol_short!("pool"), symbol_short!("unlocked")),
(user, amount, total_credits),
);
Ok(())
}

/// Return total credits for `user` (banked + currently accruing). Returns 0 if no position.
pub fn calculate_credits(env: Env, user: Address) -> i128 {
pub fn calculate_credits(env: Env, user: Address) -> Result<i128, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
let Some(pos) = get_position(&env, &user) else {
return 0;
return Ok(0);
};
let rate = get_credit_rate(&env);
let elapsed = env.ledger().sequence().saturating_sub(pos.checkpoint_ledger);
pos.total_credits + pos.amount * rate * elapsed as i128
let elapsed = env
.ledger()
.sequence()
.saturating_sub(pos.checkpoint_ledger);
Ok(pos.total_credits + pos.amount * rate * elapsed as i128)
}

/// Return the current position for `user`, or `None` if no position exists.
pub fn get_user_position(env: Env, user: Address) -> Option<Position> {
pub fn get_user_position(env: Env, user: Address) -> Result<Option<Position>, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
get_position(&env, &user)
Ok(get_position(&env, &user))
}

// ── Pause / Unpause ───────────────────────────────────────────────────────

/// Admin: pause the pool. While paused, `lock_assets` and `unlock_assets` are blocked.
///
/// Emits a `("pool", "paused")` event.
pub fn pause(env: Env) {
get_admin(&env).require_auth();
pub fn pause(env: Env) -> Result<(), PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
bump_instance(&env);
env.storage().instance().set(&DataKey::Paused, &true);
env.events().publish(
(symbol_short!("pool"), symbol_short!("paused")),
(),
);
env.events()
.publish((symbol_short!("pool"), symbol_short!("paused")), ());
Ok(())
}

/// Admin: unpause the pool, restoring normal operation.
///
/// Emits a `("pool", "unpaused")` event.
pub fn unpause(env: Env) {
get_admin(&env).require_auth();
pub fn unpause(env: Env) -> Result<(), PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
bump_instance(&env);
env.storage().instance().set(&DataKey::Paused, &false);
env.events().publish(
(symbol_short!("pool"), symbol_short!("unpaused")),
(),
);
env.events()
.publish((symbol_short!("pool"), symbol_short!("unpaused")), ());
Ok(())
}

/// Return whether the pool is currently paused.
pub fn is_paused(env: Env) -> bool {
pub fn is_paused(env: Env) -> Result<bool, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
pool_is_paused(&env)
Ok(pool_is_paused(&env))
}

/// Admin: return all tokens for `user` during an emergency.
Expand Down Expand Up @@ -392,8 +435,9 @@ impl FarmingPool {
// ── Boost / Stake system (unchanged) ─────────────────────────────────────

/// Stake `amount` tokens. If a prior stake exists, earned credits are checkpointed first.
pub fn stake(env: Env, from: Address, amount: i128) {
pub fn stake(env: Env, from: Address, amount: i128) -> Result<(), PoolError> {
from.require_auth();
require_initialized(&env)?;
assert!(amount > 0, "amount must be positive");
bump_instance(&env);

Expand All @@ -411,27 +455,37 @@ impl FarmingPool {
};

// Pull tokens from caller into the contract.
token::TokenClient::new(&env, &get_stake_token(&env))
.transfer(&from, &env.current_contract_address(), &amount);
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&from,
&env.current_contract_address(),
&amount,
);

set_user_stake(&env, &from, &new_stake);
Ok(())
}

/// Unstake all tokens. Returns the total credits earned.
pub fn unstake(env: Env, from: Address) -> i128 {
pub fn unstake(env: Env, from: Address) -> Result<i128, PoolError> {
from.require_auth();
require_initialized(&env)?;
bump_instance(&env);

let mut stake = get_user_stake(&env, &from).expect("no active stake");
checkpoint(&env, &from, &mut stake);
let total_credits = stake.credits_banked;

// Return staked tokens to caller.
token::TokenClient::new(&env, &get_stake_token(&env))
.transfer(&env.current_contract_address(), &from, &stake.amount);
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&env.current_contract_address(),
&from,
&stake.amount,
);

remove_user_stake(&env, &from);
total_credits
Ok(total_credits)
}

/// Set the caller's boost allocation percentage (1–100%).
Expand All @@ -440,8 +494,9 @@ impl FarmingPool {
/// rewards are lost when the boost is updated.
///
/// Emits a `boost_applied` event.
pub fn set_boost(env: Env, user: Address, allocation_pct: u32) {
pub fn set_boost(env: Env, user: Address, allocation_pct: u32) -> Result<(), PoolError> {
user.require_auth();
require_initialized(&env)?;
assert!(
allocation_pct >= 1 && allocation_pct <= 100,
"allocation_pct must be 1–100"
Expand All @@ -463,55 +518,66 @@ impl FarmingPool {
(symbol_short!("boost"), symbol_short!("applied")),
(user, allocation_pct, multiplier),
);
Ok(())
}

/// Return the current boost configuration for `user`, or `None` if no boost is set.
///
/// `BoostConfig.multiplier` reflects the current global multiplier.
/// `BoostConfig.allocation_pct` is the user's chosen allocation.
pub fn get_boost_config(env: Env, user: Address) -> Option<BoostConfig> {
pub fn get_boost_config(env: Env, user: Address) -> Result<Option<BoostConfig>, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
get_user_boost(&env, &user).map(|allocation_pct| BoostConfig {
multiplier: get_global_multiplier(&env),
allocation_pct,
})
Ok(
get_user_boost(&env, &user).map(|allocation_pct| BoostConfig {
multiplier: get_global_multiplier(&env),
allocation_pct,
}),
)
}

/// Admin: update the global boost multiplier.
///
/// Emits a `mult_set` event. Note that in-flight credits are not retroactively
/// recalculated; the new multiplier applies from the next ledger onward for
/// users whose boost configs are not checkpointed yet.
pub fn set_global_multiplier(env: Env, multiplier: u32) {
get_admin(&env).require_auth();
pub fn set_global_multiplier(env: Env, multiplier: u32) -> Result<(), PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
assert!(multiplier >= 1, "multiplier must be >= 1");
bump_instance(&env);

env.storage().instance().set(&DataKey::GlobalMultiplier, &multiplier);
env.storage()
.instance()
.set(&DataKey::GlobalMultiplier, &multiplier);

env.events().publish(
(symbol_short!("boost"), symbol_short!("mult_set")),
multiplier,
);
Ok(())
}

/// Return total credits for `user` in the boost/stake system (banked + currently accruing).
pub fn get_credits(env: Env, user: Address) -> i128 {
pub fn get_credits(env: Env, user: Address) -> Result<i128, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
let Some(stake) = get_user_stake(&env, &user) else {
return 0;
return Ok(0);
};
let allocation_pct = get_user_boost(&env, &user).unwrap_or(0);
let multiplier = get_global_multiplier(&env);
let rate = get_credit_rate(&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, allocation_pct, multiplier, rate, elapsed))
}

/// Return the current stake record for `user`, or `None` if not staked.
pub fn get_stake(env: Env, user: Address) -> Option<UserStake> {
pub fn get_stake(env: Env, user: Address) -> Result<Option<UserStake>, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
get_user_stake(&env, &user)
Ok(get_user_stake(&env, &user))
}
}

Expand Down
Loading
Loading