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
7 changes: 6 additions & 1 deletion .github/workflows/onchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ jobs:

- name: Remove base worktree
if: always() && github.event_name == 'pull_request'
run: git worktree remove --force "$RUNNER_TEMP/lumenpulse-base"
run: |
if git -C "$RUNNER_TEMP/lumenpulse-base" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git worktree remove --force "$RUNNER_TEMP/lumenpulse-base"
else
echo "Base worktree not present; skipping cleanup."
fi

- name: Run tests
if: env.SKIP_TESTS != 'true'
Expand Down
1 change: 1 addition & 0 deletions apps/onchain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"contracts/project_registry",
"contracts/protocol_registry",
"contracts/reentrancy-guard",
"contracts/stable_swap_pool",
"contracts/upgradable-contract",
"contracts/version-interface",
"contracts/vesting-wallet",
Expand Down
3 changes: 1 addition & 2 deletions apps/onchain/contracts/stable_swap_pool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "=21.5.1"
soroban-sdk-macros = "=21.5.1"
soroban-sdk = { workspace = true }
reentrancy-guard = { path = "../reentrancy-guard" }

[dev-dependencies]
Expand Down
88 changes: 53 additions & 35 deletions apps/onchain/contracts/stable_swap_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@
mod events;
mod storage;

#[cfg(test)]
mod test;

use reentrancy_guard::{acquire as acquire_reentrancy, release as release_reentrancy};
use soroban_sdk::token::TokenClient;
use soroban_sdk::{contract, contractimpl, Address, Env, Symbol, Vec};
use soroban_sdk::{contract, contracterror, contractimpl, Address, Env, Symbol, Vec};
use storage::{DataKey, LEDGER_BUMP, LEDGER_THRESHOLD};

const AMPLIFICATION_FACTOR: i128 = 100; // A parameter for stable swap bonding curve
const SWAP_FEE_BP: u32 = 4; // 0.04% swap fee in basis points
const LP_FEE_BP: u32 = 1; // 0.01% LP fee

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum StableSwapPoolError {
AlreadyInitialized = 1,
NotInitialized = 2,
InvalidAmount = 3,
SlippageExceeded = 4,
InsufficientBalance = 5,
Reentrancy = 6,
}

#[contract]
pub struct StableSwapPoolContract;

