diff --git a/Cargo.toml b/Cargo.toml index 1a338a0..5e3bfbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,13 @@ resolver = "2" members = [ "contracts/tbrg-token", "contracts/oracle", - "contracts/pool-factory" + "contracts/pool-factory", + "contracts/amm", + "contracts/liquidity-mining", + "contracts/synthetic-asset-factory", + "contracts/options", + "contracts/yield-strategy", + "contracts/flash-loan-router" ] exclude = [ diff --git a/contracts/amm/Cargo.toml b/contracts/amm/Cargo.toml new file mode 100644 index 0000000..5922b94 --- /dev/null +++ b/contracts/amm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "amm" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/amm/src/errors.rs b/contracts/amm/src/errors.rs new file mode 100644 index 0000000..bd19c27 --- /dev/null +++ b/contracts/amm/src/errors.rs @@ -0,0 +1,21 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum AmmError { + /// Invalid token amount provided + InvalidAmount = 1, + /// Slippage tolerance exceeded + SlippageExceeded = 2, + /// Invalid token address + InvalidToken = 3, + /// Insufficient liquidity in pool + InsufficientLiquidity = 4, + /// Insufficient LP shares + InsufficientShares = 5, + /// Invalid token ordering + InvalidTokenOrder = 6, + /// Invalid fee rate + InvalidFeeRate = 7, +} diff --git a/contracts/amm/src/events.rs b/contracts/amm/src/events.rs new file mode 100644 index 0000000..901659b --- /dev/null +++ b/contracts/amm/src/events.rs @@ -0,0 +1,29 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DepositEvent { + pub depositor: Address, + pub amount_a: i128, + pub amount_b: i128, + pub shares: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WithdrawEvent { + pub withdrawer: Address, + pub shares: i128, + pub amount_a: i128, + pub amount_b: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SwapEvent { + pub from: Address, + pub sell_token: Address, + pub sell_amount: i128, + pub buy_token: Address, + pub buy_amount: i128, +} diff --git a/contracts/amm/src/lib.rs b/contracts/amm/src/lib.rs new file mode 100644 index 0000000..71640df --- /dev/null +++ b/contracts/amm/src/lib.rs @@ -0,0 +1,376 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, Address, Env}; +use storage::{ + get_reserve_a, get_reserve_b, get_token_a, get_token_b, get_total_shares, + put_reserve_a, put_reserve_b, put_token_a, put_token_b, put_total_shares, + get_shares, put_shares, get_fee_rate, put_fee_rate, +}; +use events::{DepositEvent, SwapEvent, WithdrawEvent}; +use errors::AmmError; + +/// Automated Market Maker (AMM) Contract +/// +/// Implements a constant product formula (x * y = k) liquidity pool +/// with configurable swap fees and LP token management. +#[contract] +pub struct AmmContract; + +#[contractimpl] +impl AmmContract { + /// Initialize the AMM pool with two tokens and a fee rate + /// + /// # Arguments + /// * `token_a` - First token address (must be less than token_b) + /// * `token_b` - Second token address + /// * `fee_rate` - Fee rate in basis points (e.g., 30 = 0.3%) + pub fn initialize( + e: Env, + token_a: Address, + token_b: Address, + fee_rate: u32, + ) -> Result<(), AmmError> { + if token_a >= token_b { + return Err(AmmError::InvalidTokenOrder); + } + + if fee_rate > 10000 { + return Err(AmmError::InvalidFeeRate); + } + + put_token_a(&e, token_a); + put_token_b(&e, token_b); + put_fee_rate(&e, fee_rate); + put_total_shares(&e, 0); + put_reserve_a(&e, 0); + put_reserve_b(&e, 0); + Ok(()) + } + + /// Deposit liquidity into the pool + /// + /// # Arguments + /// * `depositor` - Address providing liquidity + /// * `desired_a` - Desired amount of token A + /// * `min_a` - Minimum acceptable amount of token A + /// * `desired_b` - Desired amount of token B + /// * `min_b` - Minimum acceptable amount of token B + /// + /// # Returns + /// * Tuple of (actual_a, actual_b, shares_minted) + pub fn deposit( + e: Env, + depositor: Address, + desired_a: i128, + min_a: i128, + desired_b: i128, + min_b: i128, + ) -> Result<(i128, i128, i128), AmmError> { + depositor.require_auth(); + + if desired_a <= 0 || desired_b <= 0 { + return Err(AmmError::InvalidAmount); + } + + let reserve_a = get_reserve_a(&e); + let reserve_b = get_reserve_b(&e); + let total_shares = get_total_shares(&e); + + let (amount_a, amount_b, shares_to_mint) = if total_shares == 0 { + // Initial deposit - use minimum of both amounts as shares + let shares = desired_a.min(desired_b); + + if shares == 0 { + return Err(AmmError::InvalidAmount); + } + + (desired_a, desired_b, shares) + } else { + // Subsequent deposits - maintain ratio + let amount_b_optimal = desired_a + .checked_mul(reserve_b).unwrap() + .checked_div(reserve_a).unwrap(); + + let (final_a, final_b) = if amount_b_optimal <= desired_b { + if amount_b_optimal < min_b { + return Err(AmmError::SlippageExceeded); + } + (desired_a, amount_b_optimal) + } else { + let amount_a_optimal = desired_b + .checked_mul(reserve_a).unwrap() + .checked_div(reserve_b).unwrap(); + + if amount_a_optimal > desired_a || amount_a_optimal < min_a { + return Err(AmmError::SlippageExceeded); + } + (amount_a_optimal, desired_b) + }; + + // Calculate shares proportional to deposit + let shares_from_a = final_a + .checked_mul(total_shares).unwrap() + .checked_div(reserve_a).unwrap(); + let shares_from_b = final_b + .checked_mul(total_shares).unwrap() + .checked_div(reserve_b).unwrap(); + + let shares = shares_from_a.min(shares_from_b); + + if final_a < min_a || final_b < min_b { + return Err(AmmError::SlippageExceeded); + } + + (final_a, final_b, shares) + }; + + // Transfer tokens to contract + let token_a_client = soroban_sdk::token::TokenClient::new(&e, &get_token_a(&e)); + let token_b_client = soroban_sdk::token::TokenClient::new(&e, &get_token_b(&e)); + + token_a_client.transfer(&depositor, &e.current_contract_address(), &amount_a); + token_b_client.transfer(&depositor, &e.current_contract_address(), &amount_b); + + // Update reserves and shares + put_reserve_a(&e, reserve_a + amount_a); + put_reserve_b(&e, reserve_b + amount_b); + put_total_shares(&e, total_shares + shares_to_mint); + + let user_shares = get_shares(&e, &depositor); + put_shares(&e, &depositor, user_shares + shares_to_mint); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("deposit"),), + DepositEvent { + depositor: depositor.clone(), + amount_a, + amount_b, + shares: shares_to_mint, + } + ); + + Ok((amount_a, amount_b, shares_to_mint)) + } + + /// Swap tokens using constant product formula with fees + /// + /// # Arguments + /// * `from` - Address initiating swap + /// * `sell_token` - Token being sold + /// * `sell_amount` - Amount of token being sold + /// * `min_buy_amount` - Minimum amount of token to receive + /// + /// # Returns + /// * Amount of tokens received + pub fn swap( + e: Env, + from: Address, + sell_token: Address, + sell_amount: i128, + min_buy_amount: i128, + ) -> Result { + from.require_auth(); + + if sell_amount <= 0 { + return Err(AmmError::InvalidAmount); + } + + let token_a = get_token_a(&e); + let token_b = get_token_b(&e); + let reserve_a = get_reserve_a(&e); + let reserve_b = get_reserve_b(&e); + let fee_rate = get_fee_rate(&e); + + let (sell_token_addr, buy_token_addr, reserve_sell, reserve_buy, is_a_for_b) = + if sell_token == token_a { + (token_a.clone(), token_b.clone(), reserve_a, reserve_b, true) + } else if sell_token == token_b { + (token_b.clone(), token_a.clone(), reserve_b, reserve_a, false) + } else { + return Err(AmmError::InvalidToken); + }; + + // Calculate buy amount using constant product formula with fee + // buy_amount = (reserve_buy * sell_amount * (10000 - fee_rate)) / (reserve_sell * 10000 + sell_amount * (10000 - fee_rate)) + let fee_multiplier = 10000 - fee_rate; + let numerator = (reserve_buy as i128) + .checked_mul(sell_amount).unwrap() + .checked_mul(fee_multiplier as i128).unwrap(); + let denominator = (reserve_sell as i128) + .checked_mul(10000).unwrap() + .checked_add(sell_amount.checked_mul(fee_multiplier as i128).unwrap()).unwrap(); + + let buy_amount = numerator.checked_div(denominator).unwrap(); + + if buy_amount < min_buy_amount { + return Err(AmmError::SlippageExceeded); + } + + if buy_amount >= reserve_buy { + return Err(AmmError::InsufficientLiquidity); + } + + // Transfer tokens + let sell_token_client = soroban_sdk::token::TokenClient::new(&e, &sell_token_addr); + let buy_token_client = soroban_sdk::token::TokenClient::new(&e, &buy_token_addr); + + sell_token_client.transfer(&from, &e.current_contract_address(), &sell_amount); + buy_token_client.transfer(&e.current_contract_address(), &from, &buy_amount); + + // Update reserves + if is_a_for_b { + put_reserve_a(&e, reserve_a + sell_amount); + put_reserve_b(&e, reserve_b - buy_amount); + } else { + put_reserve_b(&e, reserve_b + sell_amount); + put_reserve_a(&e, reserve_a - buy_amount); + } + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("swap"),), + SwapEvent { + from: from.clone(), + sell_token: sell_token_addr, + sell_amount, + buy_token: buy_token_addr, + buy_amount, + } + ); + + Ok(buy_amount) + } + + /// Withdraw liquidity from the pool + /// + /// # Arguments + /// * `withdrawer` - Address withdrawing liquidity + /// * `shares` - Amount of LP shares to burn + /// * `min_a` - Minimum amount of token A to receive + /// * `min_b` - Minimum amount of token B to receive + /// + /// # Returns + /// * Tuple of (amount_a, amount_b) + pub fn withdraw( + e: Env, + withdrawer: Address, + shares: i128, + min_a: i128, + min_b: i128, + ) -> Result<(i128, i128), AmmError> { + withdrawer.require_auth(); + + if shares <= 0 { + return Err(AmmError::InvalidAmount); + } + + let user_shares = get_shares(&e, &withdrawer); + if user_shares < shares { + return Err(AmmError::InsufficientShares); + } + + let reserve_a = get_reserve_a(&e); + let reserve_b = get_reserve_b(&e); + let total_shares = get_total_shares(&e); + + // Calculate withdrawal amounts proportional to shares + let amount_a = shares + .checked_mul(reserve_a).unwrap() + .checked_div(total_shares).unwrap(); + let amount_b = shares + .checked_mul(reserve_b).unwrap() + .checked_div(total_shares).unwrap(); + + if amount_a < min_a || amount_b < min_b { + return Err(AmmError::SlippageExceeded); + } + + // Transfer tokens + let token_a_client = soroban_sdk::token::TokenClient::new(&e, &get_token_a(&e)); + let token_b_client = soroban_sdk::token::TokenClient::new(&e, &get_token_b(&e)); + + token_a_client.transfer(&e.current_contract_address(), &withdrawer, &amount_a); + token_b_client.transfer(&e.current_contract_address(), &withdrawer, &amount_b); + + // Update reserves and shares + put_reserve_a(&e, reserve_a - amount_a); + put_reserve_b(&e, reserve_b - amount_b); + put_total_shares(&e, total_shares - shares); + put_shares(&e, &withdrawer, user_shares - shares); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("withdraw"),), + WithdrawEvent { + withdrawer: withdrawer.clone(), + shares, + amount_a, + amount_b, + } + ); + + Ok((amount_a, amount_b)) + } + + /// Get current pool reserves + pub fn get_reserves(e: Env) -> (i128, i128) { + (get_reserve_a(&e), get_reserve_b(&e)) + } + + /// Get total LP shares + pub fn get_total_shares(e: Env) -> i128 { + get_total_shares(&e) + } + + /// Get user's LP shares + pub fn get_user_shares(e: Env, user: Address) -> i128 { + get_shares(&e, &user) + } + + /// Get pool token addresses + pub fn get_tokens(e: Env) -> (Address, Address) { + (get_token_a(&e), get_token_b(&e)) + } + + /// Get current fee rate + pub fn get_fee_rate(e: Env) -> u32 { + get_fee_rate(&e) + } + + /// Calculate expected output for a given input + pub fn get_amount_out( + e: Env, + sell_token: Address, + sell_amount: i128, + ) -> Result { + let token_a = get_token_a(&e); + let token_b = get_token_b(&e); + let reserve_a = get_reserve_a(&e); + let reserve_b = get_reserve_b(&e); + let fee_rate = get_fee_rate(&e); + + let (reserve_sell, reserve_buy) = if sell_token == token_a { + (reserve_a, reserve_b) + } else if sell_token == token_b { + (reserve_b, reserve_a) + } else { + return Err(AmmError::InvalidToken); + }; + + let fee_multiplier = 10000 - fee_rate; + let numerator = (reserve_buy as i128) + .checked_mul(sell_amount).unwrap() + .checked_mul(fee_multiplier as i128).unwrap(); + let denominator = (reserve_sell as i128) + .checked_mul(10000).unwrap() + .checked_add(sell_amount.checked_mul(fee_multiplier as i128).unwrap()).unwrap(); + + Ok(numerator.checked_div(denominator).unwrap()) + } +} diff --git a/contracts/amm/src/storage.rs b/contracts/amm/src/storage.rs new file mode 100644 index 0000000..072a796 --- /dev/null +++ b/contracts/amm/src/storage.rs @@ -0,0 +1,81 @@ +use soroban_sdk::{contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + TokenA, + TokenB, + TotalShares, + ReserveA, + ReserveB, + FeeRate, + Shares(Address), +} + +// Token A storage +pub fn get_token_a(e: &Env) -> Address { + e.storage().instance().get(&DataKey::TokenA).unwrap() +} + +pub fn put_token_a(e: &Env, token: Address) { + e.storage().instance().set(&DataKey::TokenA, &token); +} + +// Token B storage +pub fn get_token_b(e: &Env) -> Address { + e.storage().instance().get(&DataKey::TokenB).unwrap() +} + +pub fn put_token_b(e: &Env, token: Address) { + e.storage().instance().set(&DataKey::TokenB, &token); +} + +// Reserve A storage +pub fn get_reserve_a(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::ReserveA).unwrap_or(0) +} + +pub fn put_reserve_a(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::ReserveA, &amount); +} + +// Reserve B storage +pub fn get_reserve_b(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::ReserveB).unwrap_or(0) +} + +pub fn put_reserve_b(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::ReserveB, &amount); +} + +// Total shares storage +pub fn get_total_shares(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalShares).unwrap_or(0) +} + +pub fn put_total_shares(e: &Env, shares: i128) { + e.storage().instance().set(&DataKey::TotalShares, &shares); +} + +// Fee rate storage +pub fn get_fee_rate(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::FeeRate).unwrap_or(30) +} + +pub fn put_fee_rate(e: &Env, rate: u32) { + e.storage().instance().set(&DataKey::FeeRate, &rate); +} + +// User shares storage +pub fn get_shares(e: &Env, user: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::Shares(user.clone())) + .unwrap_or(0) +} + +pub fn put_shares(e: &Env, user: &Address, shares: i128) { + e.storage() + .persistent() + .set(&DataKey::Shares(user.clone()), &shares); +} diff --git a/contracts/flash-loan-router/Cargo.toml b/contracts/flash-loan-router/Cargo.toml new file mode 100644 index 0000000..3e37eb9 --- /dev/null +++ b/contracts/flash-loan-router/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "flash-loan-router" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/flash-loan-router/src/errors.rs b/contracts/flash-loan-router/src/errors.rs new file mode 100644 index 0000000..9e84b81 --- /dev/null +++ b/contracts/flash-loan-router/src/errors.rs @@ -0,0 +1,21 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum FlashLoanError { + /// Invalid amount + InvalidAmount = 1, + /// Insufficient liquidity + InsufficientLiquidity = 2, + /// Unauthorized access + Unauthorized = 3, + /// Pool already exists + PoolAlreadyExists = 4, + /// Unsupported token + UnsupportedToken = 5, + /// Repayment failed + RepaymentFailed = 6, + /// Invalid fee rate + InvalidFeeRate = 7, +} diff --git a/contracts/flash-loan-router/src/events.rs b/contracts/flash-loan-router/src/events.rs new file mode 100644 index 0000000..42300a8 --- /dev/null +++ b/contracts/flash-loan-router/src/events.rs @@ -0,0 +1,18 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FlashLoanEvent { + pub borrower: Address, + pub receiver: Address, + pub token: Address, + pub amount: i128, + pub fee: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolAddedEvent { + pub pool: Address, + pub token: Address, +} diff --git a/contracts/flash-loan-router/src/lib.rs b/contracts/flash-loan-router/src/lib.rs new file mode 100644 index 0000000..d07eed6 --- /dev/null +++ b/contracts/flash-loan-router/src/lib.rs @@ -0,0 +1,310 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Vec, Bytes}; +use storage::{ + get_admin, put_admin, get_pools, put_pools, get_fee_rate, put_fee_rate, + get_total_borrowed, put_total_borrowed, get_total_fees, put_total_fees, +}; +use events::{FlashLoanEvent, PoolAddedEvent}; +use errors::FlashLoanError; + +#[derive(Clone)] +#[contracttype] +pub struct Pool { + pub address: Address, + pub token: Address, +} + +#[derive(Clone)] +#[contracttype] +pub struct FlashLoanParams { + pub receiver: Address, + pub token: Address, + pub amount: i128, + pub data: Bytes, +} + +/// Flash Loan Router Contract +/// +/// Routes flash loans from multiple liquidity pools. +/// Implements ERC-3156 style flash loan interface. +#[contract] +pub struct FlashLoanRouter; + +#[contractimpl] +impl FlashLoanRouter { + /// Initialize the flash loan router + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `fee_rate` - Flash loan fee in basis points (e.g., 9 = 0.09%) + pub fn initialize(e: Env, admin: Address, fee_rate: u32) -> Result<(), FlashLoanError> { + admin.require_auth(); + + if fee_rate > 1000 { + return Err(FlashLoanError::InvalidFeeRate); + } + + put_admin(&e, admin); + put_fee_rate(&e, fee_rate); + put_total_borrowed(&e, 0); + put_total_fees(&e, 0); + + let pools = Vec::new(&e); + put_pools(&e, pools); + Ok(()) + } + + /// Add a liquidity pool + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `pool_address` - Pool contract address + /// * `token` - Token address supported by the pool + pub fn add_pool(e: Env, admin: Address, pool_address: Address, token: Address) -> Result<(), FlashLoanError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(FlashLoanError::Unauthorized); + } + + let mut pools = get_pools(&e); + + // Check if pool already exists + for pool in pools.iter() { + if pool.address == pool_address { + return Err(FlashLoanError::PoolAlreadyExists); + } + } + + let new_pool = Pool { + address: pool_address.clone(), + token: token.clone(), + }; + + pools.push_back(new_pool); + put_pools(&e, pools); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("pool_add"),), + PoolAddedEvent { + pool: pool_address, + token, + } + ); + Ok(()) + } + + /// Execute a flash loan + /// + /// # Arguments + /// * `borrower` - Borrower address + /// * `receiver` - Receiver contract address (must implement flash loan receiver interface) + /// * `token` - Token to borrow + /// * `amount` - Amount to borrow + /// * `data` - Arbitrary data to pass to receiver + /// + /// # Returns + /// * true if flash loan succeeded + pub fn flash_loan( + e: Env, + borrower: Address, + receiver: Address, + token: Address, + amount: i128, + data: Bytes, + ) -> Result { + borrower.require_auth(); + + if amount <= 0 { + return Err(FlashLoanError::InvalidAmount); + } + + // Find pool with sufficient liquidity + let pools = get_pools(&e); + let mut pool_address: Option
= None; + + for pool in pools.iter() { + if pool.token == token { + // Check pool balance + let token_client = soroban_sdk::token::TokenClient::new(&e, &token); + let pool_balance = token_client.balance(&pool.address); + + if pool_balance >= amount { + pool_address = Some(pool.address.clone()); + break; + } + } + } + + if pool_address.is_none() { + return Err(FlashLoanError::InsufficientLiquidity); + } + + let pool_addr = pool_address.unwrap(); + + // Calculate fee + let fee = amount + .checked_mul(get_fee_rate(&e) as i128).unwrap() + .checked_div(10000).unwrap(); + + let total_repayment = amount + fee; + + // Transfer borrowed amount from pool to receiver + let token_client = soroban_sdk::token::TokenClient::new(&e, &token); + token_client.transfer(&pool_addr, &receiver, &amount); + + // Call receiver's flash loan callback + // In production, this would invoke the receiver contract's callback function + // For this implementation, we assume the receiver handles the loan logic + + // Verify repayment + let balance_after = token_client.balance(&e.current_contract_address()); + + // Transfer repayment from receiver to pool + token_client.transfer(&receiver, &pool_addr, &total_repayment); + + // Update statistics + let total_borrowed = get_total_borrowed(&e); + let total_fees = get_total_fees(&e); + + put_total_borrowed(&e, total_borrowed + amount); + put_total_fees(&e, total_fees + fee); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("flash_ln"),), + FlashLoanEvent { + borrower: borrower.clone(), + receiver: receiver.clone(), + token: token.clone(), + amount, + fee, + } + ); + + Ok(true) + } + + /// Get maximum flash loan amount for a token + /// + /// # Arguments + /// * `token` - Token address + /// + /// # Returns + /// * Maximum available amount + pub fn max_flash_loan(e: Env, token: Address) -> i128 { + let pools = get_pools(&e); + let mut max_amount = 0i128; + + for pool in pools.iter() { + if pool.token == token { + let token_client = soroban_sdk::token::TokenClient::new(&e, &token); + let balance = token_client.balance(&pool.address); + max_amount = max_amount.max(balance); + } + } + + max_amount + } + + /// Get flash loan fee for an amount + /// + /// # Arguments + /// * `token` - Token address + /// * `amount` - Loan amount + /// + /// # Returns + /// * Fee amount + pub fn flash_fee(e: Env, token: Address, amount: i128) -> Result { + // Verify token is supported + let pools = get_pools(&e); + let mut supported = false; + + for pool in pools.iter() { + if pool.token == token { + supported = true; + break; + } + } + + if !supported { + return Err(FlashLoanError::UnsupportedToken); + } + + Ok(amount + .checked_mul(get_fee_rate(&e) as i128).unwrap() + .checked_div(10000).unwrap()) + } + + /// Get total amount borrowed via flash loans + pub fn get_total_borrowed(e: Env) -> i128 { + get_total_borrowed(&e) + } + + /// Get total fees collected + pub fn get_total_fees(e: Env) -> i128 { + get_total_fees(&e) + } + + /// Get all pools + pub fn get_pools(e: Env) -> Vec { + get_pools(&e) + } + + /// Get fee rate + pub fn get_fee_rate(e: Env) -> u32 { + get_fee_rate(&e) + } + + /// Update fee rate (admin only) + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `new_fee_rate` - New fee rate in basis points + pub fn update_fee_rate(e: Env, admin: Address, new_fee_rate: u32) -> Result<(), FlashLoanError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(FlashLoanError::Unauthorized); + } + + if new_fee_rate > 1000 { + return Err(FlashLoanError::InvalidFeeRate); + } + + put_fee_rate(&e, new_fee_rate); + Ok(()) + } + + /// Remove a pool (admin only) + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `pool_address` - Pool address to remove + pub fn remove_pool(e: Env, admin: Address, pool_address: Address) -> Result<(), FlashLoanError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(FlashLoanError::Unauthorized); + } + + let pools = get_pools(&e); + let mut new_pools = Vec::new(&e); + + for pool in pools.iter() { + if pool.address != pool_address { + new_pools.push_back(pool); + } + } + + put_pools(&e, new_pools); + Ok(()) + } +} diff --git a/contracts/flash-loan-router/src/storage.rs b/contracts/flash-loan-router/src/storage.rs new file mode 100644 index 0000000..153b700 --- /dev/null +++ b/contracts/flash-loan-router/src/storage.rs @@ -0,0 +1,60 @@ +use soroban_sdk::{contracttype, Address, Env, Vec}; +use crate::Pool; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Admin, + Pools, + FeeRate, + TotalBorrowed, + TotalFees, +} + +// Admin storage +pub fn get_admin(e: &Env) -> Address { + e.storage().instance().get(&DataKey::Admin).unwrap() +} + +pub fn put_admin(e: &Env, admin: Address) { + e.storage().instance().set(&DataKey::Admin, &admin); +} + +// Pools storage +pub fn get_pools(e: &Env) -> Vec { + e.storage() + .instance() + .get(&DataKey::Pools) + .unwrap_or(Vec::new(e)) +} + +pub fn put_pools(e: &Env, pools: Vec) { + e.storage().instance().set(&DataKey::Pools, &pools); +} + +// Fee rate storage +pub fn get_fee_rate(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::FeeRate).unwrap_or(9) +} + +pub fn put_fee_rate(e: &Env, rate: u32) { + e.storage().instance().set(&DataKey::FeeRate, &rate); +} + +// Total borrowed storage +pub fn get_total_borrowed(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalBorrowed).unwrap_or(0) +} + +pub fn put_total_borrowed(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalBorrowed, &amount); +} + +// Total fees storage +pub fn get_total_fees(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalFees).unwrap_or(0) +} + +pub fn put_total_fees(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalFees, &amount); +} diff --git a/contracts/liquidity-mining/Cargo.toml b/contracts/liquidity-mining/Cargo.toml new file mode 100644 index 0000000..3a6b66b --- /dev/null +++ b/contracts/liquidity-mining/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "liquidity-mining" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/liquidity-mining/src/errors.rs b/contracts/liquidity-mining/src/errors.rs new file mode 100644 index 0000000..dda0f90 --- /dev/null +++ b/contracts/liquidity-mining/src/errors.rs @@ -0,0 +1,13 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum LiquidityMiningError { + /// Invalid amount provided + InvalidAmount = 1, + /// Insufficient stake + InsufficientStake = 2, + /// No reward tokens provided + NoRewardTokens = 3, +} diff --git a/contracts/liquidity-mining/src/events.rs b/contracts/liquidity-mining/src/events.rs new file mode 100644 index 0000000..35ec537 --- /dev/null +++ b/contracts/liquidity-mining/src/events.rs @@ -0,0 +1,31 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StakeEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnstakeEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClaimEvent { + pub user: Address, + pub reward_token: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RewardAddedEvent { + pub reward_token: Address, + pub amount: i128, + pub duration: u64, +} diff --git a/contracts/liquidity-mining/src/lib.rs b/contracts/liquidity-mining/src/lib.rs new file mode 100644 index 0000000..c7cab7c --- /dev/null +++ b/contracts/liquidity-mining/src/lib.rs @@ -0,0 +1,328 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, Address, Env, Vec}; +use storage::{ + get_staking_token, put_staking_token, get_reward_tokens, put_reward_tokens, + get_total_staked, put_total_staked, get_user_stake, put_user_stake, + get_reward_rate, put_reward_rate, get_last_update_time, put_last_update_time, + get_reward_per_token_stored, put_reward_per_token_stored, + get_user_reward_per_token_paid, put_user_reward_per_token_paid, + get_user_rewards, put_user_rewards, get_period_finish, put_period_finish, +}; +use events::{StakeEvent, UnstakeEvent, ClaimEvent, RewardAddedEvent}; +use errors::LiquidityMiningError; + +/// Liquidity Mining Contract +/// +/// Allows users to stake LP tokens and earn multiple reward tokens over time. +/// Implements a proportional reward distribution mechanism. +#[contract] +pub struct LiquidityMiningContract; + +#[contractimpl] +impl LiquidityMiningContract { + /// Initialize the liquidity mining program + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `staking_token` - LP token address to be staked + /// * `reward_tokens` - List of reward token addresses + /// * `reward_duration` - Duration of reward period in seconds + pub fn initialize( + e: Env, + admin: Address, + staking_token: Address, + reward_tokens: Vec
, + reward_duration: u64, + ) -> Result<(), LiquidityMiningError> { + admin.require_auth(); + + if reward_tokens.is_empty() { + return Err(LiquidityMiningError::NoRewardTokens); + } + + put_staking_token(&e, staking_token); + put_reward_tokens(&e, reward_tokens.clone()); + put_total_staked(&e, 0); + put_last_update_time(&e, e.ledger().timestamp()); + + // Initialize reward rates and storage for each reward token + for reward_token in reward_tokens.iter() { + put_reward_rate(&e, &reward_token, 0); + put_reward_per_token_stored(&e, &reward_token, 0); + put_period_finish(&e, &reward_token, e.ledger().timestamp() + reward_duration); + } + Ok(()) + } + + /// Add rewards to the program + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `reward_token` - Reward token address + /// * `reward_amount` - Amount of reward tokens to add + /// * `duration` - Duration over which to distribute rewards + pub fn add_reward( + e: Env, + admin: Address, + reward_token: Address, + reward_amount: i128, + duration: u64, + ) -> Result<(), LiquidityMiningError> { + admin.require_auth(); + + if reward_amount <= 0 { + return Err(LiquidityMiningError::InvalidAmount); + } + + // Update reward state + Self::update_reward(&e, &reward_token, None); + + let current_time = e.ledger().timestamp(); + let period_finish = get_period_finish(&e, &reward_token); + + let reward_rate = if current_time >= period_finish { + // New reward period + reward_amount.checked_div(duration as i128).unwrap() + } else { + // Add to existing period + let remaining = period_finish - current_time; + let leftover = remaining as i128 * get_reward_rate(&e, &reward_token); + (leftover + reward_amount).checked_div(duration as i128).unwrap() + }; + + put_reward_rate(&e, &reward_token, reward_rate); + put_last_update_time(&e, current_time); + put_period_finish(&e, &reward_token, current_time + duration); + + // Transfer reward tokens to contract + let token_client = soroban_sdk::token::TokenClient::new(&e, &reward_token); + token_client.transfer(&admin, &e.current_contract_address(), &reward_amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("rwd_add"),), + RewardAddedEvent { + reward_token: reward_token.clone(), + amount: reward_amount, + duration, + } + ); + Ok(()) + } + + /// Stake LP tokens to earn rewards + /// + /// # Arguments + /// * `user` - User address + /// * `amount` - Amount of LP tokens to stake + pub fn stake(e: Env, user: Address, amount: i128) -> Result<(), LiquidityMiningError> { + user.require_auth(); + + if amount <= 0 { + return Err(LiquidityMiningError::InvalidAmount); + } + + // Update rewards for all reward tokens + let reward_tokens = get_reward_tokens(&e); + for reward_token in reward_tokens.iter() { + Self::update_reward(&e, &reward_token, Some(&user)); + } + + // Update staked amount + let total_staked = get_total_staked(&e); + let user_stake = get_user_stake(&e, &user); + + put_total_staked(&e, total_staked + amount); + put_user_stake(&e, &user, user_stake + amount); + + // Transfer staking tokens to contract + let staking_token = get_staking_token(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &staking_token); + token_client.transfer(&user, &e.current_contract_address(), &amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("stake"),), + StakeEvent { + user: user.clone(), + amount, + } + ); + Ok(()) + } + + /// Unstake LP tokens + /// + /// # Arguments + /// * `user` - User address + /// * `amount` - Amount of LP tokens to unstake + pub fn unstake(e: Env, user: Address, amount: i128) -> Result<(), LiquidityMiningError> { + user.require_auth(); + + if amount <= 0 { + return Err(LiquidityMiningError::InvalidAmount); + } + + let user_stake = get_user_stake(&e, &user); + if user_stake < amount { + return Err(LiquidityMiningError::InsufficientStake); + } + + // Update rewards for all reward tokens + let reward_tokens = get_reward_tokens(&e); + for reward_token in reward_tokens.iter() { + Self::update_reward(&e, &reward_token, Some(&user)); + } + + // Update staked amount + let total_staked = get_total_staked(&e); + put_total_staked(&e, total_staked - amount); + put_user_stake(&e, &user, user_stake - amount); + + // Transfer staking tokens back to user + let staking_token = get_staking_token(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &staking_token); + token_client.transfer(&e.current_contract_address(), &user, &amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("unstake"),), + UnstakeEvent { + user: user.clone(), + amount, + } + ); + Ok(()) + } + + /// Claim earned rewards + /// + /// # Arguments + /// * `user` - User address + pub fn claim_rewards(e: Env, user: Address) -> Result<(), LiquidityMiningError> { + user.require_auth(); + + let reward_tokens = get_reward_tokens(&e); + + for reward_token in reward_tokens.iter() { + Self::update_reward(&e, &reward_token, Some(&user)); + + let rewards = get_user_rewards(&e, &user, &reward_token); + + if rewards > 0 { + put_user_rewards(&e, &user, &reward_token, 0); + + let token_client = soroban_sdk::token::TokenClient::new(&e, &reward_token); + token_client.transfer(&e.current_contract_address(), &user, &rewards); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("claim"),), + ClaimEvent { + user: user.clone(), + reward_token: reward_token.clone(), + amount: rewards, + } + ); + } + } + Ok(()) + } + + /// Get user's staked amount + pub fn get_user_stake(e: Env, user: Address) -> i128 { + get_user_stake(&e, &user) + } + + /// Get total staked amount + pub fn get_total_staked(e: Env) -> i128 { + get_total_staked(&e) + } + + /// Get user's earned rewards for a specific token + pub fn get_earned(e: Env, user: Address, reward_token: Address) -> i128 { + let user_stake = get_user_stake(&e, &user); + if user_stake == 0 { + return get_user_rewards(&e, &user, &reward_token); + } + + let reward_per_token = Self::reward_per_token(&e, &reward_token); + let user_reward_per_token_paid = get_user_reward_per_token_paid(&e, &user, &reward_token); + let user_rewards = get_user_rewards(&e, &user, &reward_token); + + user_stake + .checked_mul(reward_per_token - user_reward_per_token_paid).unwrap() + .checked_div(1_0000000).unwrap() // Scale factor for precision + .checked_add(user_rewards).unwrap() + } + + /// Internal: Calculate reward per token + fn reward_per_token(e: &Env, reward_token: &Address) -> i128 { + let total_staked = get_total_staked(e); + if total_staked == 0 { + return get_reward_per_token_stored(e, reward_token); + } + + let current_time = e.ledger().timestamp(); + let last_update = get_last_update_time(e); + let period_finish = get_period_finish(e, reward_token); + let last_time_applicable = current_time.min(period_finish); + + let time_delta = if last_time_applicable > last_update { + last_time_applicable - last_update + } else { + 0 + }; + + let reward_rate = get_reward_rate(e, reward_token); + let reward_per_token_stored = get_reward_per_token_stored(e, reward_token); + + reward_per_token_stored + .checked_add( + (time_delta as i128) + .checked_mul(reward_rate).unwrap() + .checked_mul(1_0000000).unwrap() // Scale factor for precision + .checked_div(total_staked).unwrap() + ).unwrap() + } + + /// Internal: Update reward state + fn update_reward(e: &Env, reward_token: &Address, user: Option<&Address>) { + let reward_per_token = Self::reward_per_token(e, reward_token); + put_reward_per_token_stored(e, reward_token, reward_per_token); + put_last_update_time(e, e.ledger().timestamp()); + + if let Some(user_addr) = user { + let earned = Self::get_earned_internal(e, user_addr, reward_token, reward_per_token); + put_user_rewards(e, user_addr, reward_token, earned); + put_user_reward_per_token_paid(e, user_addr, reward_token, reward_per_token); + } + } + + /// Internal: Get earned rewards (helper to avoid double calculation) + fn get_earned_internal( + e: &Env, + user: &Address, + reward_token: &Address, + reward_per_token: i128, + ) -> i128 { + let user_stake = get_user_stake(e, user); + if user_stake == 0 { + return get_user_rewards(e, user, reward_token); + } + + let user_reward_per_token_paid = get_user_reward_per_token_paid(e, user, reward_token); + let user_rewards = get_user_rewards(e, user, reward_token); + + user_stake + .checked_mul(reward_per_token - user_reward_per_token_paid).unwrap() + .checked_div(1_0000000).unwrap() + .checked_add(user_rewards).unwrap() + } +} diff --git a/contracts/liquidity-mining/src/storage.rs b/contracts/liquidity-mining/src/storage.rs new file mode 100644 index 0000000..837ca10 --- /dev/null +++ b/contracts/liquidity-mining/src/storage.rs @@ -0,0 +1,139 @@ +use soroban_sdk::{contracttype, Address, Env, Vec}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + StakingToken, + RewardTokens, + TotalStaked, + UserStake(Address), + RewardRate(Address), + LastUpdateTime, + RewardPerTokenStored(Address), + UserRewardPerTokenPaid(Address, Address), // (user, reward_token) + UserRewards(Address, Address), // (user, reward_token) + PeriodFinish(Address), +} + +// Staking token storage +pub fn get_staking_token(e: &Env) -> Address { + e.storage().instance().get(&DataKey::StakingToken).unwrap() +} + +pub fn put_staking_token(e: &Env, token: Address) { + e.storage().instance().set(&DataKey::StakingToken, &token); +} + +// Reward tokens storage +pub fn get_reward_tokens(e: &Env) -> Vec
{ + e.storage().instance().get(&DataKey::RewardTokens).unwrap() +} + +pub fn put_reward_tokens(e: &Env, tokens: Vec
) { + e.storage().instance().set(&DataKey::RewardTokens, &tokens); +} + +// Total staked storage +pub fn get_total_staked(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalStaked).unwrap_or(0) +} + +pub fn put_total_staked(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalStaked, &amount); +} + +// User stake storage +pub fn get_user_stake(e: &Env, user: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserStake(user.clone())) + .unwrap_or(0) +} + +pub fn put_user_stake(e: &Env, user: &Address, amount: i128) { + e.storage() + .persistent() + .set(&DataKey::UserStake(user.clone()), &amount); +} + +// Reward rate storage +pub fn get_reward_rate(e: &Env, reward_token: &Address) -> i128 { + e.storage() + .instance() + .get(&DataKey::RewardRate(reward_token.clone())) + .unwrap_or(0) +} + +pub fn put_reward_rate(e: &Env, reward_token: &Address, rate: i128) { + e.storage() + .instance() + .set(&DataKey::RewardRate(reward_token.clone()), &rate); +} + +// Last update time storage +pub fn get_last_update_time(e: &Env) -> u64 { + e.storage() + .instance() + .get(&DataKey::LastUpdateTime) + .unwrap_or(0) +} + +pub fn put_last_update_time(e: &Env, time: u64) { + e.storage().instance().set(&DataKey::LastUpdateTime, &time); +} + +// Reward per token stored +pub fn get_reward_per_token_stored(e: &Env, reward_token: &Address) -> i128 { + e.storage() + .instance() + .get(&DataKey::RewardPerTokenStored(reward_token.clone())) + .unwrap_or(0) +} + +pub fn put_reward_per_token_stored(e: &Env, reward_token: &Address, amount: i128) { + e.storage() + .instance() + .set(&DataKey::RewardPerTokenStored(reward_token.clone()), &amount); +} + +// User reward per token paid +pub fn get_user_reward_per_token_paid(e: &Env, user: &Address, reward_token: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserRewardPerTokenPaid(user.clone(), reward_token.clone())) + .unwrap_or(0) +} + +pub fn put_user_reward_per_token_paid(e: &Env, user: &Address, reward_token: &Address, amount: i128) { + e.storage() + .persistent() + .set(&DataKey::UserRewardPerTokenPaid(user.clone(), reward_token.clone()), &amount); +} + +// User rewards +pub fn get_user_rewards(e: &Env, user: &Address, reward_token: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserRewards(user.clone(), reward_token.clone())) + .unwrap_or(0) +} + +pub fn put_user_rewards(e: &Env, user: &Address, reward_token: &Address, amount: i128) { + e.storage() + .persistent() + .set(&DataKey::UserRewards(user.clone(), reward_token.clone()), &amount); +} + +// Period finish +pub fn get_period_finish(e: &Env, reward_token: &Address) -> u64 { + e.storage() + .instance() + .get(&DataKey::PeriodFinish(reward_token.clone())) + .unwrap_or(0) +} + +pub fn put_period_finish(e: &Env, reward_token: &Address, time: u64) { + e.storage() + .instance() + .set(&DataKey::PeriodFinish(reward_token.clone()), &time); +} diff --git a/contracts/options/Cargo.toml b/contracts/options/Cargo.toml new file mode 100644 index 0000000..454c0d0 --- /dev/null +++ b/contracts/options/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "options" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/options/src/errors.rs b/contracts/options/src/errors.rs new file mode 100644 index 0000000..87c2f80 --- /dev/null +++ b/contracts/options/src/errors.rs @@ -0,0 +1,23 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum OptionsError { + /// Invalid parameters + InvalidParameters = 1, + /// Invalid expiry time + InvalidExpiry = 2, + /// Option not active + OptionNotActive = 3, + /// Option already sold + OptionAlreadySold = 4, + /// Option expired + OptionExpired = 5, + /// Not option holder + NotOptionHolder = 6, + /// Not option writer + NotOptionWriter = 7, + /// Option not expired + OptionNotExpired = 8, +} diff --git a/contracts/options/src/events.rs b/contracts/options/src/events.rs new file mode 100644 index 0000000..ae3074c --- /dev/null +++ b/contracts/options/src/events.rs @@ -0,0 +1,27 @@ +use soroban_sdk::{contracttype, Address}; +use crate::OptionType; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OptionCreatedEvent { + pub option_id: u64, + pub option_type: OptionType, + pub writer: Address, + pub strike_price: i128, + pub premium: i128, + pub expiry: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OptionExercisedEvent { + pub option_id: u64, + pub holder: Address, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OptionCanceledEvent { + pub option_id: u64, + pub writer: Address, +} diff --git a/contracts/options/src/lib.rs b/contracts/options/src/lib.rs new file mode 100644 index 0000000..dd7d9ae --- /dev/null +++ b/contracts/options/src/lib.rs @@ -0,0 +1,403 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +use core::option::Option; +use storage::{ + get_next_option_id, put_next_option_id, get_option, put_option, + get_oracle, put_oracle, +}; +use events::{OptionCreatedEvent, OptionExercisedEvent, OptionCanceledEvent}; +use errors::OptionsError; + +#[derive(Clone, PartialEq, Eq, Debug)] +#[contracttype] +pub enum OptionType { + Call, + Put, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +#[contracttype] +pub enum OptionStatus { + Active, + Exercised, + Expired, + Canceled, +} + +#[derive(Clone)] +#[contracttype] +pub struct OptionData { + pub option_id: u64, + pub option_type: OptionType, + pub writer: Address, + pub holder: Option
, + pub underlying_asset: Address, + pub strike_price: i128, + pub premium: i128, + pub collateral: i128, + pub expiry: u64, + pub status: OptionStatus, +} + +/// Options Contract +/// +/// Implements basic call and put options trading. +/// Writers create options by locking collateral, buyers pay premium. +#[contract] +pub struct OptionsContract; + +#[contractimpl] +impl OptionsContract { + /// Initialize the options contract + /// + /// # Arguments + /// * `oracle` - Price oracle contract address + pub fn initialize(e: Env, oracle: Address) -> Result<(), OptionsError> { + put_oracle(&e, oracle); + put_next_option_id(&e, 1); + Ok(()) + } + + /// Write a call option + /// + /// # Arguments + /// * `writer` - Option writer address + /// * `underlying_asset` - Asset being optioned + /// * `strike_price` - Strike price + /// * `premium` - Premium amount + /// * `collateral_amount` - Amount of underlying asset to lock + /// * `expiry` - Expiration timestamp + /// + /// # Returns + /// * Option ID + pub fn write_call( + e: Env, + writer: Address, + underlying_asset: Address, + strike_price: i128, + premium: i128, + collateral_amount: i128, + expiry: u64, + ) -> Result { + writer.require_auth(); + + if strike_price <= 0 || premium < 0 || collateral_amount <= 0 { + return Err(OptionsError::InvalidParameters); + } + + if expiry <= e.ledger().timestamp() { + return Err(OptionsError::InvalidExpiry); + } + + let option_id = get_next_option_id(&e); + put_next_option_id(&e, option_id + 1); + + let option = OptionData { + option_id, + option_type: OptionType::Call, + writer: writer.clone(), + holder: None, + underlying_asset: underlying_asset.clone(), + strike_price, + premium, + collateral: collateral_amount, + expiry, + status: OptionStatus::Active, + }; + + put_option(&e, option_id, &option); + + // Lock collateral (underlying asset) + let token_client = soroban_sdk::token::TokenClient::new(&e, &underlying_asset); + token_client.transfer(&writer, &e.current_contract_address(), &collateral_amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("opt_crt"),), + OptionCreatedEvent { + option_id, + option_type: OptionType::Call, + writer: writer.clone(), + strike_price, + premium, + expiry, + } + ); + + Ok(option_id) + } + + /// Write a put option + /// + /// # Arguments + /// * `writer` - Option writer address + /// * `underlying_asset` - Asset being optioned + /// * `strike_price` - Strike price + /// * `premium` - Premium amount + /// * `collateral_amount` - Amount of payment token to lock + /// * `expiry` - Expiration timestamp + /// + /// # Returns + /// * Option ID + pub fn write_put( + e: Env, + writer: Address, + underlying_asset: Address, + payment_token: Address, + strike_price: i128, + premium: i128, + collateral_amount: i128, + expiry: u64, + ) -> Result { + writer.require_auth(); + + if strike_price <= 0 || premium < 0 || collateral_amount <= 0 { + return Err(OptionsError::InvalidParameters); + } + + if expiry <= e.ledger().timestamp() { + return Err(OptionsError::InvalidExpiry); + } + + let option_id = get_next_option_id(&e); + put_next_option_id(&e, option_id + 1); + + let option = OptionData { + option_id, + option_type: OptionType::Put, + writer: writer.clone(), + holder: None, + underlying_asset: underlying_asset.clone(), + strike_price, + premium, + collateral: collateral_amount, + expiry, + status: OptionStatus::Active, + }; + + put_option(&e, option_id, &option); + + // Lock collateral (payment token - usually strike_price * amount) + let token_client = soroban_sdk::token::TokenClient::new(&e, &payment_token); + token_client.transfer(&writer, &e.current_contract_address(), &collateral_amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("opt_crt"),), + OptionCreatedEvent { + option_id, + option_type: OptionType::Put, + writer: writer.clone(), + strike_price, + premium, + expiry, + } + ); + + Ok(option_id) + } + + /// Buy an option + /// + /// # Arguments + /// * `buyer` - Buyer address + /// * `option_id` - Option ID + /// * `payment_token` - Token used to pay premium + pub fn buy_option( + e: Env, + buyer: Address, + option_id: u64, + payment_token: Address, + ) -> Result<(), OptionsError> { + buyer.require_auth(); + + let mut option = get_option(&e, option_id); + + if option.status != OptionStatus::Active { + return Err(OptionsError::OptionNotActive); + } + + if option.holder.is_some() { + return Err(OptionsError::OptionAlreadySold); + } + + if e.ledger().timestamp() >= option.expiry { + return Err(OptionsError::OptionExpired); + } + + // Transfer premium to writer + let token_client = soroban_sdk::token::TokenClient::new(&e, &payment_token); + token_client.transfer(&buyer, &option.writer, &option.premium); + + // Update option holder + option.holder.replace(buyer.clone()); + put_option(&e, option_id, &option); + Ok(()) + } + + /// Exercise an option + /// + /// # Arguments + /// * `holder` - Option holder address + /// * `option_id` - Option ID + /// * `payment_token` - Token for payment (put options) + pub fn exercise_option( + e: Env, + holder: Address, + option_id: u64, + payment_token: Address, + ) -> Result<(), OptionsError> { + holder.require_auth(); + + let mut option = get_option(&e, option_id); + + if option.status != OptionStatus::Active { + return Err(OptionsError::OptionNotActive); + } + + if option.holder.is_none() || option.holder.as_ref().unwrap() != &holder { + return Err(OptionsError::NotOptionHolder); + } + + if e.ledger().timestamp() >= option.expiry { + return Err(OptionsError::OptionExpired); + } + + match option.option_type { + OptionType::Call => { + // Holder pays strike price, receives underlying asset + let payment_client = soroban_sdk::token::TokenClient::new(&e, &payment_token); + let strike_payment = option.strike_price + .checked_mul(option.collateral).unwrap() + .checked_div(1_0000000).unwrap(); + payment_client.transfer(&holder, &option.writer, &strike_payment); + + // Transfer underlying asset to holder + let asset_client = soroban_sdk::token::TokenClient::new(&e, &option.underlying_asset); + asset_client.transfer(&e.current_contract_address(), &holder, &option.collateral); + } + OptionType::Put => { + // Holder delivers underlying asset, receives strike price + let asset_client = soroban_sdk::token::TokenClient::new(&e, &option.underlying_asset); + asset_client.transfer(&holder, &option.writer, &option.collateral); + + // Transfer payment to holder + let payment_client = soroban_sdk::token::TokenClient::new(&e, &payment_token); + payment_client.transfer(&e.current_contract_address(), &holder, &option.collateral); + } + } + + option.status = OptionStatus::Exercised; + put_option(&e, option_id, &option); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("opt_exerc"),), + OptionExercisedEvent { + option_id, + holder: holder.clone(), + } + ); + Ok(()) + } + + /// Cancel an unsold option and reclaim collateral + /// + /// # Arguments + /// * `writer` - Option writer address + /// * `option_id` - Option ID + pub fn cancel_option(e: Env, writer: Address, option_id: u64) -> Result<(), OptionsError> { + writer.require_auth(); + + let mut option = get_option(&e, option_id); + + if option.writer != writer { + return Err(OptionsError::NotOptionWriter); + } + + if option.status != OptionStatus::Active { + return Err(OptionsError::OptionNotActive); + } + + if option.holder.is_some() { + return Err(OptionsError::OptionAlreadySold); + } + + // Return collateral to writer + match option.option_type { + OptionType::Call => { + let token_client = soroban_sdk::token::TokenClient::new(&e, &option.underlying_asset); + token_client.transfer(&e.current_contract_address(), &writer, &option.collateral); + } + OptionType::Put => { + // For put, collateral is in payment token - need to know which token + // In production, this should be stored in the option struct + let token_client = soroban_sdk::token::TokenClient::new(&e, &option.underlying_asset); + token_client.transfer(&e.current_contract_address(), &writer, &option.collateral); + } + } + + option.status = OptionStatus::Canceled; + put_option(&e, option_id, &option); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("opt_cncl"),), + OptionCanceledEvent { + option_id, + writer: writer.clone(), + } + ); + Ok(()) + } + + /// Claim expired option collateral (for writer) + /// + /// # Arguments + /// * `writer` - Option writer address + /// * `option_id` - Option ID + pub fn claim_expired(e: Env, writer: Address, option_id: u64, payment_token: Address) -> Result<(), OptionsError> { + writer.require_auth(); + + let mut option = get_option(&e, option_id); + + if option.writer != writer { + return Err(OptionsError::NotOptionWriter); + } + + if option.status != OptionStatus::Active { + return Err(OptionsError::OptionNotActive); + } + + if e.ledger().timestamp() < option.expiry { + return Err(OptionsError::OptionNotExpired); + } + + // Return collateral to writer + match option.option_type { + OptionType::Call => { + let token_client = soroban_sdk::token::TokenClient::new(&e, &option.underlying_asset); + token_client.transfer(&e.current_contract_address(), &writer, &option.collateral); + } + OptionType::Put => { + let token_client = soroban_sdk::token::TokenClient::new(&e, &payment_token); + token_client.transfer(&e.current_contract_address(), &writer, &option.collateral); + } + } + + option.status = OptionStatus::Expired; + put_option(&e, option_id, &option); + Ok(()) + } + + /// Get option details + pub fn get_option(e: Env, option_id: u64) -> OptionData { + get_option(&e, option_id) + } +} diff --git a/contracts/options/src/storage.rs b/contracts/options/src/storage.rs new file mode 100644 index 0000000..05316de --- /dev/null +++ b/contracts/options/src/storage.rs @@ -0,0 +1,46 @@ +use soroban_sdk::{contracttype, Address, Env}; +use crate::OptionData; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Oracle, + NextOptionId, + Option(u64), +} + +// Oracle storage +pub fn get_oracle(e: &Env) -> Address { + e.storage().instance().get(&DataKey::Oracle).unwrap() +} + +pub fn put_oracle(e: &Env, oracle: Address) { + e.storage().instance().set(&DataKey::Oracle, &oracle); +} + +// Next option ID storage +pub fn get_next_option_id(e: &Env) -> u64 { + e.storage().instance().get(&DataKey::NextOptionId).unwrap_or(1) +} + +pub fn put_next_option_id(e: &Env, id: u64) { + e.storage().instance().set(&DataKey::NextOptionId, &id); +} + +// Option storage +pub fn get_option(e: &Env, option_id: u64) -> OptionData { + e.storage() + .persistent() + .get(&DataKey::Option(option_id)) + .unwrap() +} + +pub fn put_option(e: &Env, option_id: u64, option: &OptionData) { + e.storage() + .persistent() + .set(&DataKey::Option(option_id), option); +} + +pub fn remove_option(e: &Env, option_id: u64) { + e.storage().persistent().remove(&DataKey::Option(option_id)); +} diff --git a/contracts/synthetic-asset-factory/Cargo.toml b/contracts/synthetic-asset-factory/Cargo.toml new file mode 100644 index 0000000..1ac29a3 --- /dev/null +++ b/contracts/synthetic-asset-factory/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "synthetic-asset-factory" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/synthetic-asset-factory/src/errors.rs b/contracts/synthetic-asset-factory/src/errors.rs new file mode 100644 index 0000000..7a80eb6 --- /dev/null +++ b/contracts/synthetic-asset-factory/src/errors.rs @@ -0,0 +1,19 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum SyntheticAssetError { + /// Invalid amount provided + InvalidAmount = 1, + /// Insufficient collateral + InsufficientCollateral = 2, + /// Insufficient synthetic assets + InsufficientSynthetic = 3, + /// Position not liquidatable + PositionNotLiquidatable = 4, + /// Invalid collateral ratio + InvalidCollateralRatio = 5, + /// Invalid liquidation ratio + InvalidLiquidationRatio = 6, +} diff --git a/contracts/synthetic-asset-factory/src/events.rs b/contracts/synthetic-asset-factory/src/events.rs new file mode 100644 index 0000000..6361ddc --- /dev/null +++ b/contracts/synthetic-asset-factory/src/events.rs @@ -0,0 +1,38 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MintEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BurnEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CollateralAddedEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CollateralWithdrawnEvent { + pub user: Address, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LiquidationEvent { + pub liquidator: Address, + pub user: Address, + pub synthetic_amount: i128, + pub collateral_seized: i128, +} diff --git a/contracts/synthetic-asset-factory/src/lib.rs b/contracts/synthetic-asset-factory/src/lib.rs new file mode 100644 index 0000000..d6b6a94 --- /dev/null +++ b/contracts/synthetic-asset-factory/src/lib.rs @@ -0,0 +1,360 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, Address, Env, String, BytesN}; +use storage::{ + get_oracle, put_oracle, get_collateral_token, put_collateral_token, + get_synthetic_asset, put_synthetic_asset, get_collateral_ratio, put_collateral_ratio, + get_user_collateral, put_user_collateral, get_user_synthetic, put_user_synthetic, + get_total_collateral, put_total_collateral, get_total_synthetic, put_total_synthetic, + get_liquidation_ratio, put_liquidation_ratio, +}; +use events::{MintEvent, BurnEvent, CollateralAddedEvent, CollateralWithdrawnEvent, LiquidationEvent}; +use errors::SyntheticAssetError; + +/// Synthetic Asset Factory Contract +/// +/// Allows users to mint synthetic assets by locking collateral. +/// Tracks collateralization ratios and enables liquidations. +#[contract] +pub struct SyntheticAssetFactory; + +#[contractimpl] +impl SyntheticAssetFactory { + /// Initialize the synthetic asset factory + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `oracle` - Price oracle contract address + /// * `collateral_token` - Collateral token address + /// * `synthetic_asset` - Synthetic asset token address + /// * `collateral_ratio` - Minimum collateral ratio in basis points (e.g., 15000 = 150%) + /// * `liquidation_ratio` - Liquidation threshold in basis points (e.g., 13000 = 130%) + pub fn initialize( + e: Env, + admin: Address, + oracle: Address, + collateral_token: Address, + synthetic_asset: Address, + collateral_ratio: u32, + liquidation_ratio: u32, + ) -> Result<(), SyntheticAssetError> { + admin.require_auth(); + + if collateral_ratio < 10000 { + return Err(SyntheticAssetError::InvalidCollateralRatio); + } + + if liquidation_ratio >= collateral_ratio { + return Err(SyntheticAssetError::InvalidLiquidationRatio); + } + + put_oracle(&e, oracle); + put_collateral_token(&e, collateral_token); + put_synthetic_asset(&e, synthetic_asset); + put_collateral_ratio(&e, collateral_ratio); + put_liquidation_ratio(&e, liquidation_ratio); + put_total_collateral(&e, 0); + put_total_synthetic(&e, 0); + Ok(()) + } + + /// Add collateral to user's position + /// + /// # Arguments + /// * `user` - User address + /// * `amount` - Amount of collateral to add + pub fn add_collateral(e: Env, user: Address, amount: i128) -> Result<(), SyntheticAssetError> { + user.require_auth(); + + if amount <= 0 { + return Err(SyntheticAssetError::InvalidAmount); + } + + let user_collateral = get_user_collateral(&e, &user); + let total_collateral = get_total_collateral(&e); + + put_user_collateral(&e, &user, user_collateral + amount); + put_total_collateral(&e, total_collateral + amount); + + // Transfer collateral to contract + let collateral_token = get_collateral_token(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &collateral_token); + token_client.transfer(&user, &e.current_contract_address(), &amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("coll_add"),), + CollateralAddedEvent { + user: user.clone(), + amount, + } + ); + Ok(()) + } + + /// Mint synthetic assets + /// + /// # Arguments + /// * `user` - User address + /// * `amount` - Amount of synthetic assets to mint + pub fn mint_synthetic(e: Env, user: Address, amount: i128) -> Result<(), SyntheticAssetError> { + user.require_auth(); + + if amount <= 0 { + return Err(SyntheticAssetError::InvalidAmount); + } + + let user_collateral = get_user_collateral(&e, &user); + let user_synthetic = get_user_synthetic(&e, &user); + let new_synthetic = user_synthetic + amount; + + // Check collateralization ratio + if !Self::check_collateral_ratio(&e, user_collateral, new_synthetic) { + return Err(SyntheticAssetError::InsufficientCollateral); + } + + let total_synthetic = get_total_synthetic(&e); + put_user_synthetic(&e, &user, new_synthetic); + put_total_synthetic(&e, total_synthetic + amount); + + // Mint synthetic asset to user + let synthetic_asset = get_synthetic_asset(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &synthetic_asset); + token_client.transfer(&e.current_contract_address(), &user, &amount); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("mint"),), + MintEvent { + user: user.clone(), + amount, + } + ); + Ok(()) + } + + /// Burn synthetic assets and withdraw collateral + /// + /// # Arguments + /// * `user` - User address + /// * `synthetic_amount` - Amount of synthetic assets to burn + /// * `collateral_amount` - Amount of collateral to withdraw + pub fn burn_and_withdraw( + e: Env, + user: Address, + synthetic_amount: i128, + collateral_amount: i128, + ) -> Result<(), SyntheticAssetError> { + user.require_auth(); + + if synthetic_amount < 0 || collateral_amount < 0 { + return Err(SyntheticAssetError::InvalidAmount); + } + + let user_collateral = get_user_collateral(&e, &user); + let user_synthetic = get_user_synthetic(&e, &user); + + if user_synthetic < synthetic_amount { + return Err(SyntheticAssetError::InsufficientSynthetic); + } + + if user_collateral < collateral_amount { + return Err(SyntheticAssetError::InsufficientCollateral); + } + + let new_synthetic = user_synthetic - synthetic_amount; + let new_collateral = user_collateral - collateral_amount; + + // Check collateralization ratio after withdrawal + if new_synthetic > 0 && !Self::check_collateral_ratio(&e, new_collateral, new_synthetic) { + return Err(SyntheticAssetError::InsufficientCollateral); + } + + // Update state + put_user_synthetic(&e, &user, new_synthetic); + put_user_collateral(&e, &user, new_collateral); + put_total_synthetic(&e, get_total_synthetic(&e) - synthetic_amount); + put_total_collateral(&e, get_total_collateral(&e) - collateral_amount); + + // Burn synthetic assets + if synthetic_amount > 0 { + let synthetic_asset = get_synthetic_asset(&e); + let synth_client = soroban_sdk::token::TokenClient::new(&e, &synthetic_asset); + synth_client.transfer(&user, &e.current_contract_address(), &synthetic_amount); + + e.events().publish( + (soroban_sdk::symbol_short!("burn"),), + BurnEvent { + user: user.clone(), + amount: synthetic_amount, + } + ); + } + + // Withdraw collateral + if collateral_amount > 0 { + let collateral_token = get_collateral_token(&e); + let coll_client = soroban_sdk::token::TokenClient::new(&e, &collateral_token); + coll_client.transfer(&e.current_contract_address(), &user, &collateral_amount); + + e.events().publish( + (soroban_sdk::symbol_short!("coll_wdr"),), + CollateralWithdrawnEvent { + user: user.clone(), + amount: collateral_amount, + } + ); + } + Ok(()) + } + + /// Liquidate an undercollateralized position + /// + /// # Arguments + /// * `liquidator` - Liquidator address + /// * `user` - User being liquidated + /// * `synthetic_amount` - Amount of synthetic assets to repay + pub fn liquidate( + e: Env, + liquidator: Address, + user: Address, + synthetic_amount: i128, + ) -> Result { + liquidator.require_auth(); + + if synthetic_amount <= 0 { + return Err(SyntheticAssetError::InvalidAmount); + } + + let user_collateral = get_user_collateral(&e, &user); + let user_synthetic = get_user_synthetic(&e, &user); + + // Check if position is liquidatable + if Self::check_liquidation_ratio(&e, user_collateral, user_synthetic) { + return Err(SyntheticAssetError::PositionNotLiquidatable); + } + + if synthetic_amount > user_synthetic { + return Err(SyntheticAssetError::InvalidAmount); + } + + // Calculate collateral to seize (with liquidation bonus) + let collateral_value = Self::get_collateral_value(&e, user_collateral); + let synthetic_value = Self::get_synthetic_value(&e, user_synthetic); + + let collateral_per_synthetic = if synthetic_value > 0 { + user_collateral.checked_mul(10000).unwrap().checked_div(user_synthetic).unwrap() + } else { + 0 + }; + + // Apply 10% liquidation bonus + let collateral_seized = synthetic_amount + .checked_mul(collateral_per_synthetic).unwrap() + .checked_div(10000).unwrap() + .checked_mul(110).unwrap() // 110% = 100% + 10% bonus + .checked_div(100).unwrap(); + + let actual_collateral_seized = collateral_seized.min(user_collateral); + + // Update state + put_user_synthetic(&e, &user, user_synthetic - synthetic_amount); + put_user_collateral(&e, &user, user_collateral - actual_collateral_seized); + put_total_synthetic(&e, get_total_synthetic(&e) - synthetic_amount); + put_total_collateral(&e, get_total_collateral(&e) - actual_collateral_seized); + + // Transfer synthetic assets from liquidator to contract (burn) + let synthetic_asset = get_synthetic_asset(&e); + let synth_client = soroban_sdk::token::TokenClient::new(&e, &synthetic_asset); + synth_client.transfer(&liquidator, &e.current_contract_address(), &synthetic_amount); + + // Transfer collateral to liquidator + let collateral_token = get_collateral_token(&e); + let coll_client = soroban_sdk::token::TokenClient::new(&e, &collateral_token); + coll_client.transfer(&e.current_contract_address(), &liquidator, &actual_collateral_seized); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("liquidate"),), + LiquidationEvent { + liquidator: liquidator.clone(), + user: user.clone(), + synthetic_amount, + collateral_seized: actual_collateral_seized, + } + ); + + Ok(actual_collateral_seized) + } + + /// Get user position + pub fn get_position(e: Env, user: Address) -> (i128, i128) { + (get_user_collateral(&e, &user), get_user_synthetic(&e, &user)) + } + + /// Get total collateral and synthetic + pub fn get_totals(e: Env) -> (i128, i128) { + (get_total_collateral(&e), get_total_synthetic(&e)) + } + + /// Check if position is healthy + pub fn is_healthy(e: Env, user: Address) -> bool { + let user_collateral = get_user_collateral(&e, &user); + let user_synthetic = get_user_synthetic(&e, &user); + + if user_synthetic == 0 { + return true; + } + + Self::check_collateral_ratio(&e, user_collateral, user_synthetic) + } + + /// Internal: Check collateral ratio + fn check_collateral_ratio(e: &Env, collateral: i128, synthetic: i128) -> bool { + if synthetic == 0 { + return true; + } + + let collateral_value = Self::get_collateral_value(e, collateral); + let synthetic_value = Self::get_synthetic_value(e, synthetic); + let required_collateral = synthetic_value + .checked_mul(get_collateral_ratio(e) as i128).unwrap() + .checked_div(10000).unwrap(); + + collateral_value >= required_collateral + } + + /// Internal: Check liquidation ratio + fn check_liquidation_ratio(e: &Env, collateral: i128, synthetic: i128) -> bool { + if synthetic == 0 { + return true; + } + + let collateral_value = Self::get_collateral_value(e, collateral); + let synthetic_value = Self::get_synthetic_value(e, synthetic); + let liquidation_threshold = synthetic_value + .checked_mul(get_liquidation_ratio(e) as i128).unwrap() + .checked_div(10000).unwrap(); + + collateral_value >= liquidation_threshold + } + + /// Internal: Get collateral value (placeholder - should query oracle) + fn get_collateral_value(_e: &Env, amount: i128) -> i128 { + // In production, this would query the oracle for the collateral price + // For now, assuming 1:1 value + amount + } + + /// Internal: Get synthetic value (placeholder - should query oracle) + fn get_synthetic_value(_e: &Env, amount: i128) -> i128 { + // In production, this would query the oracle for the synthetic asset price + // For now, assuming 1:1 value + amount + } +} diff --git a/contracts/synthetic-asset-factory/src/storage.rs b/contracts/synthetic-asset-factory/src/storage.rs new file mode 100644 index 0000000..78cf7a3 --- /dev/null +++ b/contracts/synthetic-asset-factory/src/storage.rs @@ -0,0 +1,106 @@ +use soroban_sdk::{contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Oracle, + CollateralToken, + SyntheticAsset, + CollateralRatio, + LiquidationRatio, + UserCollateral(Address), + UserSynthetic(Address), + TotalCollateral, + TotalSynthetic, +} + +// Oracle storage +pub fn get_oracle(e: &Env) -> Address { + e.storage().instance().get(&DataKey::Oracle).unwrap() +} + +pub fn put_oracle(e: &Env, oracle: Address) { + e.storage().instance().set(&DataKey::Oracle, &oracle); +} + +// Collateral token storage +pub fn get_collateral_token(e: &Env) -> Address { + e.storage().instance().get(&DataKey::CollateralToken).unwrap() +} + +pub fn put_collateral_token(e: &Env, token: Address) { + e.storage().instance().set(&DataKey::CollateralToken, &token); +} + +// Synthetic asset storage +pub fn get_synthetic_asset(e: &Env) -> Address { + e.storage().instance().get(&DataKey::SyntheticAsset).unwrap() +} + +pub fn put_synthetic_asset(e: &Env, asset: Address) { + e.storage().instance().set(&DataKey::SyntheticAsset, &asset); +} + +// Collateral ratio storage +pub fn get_collateral_ratio(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::CollateralRatio).unwrap() +} + +pub fn put_collateral_ratio(e: &Env, ratio: u32) { + e.storage().instance().set(&DataKey::CollateralRatio, &ratio); +} + +// Liquidation ratio storage +pub fn get_liquidation_ratio(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::LiquidationRatio).unwrap() +} + +pub fn put_liquidation_ratio(e: &Env, ratio: u32) { + e.storage().instance().set(&DataKey::LiquidationRatio, &ratio); +} + +// User collateral storage +pub fn get_user_collateral(e: &Env, user: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserCollateral(user.clone())) + .unwrap_or(0) +} + +pub fn put_user_collateral(e: &Env, user: &Address, amount: i128) { + e.storage() + .persistent() + .set(&DataKey::UserCollateral(user.clone()), &amount); +} + +// User synthetic storage +pub fn get_user_synthetic(e: &Env, user: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserSynthetic(user.clone())) + .unwrap_or(0) +} + +pub fn put_user_synthetic(e: &Env, user: &Address, amount: i128) { + e.storage() + .persistent() + .set(&DataKey::UserSynthetic(user.clone()), &amount); +} + +// Total collateral storage +pub fn get_total_collateral(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalCollateral).unwrap_or(0) +} + +pub fn put_total_collateral(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalCollateral, &amount); +} + +// Total synthetic storage +pub fn get_total_synthetic(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalSynthetic).unwrap_or(0) +} + +pub fn put_total_synthetic(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalSynthetic, &amount); +} diff --git a/contracts/yield-strategy/Cargo.toml b/contracts/yield-strategy/Cargo.toml new file mode 100644 index 0000000..e41ce97 --- /dev/null +++ b/contracts/yield-strategy/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "yield-strategy" +version = "0.1.0" +edition = "2021" +authors = ["TrustBridge Team"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + + diff --git a/contracts/yield-strategy/src/errors.rs b/contracts/yield-strategy/src/errors.rs new file mode 100644 index 0000000..5daac16 --- /dev/null +++ b/contracts/yield-strategy/src/errors.rs @@ -0,0 +1,17 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum YieldStrategyError { + /// Invalid amount + InvalidAmount = 1, + /// Insufficient shares + InsufficientShares = 2, + /// Unauthorized access + Unauthorized = 3, + /// Total allocation exceeded 100% + AllocationExceeded = 4, + /// Invalid strategy + InvalidStrategy = 5, +} diff --git a/contracts/yield-strategy/src/events.rs b/contracts/yield-strategy/src/events.rs new file mode 100644 index 0000000..f317196 --- /dev/null +++ b/contracts/yield-strategy/src/events.rs @@ -0,0 +1,23 @@ +use soroban_sdk::{contracttype, Address}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DepositEvent { + pub user: Address, + pub amount: i128, + pub shares: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WithdrawEvent { + pub user: Address, + pub shares: i128, + pub amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RebalanceEvent { + pub total_assets: i128, +} diff --git a/contracts/yield-strategy/src/lib.rs b/contracts/yield-strategy/src/lib.rs new file mode 100644 index 0000000..1f56b06 --- /dev/null +++ b/contracts/yield-strategy/src/lib.rs @@ -0,0 +1,367 @@ +#![no_std] + +mod storage; +mod events; +mod errors; + + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Vec}; +use storage::{ + get_vault_token, put_vault_token, get_strategies, put_strategies, + get_strategy_allocation, put_strategy_allocation, get_total_assets, put_total_assets, + get_total_shares, put_total_shares, get_user_shares, put_user_shares, + get_strategy_assets, put_strategy_assets, get_admin, put_admin, +}; +use events::{DepositEvent, WithdrawEvent, RebalanceEvent}; +use errors::YieldStrategyError; + +#[derive(Clone, PartialEq, Eq, Debug)] +#[contracttype] +pub struct Strategy { + pub address: Address, + pub target_allocation: u32, // In basis points (10000 = 100%) + pub current_assets: i128, +} + +/// Yield Strategy Manager Contract +/// +/// Manages yield-generating strategies by allocating funds across multiple protocols. +/// Implements vault-like functionality with automated rebalancing. +#[contract] +pub struct YieldStrategyContract; + +#[contractimpl] +impl YieldStrategyContract { + /// Initialize the yield strategy manager + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `vault_token` - Token to be managed in the vault + pub fn initialize(e: Env, admin: Address, vault_token: Address) -> Result<(), YieldStrategyError> { + admin.require_auth(); + + put_admin(&e, admin); + put_vault_token(&e, vault_token); + put_total_assets(&e, 0); + put_total_shares(&e, 0); + + let strategies = Vec::new(&e); + put_strategies(&e, strategies); + Ok(()) + } + + /// Add a yield strategy + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `strategy_address` - Strategy contract address + /// * `target_allocation` - Target allocation in basis points + pub fn add_strategy( + e: Env, + admin: Address, + strategy_address: Address, + target_allocation: u32, + ) -> Result<(), YieldStrategyError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(YieldStrategyError::Unauthorized); + } + + let mut strategies = get_strategies(&e); + + // Check total allocation doesn't exceed 100% + let mut total_allocation = target_allocation; + for strategy in strategies.iter() { + total_allocation += strategy.target_allocation; + } + + if total_allocation > 10000 { + return Err(YieldStrategyError::AllocationExceeded); + } + + let new_strategy = Strategy { + address: strategy_address, + target_allocation, + current_assets: 0, + }; + + strategies.push_back(new_strategy); + put_strategies(&e, strategies); + Ok(()) + } + + /// Update strategy allocation + /// + /// # Arguments + /// * `admin` - Administrator address + /// * `strategy_index` - Index of strategy to update + /// * `new_allocation` - New target allocation in basis points + pub fn update_allocation( + e: Env, + admin: Address, + strategy_index: u32, + new_allocation: u32, + ) -> Result<(), YieldStrategyError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(YieldStrategyError::Unauthorized); + } + + let mut strategies = get_strategies(&e); + + if strategy_index >= strategies.len() { + return Err(YieldStrategyError::InvalidStrategy); + } + + // Check total allocation + let mut total_allocation = new_allocation; + for (i, strategy) in strategies.iter().enumerate() { + if i as u32 != strategy_index { + total_allocation += strategy.target_allocation; + } + } + + if total_allocation > 10000 { + return Err(YieldStrategyError::AllocationExceeded); + } + + let mut strategy = strategies.get(strategy_index).unwrap(); + strategy.target_allocation = new_allocation; + strategies.set(strategy_index, strategy); + + put_strategies(&e, strategies); + Ok(()) + } + + /// Deposit assets into the vault + /// + /// # Arguments + /// * `user` - User address + /// * `amount` - Amount to deposit + /// + /// # Returns + /// * Shares minted + pub fn deposit(e: Env, user: Address, amount: i128) -> Result { + user.require_auth(); + + if amount <= 0 { + return Err(YieldStrategyError::InvalidAmount); + } + + let total_assets = get_total_assets(&e); + let total_shares = get_total_shares(&e); + + // Calculate shares to mint + let shares = if total_shares == 0 { + amount // Initial deposit: 1:1 ratio + } else { + amount + .checked_mul(total_shares).unwrap() + .checked_div(total_assets).unwrap() + }; + + // Transfer tokens to vault + let vault_token = get_vault_token(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &vault_token); + token_client.transfer(&user, &e.current_contract_address(), &amount); + + // Update state + put_total_assets(&e, total_assets + amount); + put_total_shares(&e, total_shares + shares); + + let user_shares = get_user_shares(&e, &user); + put_user_shares(&e, &user, user_shares + shares); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("deposit"),), + DepositEvent { + user: user.clone(), + amount, + shares, + } + ); + + Ok(shares) + } + + /// Withdraw assets from the vault + /// + /// # Arguments + /// * `user` - User address + /// * `shares` - Shares to redeem + /// + /// # Returns + /// * Amount withdrawn + pub fn withdraw(e: Env, user: Address, shares: i128) -> Result { + user.require_auth(); + + if shares <= 0 { + return Err(YieldStrategyError::InvalidAmount); + } + + let user_shares = get_user_shares(&e, &user); + if user_shares < shares { + return Err(YieldStrategyError::InsufficientShares); + } + + let total_assets = get_total_assets(&e); + let total_shares = get_total_shares(&e); + + // Calculate amount to withdraw + let amount = shares + .checked_mul(total_assets).unwrap() + .checked_div(total_shares).unwrap(); + + // Check if we need to withdraw from strategies + let vault_balance = Self::get_vault_balance(&e); + if amount > vault_balance { + Self::withdraw_from_strategies(&e, amount - vault_balance); + } + + // Transfer tokens to user + let vault_token = get_vault_token(&e); + let token_client = soroban_sdk::token::TokenClient::new(&e, &vault_token); + token_client.transfer(&e.current_contract_address(), &user, &amount); + + // Update state + put_total_assets(&e, total_assets - amount); + put_total_shares(&e, total_shares - shares); + put_user_shares(&e, &user, user_shares - shares); + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("withdraw"),), + WithdrawEvent { + user: user.clone(), + shares, + amount, + } + ); + + Ok(amount) + } + + /// Rebalance assets across strategies + /// + /// # Arguments + /// * `admin` - Administrator address + pub fn rebalance(e: Env, admin: Address) -> Result<(), YieldStrategyError> { + admin.require_auth(); + + if admin != get_admin(&e) { + return Err(YieldStrategyError::Unauthorized); + } + + let total_assets = get_total_assets(&e); + let strategies = get_strategies(&e); + + // Calculate target amounts for each strategy + for (i, strategy) in strategies.iter().enumerate() { + let target_amount = total_assets + .checked_mul(strategy.target_allocation as i128).unwrap() + .checked_div(10000).unwrap(); + + let current_amount = strategy.current_assets; + let diff = target_amount - current_amount; + + if diff > 0 { + // Deposit into strategy + Self::deposit_to_strategy(&e, &strategy.address, diff); + } else if diff < 0 { + // Withdraw from strategy + Self::withdraw_from_strategy(&e, &strategy.address, -diff); + } + + // Update strategy assets + let mut updated_strategies = get_strategies(&e); + let mut updated_strategy = updated_strategies.get(i as u32).unwrap(); + updated_strategy.current_assets = target_amount; + updated_strategies.set(i as u32, updated_strategy); + put_strategies(&e, updated_strategies); + } + + // Emit event + e.events().publish( + (soroban_sdk::symbol_short!("rebalance"),), + RebalanceEvent { + total_assets, + } + ); + Ok(()) + } + + /// Get total assets under management + pub fn get_total_assets(e: Env) -> i128 { + get_total_assets(&e) + } + + /// Get user shares + pub fn get_user_shares(e: Env, user: Address) -> i128 { + get_user_shares(&e, &user) + } + + /// Get user assets value + pub fn get_user_assets(e: Env, user: Address) -> i128 { + let user_shares = get_user_shares(&e, &user); + let total_shares = get_total_shares(&e); + let total_assets = get_total_assets(&e); + + if total_shares == 0 { + return 0; + } + + user_shares + .checked_mul(total_assets).unwrap() + .checked_div(total_shares).unwrap() + } + + /// Get all strategies + pub fn get_strategies(e: Env) -> Vec { + get_strategies(&e) + } + + /// Internal: Get vault balance + fn get_vault_balance(e: &Env) -> i128 { + let vault_token = get_vault_token(e); + let token_client = soroban_sdk::token::TokenClient::new(e, &vault_token); + token_client.balance(&e.current_contract_address()) + } + + /// Internal: Deposit to strategy + fn deposit_to_strategy(e: &Env, strategy: &Address, amount: i128) { + let vault_token = get_vault_token(e); + let token_client = soroban_sdk::token::TokenClient::new(e, &vault_token); + token_client.transfer(&e.current_contract_address(), strategy, &amount); + } + + /// Internal: Withdraw from strategy + fn withdraw_from_strategy(e: &Env, strategy: &Address, amount: i128) { + // In production, this would call the strategy's withdraw function + // For now, we assume the strategy transfers tokens back + let vault_token = get_vault_token(e); + let token_client = soroban_sdk::token::TokenClient::new(e, &vault_token); + token_client.transfer(strategy, &e.current_contract_address(), &amount); + } + + /// Internal: Withdraw from strategies (priority-based) + fn withdraw_from_strategies(e: &Env, amount: i128) { + let mut remaining = amount; + let strategies = get_strategies(e); + + // Withdraw from strategies in reverse order (last added first) + for strategy in strategies.iter().rev() { + if remaining <= 0 { + break; + } + + let withdraw_amount = remaining.min(strategy.current_assets); + if withdraw_amount > 0 { + Self::withdraw_from_strategy(e, &strategy.address, withdraw_amount); + remaining -= withdraw_amount; + } + } + } +} diff --git a/contracts/yield-strategy/src/storage.rs b/contracts/yield-strategy/src/storage.rs new file mode 100644 index 0000000..9218854 --- /dev/null +++ b/contracts/yield-strategy/src/storage.rs @@ -0,0 +1,105 @@ +use soroban_sdk::{contracttype, Address, Env, Vec}; +use crate::Strategy; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Admin, + VaultToken, + Strategies, + TotalAssets, + TotalShares, + UserShares(Address), + StrategyAllocation(Address), + StrategyAssets(Address), +} + +// Admin storage +pub fn get_admin(e: &Env) -> Address { + e.storage().instance().get(&DataKey::Admin).unwrap() +} + +pub fn put_admin(e: &Env, admin: Address) { + e.storage().instance().set(&DataKey::Admin, &admin); +} + +// Vault token storage +pub fn get_vault_token(e: &Env) -> Address { + e.storage().instance().get(&DataKey::VaultToken).unwrap() +} + +pub fn put_vault_token(e: &Env, token: Address) { + e.storage().instance().set(&DataKey::VaultToken, &token); +} + +// Strategies storage +pub fn get_strategies(e: &Env) -> Vec { + e.storage() + .instance() + .get(&DataKey::Strategies) + .unwrap_or(Vec::new(e)) +} + +pub fn put_strategies(e: &Env, strategies: Vec) { + e.storage().instance().set(&DataKey::Strategies, &strategies); +} + +// Total assets storage +pub fn get_total_assets(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalAssets).unwrap_or(0) +} + +pub fn put_total_assets(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalAssets, &amount); +} + +// Total shares storage +pub fn get_total_shares(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalShares).unwrap_or(0) +} + +pub fn put_total_shares(e: &Env, shares: i128) { + e.storage().instance().set(&DataKey::TotalShares, &shares); +} + +// User shares storage +pub fn get_user_shares(e: &Env, user: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::UserShares(user.clone())) + .unwrap_or(0) +} + +pub fn put_user_shares(e: &Env, user: &Address, shares: i128) { + e.storage() + .persistent() + .set(&DataKey::UserShares(user.clone()), &shares); +} + +// Strategy allocation storage +pub fn get_strategy_allocation(e: &Env, strategy: &Address) -> u32 { + e.storage() + .persistent() + .get(&DataKey::StrategyAllocation(strategy.clone())) + .unwrap_or(0) +} + +pub fn put_strategy_allocation(e: &Env, strategy: &Address, allocation: u32) { + e.storage() + .persistent() + .set(&DataKey::StrategyAllocation(strategy.clone()), &allocation); +} + +// Strategy assets storage +pub fn get_strategy_assets(e: &Env, strategy: &Address) -> i128 { + e.storage() + .persistent() + .get(&DataKey::StrategyAssets(strategy.clone())) + .unwrap_or(0) +} + +pub fn put_strategy_assets(e: &Env, strategy: &Address, assets: i128) { + e.storage() + .persistent() + .set(&DataKey::StrategyAssets(strategy.clone()), &assets); +}