Skip to content
Open
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
61 changes: 46 additions & 15 deletions soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ 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,
};
use types::{BoostConfig, DataKey, Position, UserStake};
pub use types::PoolError;

// Persistent-storage TTL: extend to ~60 days if below ~30 days (at ~5s/ledger).
const USER_TTL_THRESHOLD: u32 = 518_400;
Expand Down Expand Up @@ -204,6 +199,7 @@ impl FarmingPool {
global_multiplier: u32,
credit_rate: i128,
min_lock_period: u32,
min_stake_amount: i128,
) -> Result<(), PoolError> {
if env.storage().instance().has(&DataKey::Admin) {
return Err(PoolError::AlreadyInitialized);
Expand All @@ -224,6 +220,9 @@ impl FarmingPool {
env.storage()
.instance()
.set(&DataKey::MinLockPeriod, &min_lock_period);
env.storage()
.instance()
.set(&DataKey::MinStakeAmount, &min_stake_amount);
bump_instance(&env);
Ok(())
}
Expand All @@ -233,15 +232,15 @@ impl FarmingPool {
/// Return the current admin address.
pub fn admin(env: Env) -> Address {
bump_instance(&env);
get_admin(&env)
get_admin(&env).unwrap()
}

/// 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);
let current = get_admin(&env).unwrap();
current.require_auth();
bump_instance(&env);

Expand All @@ -262,6 +261,11 @@ impl FarmingPool {
require_initialized(&env)?;
assert!(!pool_is_paused(&env), "pool is paused");
assert!(amount > 0, "amount must be positive");
let min_stake = Self::get_min_stake_amount(env.clone()).unwrap();
if amount < min_stake {
return Err(PoolError::BelowMinimumStake);
}

bump_instance(&env);

let current = env.ledger().sequence();
Expand All @@ -278,7 +282,7 @@ impl FarmingPool {
}
};

token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
// token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&user,
Expand Down Expand Up @@ -321,7 +325,7 @@ impl FarmingPool {
let total_credits = pos.total_credits;
pos.amount -= amount;

token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
// token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&env.current_contract_address(),
Expand Down Expand Up @@ -354,7 +358,7 @@ impl FarmingPool {
.ledger()
.sequence()
.saturating_sub(pos.checkpoint_ledger);
pos.total_credits + pos.amount * rate * elapsed as i128
pos.total_credits + pos.amount * rate * elapsed as i128;
Ok(pos.total_credits + pos.amount * rate * elapsed as i128)
}

Expand Down Expand Up @@ -408,15 +412,15 @@ impl FarmingPool {
/// storage so a future claim mechanism can recover them. Emits an `emrg_exit`
/// event with `(admin, user, amount)`.
pub fn emergency_withdraw(env: Env, user: Address) -> Result<i128, PoolError> {
get_admin(&env).require_auth();
get_admin(&env).unwrap().require_auth();
if !pool_is_paused(&env) {
return Err(PoolError::NotPaused);
}
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));
let token = token::TokenClient::new(&env, &get_stake_token(&env).unwrap());

if let Some(pos) = get_position(&env, &user) {
token.transfer(&env.current_contract_address(), &user, &pos.amount);
Expand Down Expand Up @@ -467,6 +471,11 @@ impl FarmingPool {
assert!(!pool_is_paused(&env), "pool is paused");
require_initialized(&env)?;
assert!(amount > 0, "amount must be positive");
let min_stake = Self::get_min_stake_amount(env.clone()).unwrap();
if amount < min_stake {
return Err(PoolError::BelowMinimumStake);
}

bump_instance(&env);

let current = env.ledger().sequence();
Expand All @@ -483,7 +492,7 @@ impl FarmingPool {
};

// Pull tokens from caller into the contract.
token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
// token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&from,
Expand All @@ -507,7 +516,7 @@ impl FarmingPool {
let total_credits = stake.credits_banked;

// Return staked tokens to caller.
token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
// token::TokenClient::new(&env, &get_stake_token(&env)).transfer(
let stake_token = get_stake_token(&env)?;
token::TokenClient::new(&env, &stake_token).transfer(
&env.current_contract_address(),
Expand Down Expand Up @@ -602,11 +611,33 @@ impl FarmingPool {
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)
+ compute_credits(stake.amount, allocation_pct, multiplier, rate, elapsed);
Ok(stake.credits_banked
+ compute_credits(stake.amount, allocation_pct, multiplier, rate, elapsed))
}

pub fn set_min_stake_amount(env: Env, amount: i128) -> Result<(), PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
bump_instance(&env);

env.storage()
.instance()
.set(&DataKey::MinStakeAmount, &amount);

Ok(())
}
/// Return the current min stake amount , or `None` if not staked.
pub fn get_min_stake_amount(env: Env) -> Result<i128, PoolError> {
require_initialized(&env)?;
let min_stake = env.storage().instance()
.get::<DataKey, i128>(&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<Option<UserStake>, PoolError> {
require_initialized(&env)?;
Expand Down
43 changes: 42 additions & 1 deletion soroban/contracts/farming-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ fn setup_with_lock_period(

let contract_id = env.register(FarmingPool, ());
let client = FarmingPoolClient::new(&env, &contract_id);
let min_stake_amount = 100;
client.initialize(
&admin,
&asset.address(),
&global_multiplier,
&credit_rate,
&min_lock_period,
&min_stake_amount
);

let token = TokenClient::new(&env, &asset.address());
Expand Down Expand Up @@ -98,7 +100,7 @@ fn setup_without_mocked_auth() -> (Env, Address, FarmingPoolClient<'static>, Add

let contract_id = env.register(FarmingPool, ());
let client = FarmingPoolClient::new(&env, &contract_id);
client.initialize(&admin, &asset.address(), &2u32, &1i128, &0u32);
client.initialize(&admin, &asset.address(), &2u32, &1i128, &0u32, &1_i128);

let client = unsafe {
core::mem::transmute::<FarmingPoolClient<'_>, FarmingPoolClient<'static>>(client)
Expand Down Expand Up @@ -881,3 +883,42 @@ fn test_emergency_withdraw_while_unpaused_returns_not_paused() {
let result = t.client.try_emergency_withdraw(&t.user);
assert!(matches!(result, Err(Ok(PoolError::NotPaused))));
}


#[test]
#[should_panic(expected = "Error(Contract, #15)")]
fn test_set_min_stake_amount_lock_assets() {
let t = setup(1, 1);

t.client.lock_assets(&t.user, &10i128);
}

#[test]
#[should_panic(expected = "Error(Contract, #15)")]
fn test_set_min_stake_amount_stake() {
let t = setup(1, 1);

t.client.stake(&t.user, &50);
}

#[test]
fn test_set_min_stake_amount_lock_assets_pass() {
let t = setup(1, 1);

t.client.lock_assets(&t.user, &100i128);
}


#[test]
fn test_set_min_stake_amount() {
let t = setup(1, 1);

let min_stake = t.client.get_min_stake_amount();
assert_eq!(min_stake, 100);

let amount = 200_i128;

t.client.set_min_stake_amount(&amount);
let min_stake = t.client.get_min_stake_amount();
assert_eq!(min_stake, amount);
}
16 changes: 6 additions & 10 deletions soroban/contracts/farming-pool/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ 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
}

/// Per-user boost configuration returned by `get_boost_config`.
Expand Down Expand Up @@ -49,16 +54,6 @@ pub struct Position {
pub total_credits: i128,
}

/// Error codes returned by pool operations.
#[contracterror]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PoolError {
/// 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,
}

/// Storage keys for all persistent and instance data.
#[contracttype]
pub enum DataKey {
Expand All @@ -79,4 +74,5 @@ pub enum DataKey {
UserPosition(Address),
/// Credits banked for a user after an emergency withdrawal, for future claim.
BankedCredits(Address),
MinStakeAmount,
}