Expand All @@ -22,11 +37,11 @@ pub struct StableSwapPoolContract;
/// - AMM-based pricing
#[contractimpl]
impl StableSwapPoolContract {
fn with_reentrancy_guard<T, F>(env: &Env, f: F) -> Result<T, Symbol>
fn with_reentrancy_guard<T, F>(env: &Env, f: F) -> Result<T, StableSwapPoolError>
where
F: FnOnce() -> Result<T, Symbol>,
F: FnOnce() -> Result<T, StableSwapPoolError>,
{
acquire_reentrancy(env).map_err(|_| Symbol::new(env, "reentrancy"))?;
acquire_reentrancy(env).map_err(|_| StableSwapPoolError::Reentrancy)?;
let result = f();
release_reentrancy(env);
result
Expand Down Expand Up @@ -66,9 +81,10 @@ impl StableSwapPoolContract {
admin: Address,
token_a: Address,
token_b: Address,
) -> Result<(), Symbol> {
) -> Result<(), StableSwapPoolError> {
admin.require_auth();
if env.storage().instance().has(&DataKey::Admin) {
return Err(Symbol::new(&env, "already_initialized"));
return Err(StableSwapPoolError::AlreadyInitialized);
}

env.storage().instance().set(&DataKey::Admin, &admin);
Expand All @@ -90,13 +106,15 @@ impl StableSwapPoolContract {
/// Returns LP tokens minted
pub fn add_liquidity(
env: Env,
user: Address,
amount_a: i128,
amount_b: i128,
min_lp: i128,
) -> Result<i128, Symbol> {
) -> Result<i128, StableSwapPoolError> {
user.require_auth();
Self::with_reentrancy_guard(&env, || {
if amount_a <= 0 || amount_b <= 0 {
return Err(Symbol::new(&env, "invalid_amount"));
return Err(StableSwapPoolError::InvalidAmount);
}

Self::bump_instance(&env);
Expand All @@ -105,20 +123,20 @@ impl StableSwapPoolContract {
.storage()
.instance()
.get(&DataKey::TokenA)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;

let token_b_addr: Address = env
.storage()
.instance()
.get(&DataKey::TokenB)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;

// Transfer tokens from caller
let token_a = TokenClient::new(&env, &token_a_addr);
let token_b = TokenClient::new(&env, &token_b_addr);

token_a.transfer(&env.invoker(), &env.current_contract_address(), &amount_a);
token_b.transfer(&env.invoker(), &env.current_contract_address(), &amount_b);
token_a.transfer(&user, &env.current_contract_address(), &amount_a);
token_b.transfer(&user, &env.current_contract_address(), &amount_b);

// Calculate LP tokens
let lp_supply: i128 = env
Expand Down Expand Up @@ -154,7 +172,7 @@ impl StableSwapPoolContract {
};

if lp_tokens < min_lp {
return Err(Symbol::new(&env, "slippage_exceeded"));
return Err(StableSwapPoolError::SlippageExceeded);
}

// Update reserves
Expand All @@ -177,17 +195,17 @@ impl StableSwapPoolContract {
let user_lp: i128 = env
.storage()
.persistent()
.get(&DataKey::UserLPBalance(env.invoker()))
.get(&DataKey::UserLPBalance(user.clone()))
.unwrap_or(0);
env.storage().persistent().set(
&DataKey::UserLPBalance(env.invoker()),
&DataKey::UserLPBalance(user.clone()),
&(user_lp + lp_tokens),
);
Self::bump_pool_ttl(&env);
Self::bump_user_lp_ttl(&env, &env.invoker());

events::LiquidityAddedEvent {
user: env.invoker(),
user: user.clone(),
amount_a,
amount_b,
lp_tokens,
Expand All @@ -201,26 +219,24 @@ impl StableSwapPoolContract {
/// Remove liquidity (both tokens proportionally)
pub fn remove_liquidity(
env: Env,
user: Address,
lp_amount: i128,
min_a: i128,
min_b: i128,
) -> Result<(i128, i128), Symbol> {
) -> Result<(i128, i128), StableSwapPoolError> {
user.require_auth();
if lp_amount <= 0 {
return Err(Symbol::new(&env, "invalid_amount"));
return Err(StableSwapPoolError::InvalidAmount);
}

Self::bump_instance(&env);

let user = env.invoker();

let user_lp: i128 = env
.storage()
.persistent()
.get(&DataKey::UserLPBalance(user.clone()))
.unwrap_or(0);

if user_lp < lp_amount {
return Err(Symbol::new(&env, "insufficient_balance"));
return Err(StableSwapPoolError::InsufficientBalance);
}

let lp_supply: i128 = env
Expand All @@ -246,7 +262,7 @@ impl StableSwapPoolContract {
let out_b = (lp_amount * reserve_b) / lp_supply;

if out_a < min_a || out_b < min_b {
return Err(Symbol::new(&env, "slippage_exceeded"));
return Err(StableSwapPoolError::SlippageExceeded);
}

// Update reserves
Expand Down Expand Up @@ -275,12 +291,12 @@ impl StableSwapPoolContract {
.storage()
.instance()
.get(&DataKey::TokenA)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;
let token_b_addr: Address = env
.storage()
.instance()
.get(&DataKey::TokenB)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;

let token_a = TokenClient::new(&env, &token_a_addr);
let token_b = TokenClient::new(&env, &token_b_addr);
Expand All @@ -299,15 +315,17 @@ impl StableSwapPoolContract {
Ok((out_a, out_b))
}

/// Swap tokens (A -> B or B -> A)
/// Swap tokens (A -> B or B -> A)
pub fn swap(
env: Env,
user: Address,
input_token: Address,
amount_in: i128,
min_out: i128,
) -> Result<i128, Symbol> {
) -> Result<i128, StableSwapPoolError> {
user.require_auth();
if amount_in <= 0 {
return Err(Symbol::new(&env, "invalid_amount"));
return Err(StableSwapPoolError::InvalidAmount);
}

Self::bump_instance(&env);
Expand All @@ -316,12 +334,12 @@ impl StableSwapPoolContract {
.storage()
.instance()
.get(&DataKey::TokenA)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;
let token_b_addr: Address = env
.storage()
.instance()
.get(&DataKey::TokenB)
.ok_or_else(|| Symbol::new(&env, "not_initialized"))?;
.ok_or(StableSwapPoolError::NotInitialized)?;

let (reserve_in, reserve_out, output_token) = if input_token == token_a_addr {
let ra: i128 = env
Expand Down Expand Up @@ -356,13 +374,13 @@ impl StableSwapPoolContract {
let amount_out = (reserve_out * amount_after_fee) / (reserve_in + amount_after_fee);

if amount_out < min_out {
return Err(Symbol::new(&env, "slippage_exceeded"));
return Err(StableSwapPoolError::SlippageExceeded);
}

Self::with_reentrancy_guard(&env, || {
// Transfer input tokens from caller
let token_in = TokenClient::new(&env, &input_token);
token_in.transfer(&env.invoker(), &env.current_contract_address(), &amount_in);
token_in.transfer(&user, &env.current_contract_address(), &amount_in);

// Update reserves
if input_token == token_a_addr {
Expand All @@ -380,10 +398,10 @@ impl StableSwapPoolContract {

// Transfer output tokens to caller
let token_out = TokenClient::new(&env, &output_token);
token_out.transfer(&env.current_contract_address(), &env.invoker(), &amount_out);
token_out.transfer(&env.current_contract_address(), &user, &amount_out);

events::SwapEvent {
user: env.invoker(),
user: user.clone(),
input_token,
amount_in,
output_token,
Expand Down
Loading
Loading