From b664505676186496622280a0ec8883220c5c12f9 Mon Sep 17 00:00:00 2001 From: floxxih Date: Fri, 20 Feb 2026 06:13:46 +0100 Subject: [PATCH] feat: add flash loan contract for arbitrage Implements flash loan functionality enabling users to borrow tokens without collateral for single-transaction arbitrage and liquidations. Features: - Liquidity pool management with add/remove liquidity functions - Flash loan with callback mechanism for same-transaction repayment - Fee calculation (0.1-0.3% configurable) - Maximum loan limits based on pool liquidity - Reentrancy protection guard - Emergency pause functionality - Analytics tracking for loans, volume, and fees - Admin functions for fee rates and pool management - Comprehensive test suite --- Cargo.toml | 1 + contracts/flash_loan/Cargo.toml | 19 ++ contracts/flash_loan/src/lib.rs | 553 +++++++++++++++++++++++++++++++ contracts/flash_loan/src/test.rs | 361 ++++++++++++++++++++ 4 files changed, 934 insertions(+) create mode 100644 contracts/flash_loan/Cargo.toml create mode 100644 contracts/flash_loan/src/lib.rs create mode 100644 contracts/flash_loan/src/test.rs diff --git a/Cargo.toml b/Cargo.toml index 8777313..5226756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "contracts/governance", "contracts/lottery", "contracts/seasonal_event", + "contracts/flash_loan", ] [workspace.dependencies] diff --git a/contracts/flash_loan/Cargo.toml b/contracts/flash_loan/Cargo.toml new file mode 100644 index 0000000..b6e24c1 --- /dev/null +++ b/contracts/flash_loan/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "flash-loan-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" diff --git a/contracts/flash_loan/src/lib.rs b/contracts/flash_loan/src/lib.rs new file mode 100644 index 0000000..35b4e2e --- /dev/null +++ b/contracts/flash_loan/src/lib.rs @@ -0,0 +1,553 @@ +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, +}; + +const BASIS_POINTS: i128 = 10_000; +const MIN_FEE_BPS: u32 = 10; +const MAX_FEE_BPS: u32 = 30; + +#[contracttype] +pub enum DataKey { + Config, + Pool(Address), + PoolList, + Analytics, + FlashLoanCounter, + FlashLoanRecord(u64), + ReentrancyGuard, + Paused, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FlashLoanStatus { + Active = 1, + Repaid = 2, + Defaulted = 3, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct FlashLoanConfig { + pub admin: Address, + pub fee_bps: u32, + pub max_loan_ratio: u32, + pub paused: bool, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct LiquidityPool { + pub token: Address, + pub total_liquidity: i128, + pub available_liquidity: i128, + pub total_borrowed: i128, + pub fees_collected: i128, + pub lenders: Vec
, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct LenderPosition { + pub lender: Address, + pub amount: i128, + pub deposit_time: u64, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct FlashLoanRecord { + pub loan_id: u64, + pub borrower: Address, + pub token: Address, + pub principal: i128, + pub fee: i128, + pub repayment_amount: i128, + pub start_time: u64, + pub end_time: u64, + pub status: FlashLoanStatus, + pub callback_contract: Address, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct FlashLoanAnalytics { + pub total_loans: u64, + pub total_volume_borrowed: i128, + pub total_fees_collected: i128, + pub total_repaid: i128, + pub defaulted_loans: u64, + pub unique_borrowers: u64, +} + +#[contract] +pub struct FlashLoanContract; + +#[contractimpl] +impl FlashLoanContract { + pub fn initialize(env: Env, admin: Address, fee_bps: u32) { + admin.require_auth(); + + if env.storage().persistent().has(&DataKey::Config) { + panic!("Already initialized"); + } + + if fee_bps < MIN_FEE_BPS || fee_bps > MAX_FEE_BPS { + panic!("Fee must be between 10-30 basis points (0.1%-0.3%)"); + } + + let config = FlashLoanConfig { + admin, + fee_bps, + max_loan_ratio: 8000, + paused: false, + }; + + let analytics = FlashLoanAnalytics { + total_loans: 0, + total_volume_borrowed: 0, + total_fees_collected: 0, + total_repaid: 0, + defaulted_loans: 0, + unique_borrowers: 0, + }; + + env.storage().persistent().set(&DataKey::Config, &config); + env.storage() + .persistent() + .set(&DataKey::Analytics, &analytics); + env.storage() + .persistent() + .set(&DataKey::FlashLoanCounter, &0u64); + env.storage() + .persistent() + .set(&DataKey::PoolList, &Vec::
::new(&env)); + env.storage().persistent().set(&DataKey::Paused, &false); + env.storage() + .persistent() + .set(&DataKey::ReentrancyGuard, &false); + } + + pub fn add_liquidity(env: Env, lender: Address, token: Address, amount: i128) { + lender.require_auth(); + Self::assert_not_paused(&env); + + if amount <= 0 { + panic!("Amount must be greater than zero"); + } + + let token_client = token::Client::new(&env, &token); + token_client.transfer(&lender, &env.current_contract_address(), &amount); + + let mut pool = Self::get_or_create_pool(&env, &token); + pool.total_liquidity += amount; + pool.available_liquidity += amount; + + if !pool.lenders.contains(&lender) { + pool.lenders.push_back(lender.clone()); + } + + env.storage() + .persistent() + .set(&DataKey::Pool(token.clone()), &pool); + + let position = LenderPosition { + lender: lender.clone(), + amount, + deposit_time: env.ledger().timestamp(), + }; + env.storage().persistent().set( + &DataKeyKey::LenderPosition(token.clone(), lender), + &position, + ); + } + + pub fn remove_liquidity(env: Env, lender: Address, token: Address, amount: i128) { + lender.require_auth(); + Self::assert_not_paused(&env); + + if amount <= 0 { + panic!("Amount must be greater than zero"); + } + + let mut pool: LiquidityPool = env + .storage() + .persistent() + .get(&DataKey::Pool(token.clone())) + .unwrap_or_else(|| panic!("Pool not found")); + + if pool.available_liquidity < amount { + panic!("Insufficient available liquidity"); + } + + let position: LenderPosition = env + .storage() + .persistent() + .get(&DataKeyKey::LenderPosition(token.clone(), lender.clone())) + .unwrap_or_else(|| panic!("Lender position not found")); + + if position.amount < amount { + panic!("Insufficient lender balance"); + } + + let token_client = token::Client::new(&env, &token); + token_client.transfer(&env.current_contract_address(), &lender, &amount); + + pool.total_liquidity -= amount; + pool.available_liquidity -= amount; + + env.storage() + .persistent() + .set(&DataKey::Pool(token.clone()), &pool); + + let new_amount = position.amount - amount; + if new_amount == 0 { + env.storage() + .persistent() + .remove(&DataKeyKey::LenderPosition(token.clone(), lender.clone())); + } else { + let updated_position = LenderPosition { + lender: lender.clone(), + amount: new_amount, + deposit_time: position.deposit_time, + }; + env.storage().persistent().set( + &DataKeyKey::LenderPosition(token, lender), + &updated_position, + ); + } + } + + pub fn flash_loan( + env: Env, + borrower: Address, + token: Address, + amount: i128, + callback_contract: Address, + callback_data: soroban_sdk::Bytes, + ) -> u64 { + borrower.require_auth(); + Self::assert_not_paused(&env); + Self::assert_not_reentrant(&env); + + if amount <= 0 { + panic!("Amount must be greater than zero"); + } + + let config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + let mut pool: LiquidityPool = env + .storage() + .persistent() + .get(&DataKey::Pool(token.clone())) + .unwrap_or_else(|| panic!("Pool not found for token")); + + let max_loan = (pool.available_liquidity as i128 * config.max_loan_ratio as i128 + / BASIS_POINTS) as i128; + if amount > max_loan { + panic!("Amount exceeds maximum loan limit"); + } + + if pool.available_liquidity < amount { + panic!("Insufficient liquidity in pool"); + } + + let fee = (amount * config.fee_bps as i128) / BASIS_POINTS; + let repayment_amount = amount + fee; + + let loan_id: u64 = env + .storage() + .persistent() + .get(&DataKey::FlashLoanCounter) + .unwrap_or(0) + + 1; + env.storage() + .persistent() + .set(&DataKey::FlashLoanCounter, &loan_id); + + let start_time = env.ledger().timestamp(); + + let record = FlashLoanRecord { + loan_id, + borrower: borrower.clone(), + token: token.clone(), + principal: amount, + fee, + repayment_amount, + start_time, + end_time: 0, + status: FlashLoanStatus::Active, + callback_contract: callback_contract.clone(), + }; + + env.storage() + .persistent() + .set(&DataKey::FlashLoanRecord(loan_id), &record); + + pool.available_liquidity -= amount; + pool.total_borrowed += amount; + env.storage() + .persistent() + .set(&DataKey::Pool(token.clone()), &pool); + + let token_client = token::Client::new(&env, &token); + token_client.transfer(&env.current_contract_address(), &borrower, &amount); + + Self::set_reentrancy_guard(&env, true); + + let callback_args = (borrower.clone(), token.clone(), amount, fee, callback_data); + let _callback_result: bool = env.invoke_contract( + &callback_contract, + &Symbol::new(&env, "flash_loan_callback"), + callback_args.into_val(&env), + ); + + Self::set_reentrancy_guard(&env, false); + + let current_balance = token_client.balance(&env.current_contract_address()); + let expected_balance_after_repayment = pool.available_liquidity + repayment_amount; + + if current_balance < expected_balance_after_repayment { + let mut updated_record: FlashLoanRecord = env + .storage() + .persistent() + .get(&DataKey::FlashLoanRecord(loan_id)) + .unwrap(); + updated_record.status = FlashLoanStatus::Defaulted; + updated_record.end_time = env.ledger().timestamp(); + env.storage() + .persistent() + .set(&DataKey::FlashLoanRecord(loan_id), &updated_record); + panic!("Flash loan not repaid within transaction"); + } + + let mut updated_pool: LiquidityPool = env + .storage() + .persistent() + .get(&DataKey::Pool(token.clone())) + .unwrap(); + + updated_pool.available_liquidity += repayment_amount; + updated_pool.fees_collected += fee; + updated_pool.total_borrowed -= amount; + env.storage() + .persistent() + .set(&DataKey::Pool(token), &updated_pool); + + let mut updated_record: FlashLoanRecord = env + .storage() + .persistent() + .get(&DataKey::FlashLoanRecord(loan_id)) + .unwrap(); + updated_record.status = FlashLoanStatus::Repaid; + updated_record.end_time = env.ledger().timestamp(); + env.storage() + .persistent() + .set(&DataKey::FlashLoanRecord(loan_id), &updated_record); + + Self::update_analytics(&env, amount, fee, true); + + loan_id + } + + pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if fee_bps < MIN_FEE_BPS || fee_bps > MAX_FEE_BPS { + panic!("Fee must be between 10-30 basis points (0.1%-0.3%)"); + } + + let mut config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.fee_bps = fee_bps; + env.storage().persistent().set(&DataKey::Config, &config); + } + + pub fn set_max_loan_ratio(env: Env, admin: Address, ratio: u32) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + if ratio == 0 || ratio > 10000 { + panic!("Ratio must be between 1-10000 basis points"); + } + + let mut config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.max_loan_ratio = ratio; + env.storage().persistent().set(&DataKey::Config, &config); + } + + pub fn pause(env: Env, admin: Address) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.paused = true; + env.storage().persistent().set(&DataKey::Config, &config); + env.storage().persistent().set(&DataKey::Paused, &true); + } + + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + config.paused = false; + env.storage().persistent().set(&DataKey::Config, &config); + env.storage().persistent().set(&DataKey::Paused, &false); + } + + pub fn withdraw_fees(env: Env, admin: Address, token: Address, amount: i128) { + admin.require_auth(); + Self::assert_admin(&env, &admin); + + let mut pool: LiquidityPool = env + .storage() + .persistent() + .get(&DataKey::Pool(token.clone())) + .unwrap_or_else(|| panic!("Pool not found")); + + if pool.fees_collected < amount { + panic!("Insufficient fees collected"); + } + + pool.fees_collected -= amount; + pool.total_liquidity -= amount; + env.storage() + .persistent() + .set(&DataKey::Pool(token.clone()), &pool); + + let token_client = token::Client::new(&env, &token); + token_client.transfer(&env.current_contract_address(), &admin, &amount); + } + + pub fn get_pool(env: Env, token: Address) -> Option { + env.storage().persistent().get(&DataKey::Pool(token)) + } + + pub fn get_flash_loan(env: Env, loan_id: u64) -> Option { + env.storage() + .persistent() + .get(&DataKey::FlashLoanRecord(loan_id)) + } + + pub fn get_config(env: Env) -> FlashLoanConfig { + env.storage().persistent().get(&DataKey::Config).unwrap() + } + + pub fn get_analytics(env: Env) -> FlashLoanAnalytics { + env.storage().persistent().get(&DataKey::Analytics).unwrap() + } + + pub fn get_all_pools(env: Env) -> Vec
{ + env.storage() + .persistent() + .get(&DataKey::PoolList) + .unwrap_or(Vec::new(&env)) + } + + pub fn calculate_fee(env: Env, amount: i128) -> i128 { + let config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + (amount * config.fee_bps as i128) / BASIS_POINTS + } + + pub fn get_lender_position( + env: Env, + token: Address, + lender: Address, + ) -> Option { + env.storage() + .persistent() + .get(&DataKeyKey::LenderPosition(token, lender)) + } + + fn get_or_create_pool(env: &Env, token: &Address) -> LiquidityPool { + if let Some(pool) = env + .storage() + .persistent() + .get(&DataKey::Pool(token.clone())) + { + return pool; + } + + let mut pool_list: Vec
= env + .storage() + .persistent() + .get(&DataKey::PoolList) + .unwrap_or(Vec::new(env)); + + if !pool_list.contains(token) { + pool_list.push_back(token.clone()); + env.storage() + .persistent() + .set(&DataKey::PoolList, &pool_list); + } + + LiquidityPool { + token: token.clone(), + total_liquidity: 0, + available_liquidity: 0, + total_borrowed: 0, + fees_collected: 0, + lenders: Vec::new(env), + } + } + + fn update_analytics(env: &Env, amount: i128, fee: i128, repaid: bool) { + let mut analytics: FlashLoanAnalytics = + env.storage().persistent().get(&DataKey::Analytics).unwrap(); + + analytics.total_loans += 1; + analytics.total_volume_borrowed += amount; + analytics.total_fees_collected += fee; + + if repaid { + analytics.total_repaid += amount + fee; + } else { + analytics.defaulted_loans += 1; + } + + env.storage() + .persistent() + .set(&DataKey::Analytics, &analytics); + } + + fn assert_admin(env: &Env, user: &Address) { + let config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + if config.admin != *user { + panic!("Admin only"); + } + } + + fn assert_not_paused(env: &Env) { + let config: FlashLoanConfig = env.storage().persistent().get(&DataKey::Config).unwrap(); + if config.paused { + panic!("Contract is paused"); + } + } + + fn assert_not_reentrant(env: &Env) { + let guard: bool = env + .storage() + .persistent() + .get(&DataKey::ReentrancyGuard) + .unwrap_or(false); + if guard { + panic!("Reentrancy detected"); + } + } + + fn set_reentrancy_guard(env: &Env, value: bool) { + env.storage() + .persistent() + .set(&DataKey::ReentrancyGuard, &value); + } +} + +#[contracttype] +pub enum DataKeyKey { + LenderPosition(Address, Address), +} + +#[cfg(test)] +mod test; diff --git a/contracts/flash_loan/src/test.rs b/contracts/flash_loan/src/test.rs new file mode 100644 index 0000000..e6ff785 --- /dev/null +++ b/contracts/flash_loan/src/test.rs @@ -0,0 +1,361 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{testutils::Address as _, token, Address, Env}; + +fn setup_token<'a>( + env: &'a Env, + admin: &'a Address, +) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) { + let token_id = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + let token_client = token::Client::new(env, &token_id); + let token_admin_client = token::StellarAssetClient::new(env, &token_id); + (token_id, token_client, token_admin_client) +} + +#[test] +fn test_initialize() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + + let config = client.get_config(); + assert_eq!(config.admin, admin); + assert_eq!(config.fee_bps, 10); + assert_eq!(config.max_loan_ratio, 8000); + assert_eq!(config.paused, false); + + let analytics = client.get_analytics(); + assert_eq!(analytics.total_loans, 0); + assert_eq!(analytics.total_volume_borrowed, 0); + assert_eq!(analytics.total_fees_collected, 0); +} + +#[test] +#[should_panic(expected = "Already initialized")] +fn test_double_initialize() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.initialize(&admin, &10); +} + +#[test] +#[should_panic(expected = "Fee must be between 10-30 basis points")] +fn test_invalid_fee_too_low() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &5); +} + +#[test] +#[should_panic(expected = "Fee must be between 10-30 basis points")] +fn test_invalid_fee_too_high() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &50); +} + +#[test] +fn test_add_liquidity() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, token_client, token_admin_client) = setup_token(&env, &token_admin); + + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &5_000); + + let pool = client.get_pool(&token_id).unwrap(); + assert_eq!(pool.total_liquidity, 5_000); + assert_eq!(pool.available_liquidity, 5_000); + assert_eq!(pool.fees_collected, 0); + + assert_eq!(token_client.balance(&lender), 5_000); + assert_eq!(token_client.balance(&contract_id), 5_000); +} + +#[test] +fn test_add_liquidity_multiple_times() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, _, token_admin_client) = setup_token(&env, &token_admin); + + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &3_000); + client.add_liquidity(&lender, &token_id, &2_000); + + let pool = client.get_pool(&token_id).unwrap(); + assert_eq!(pool.total_liquidity, 5_000); + assert_eq!(pool.available_liquidity, 5_000); +} + +#[test] +fn test_remove_liquidity() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, token_client, token_admin_client) = setup_token(&env, &token_admin); + + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &5_000); + client.remove_liquidity(&lender, &token_id, &2_000); + + let pool = client.get_pool(&token_id).unwrap(); + assert_eq!(pool.total_liquidity, 3_000); + assert_eq!(pool.available_liquidity, 3_000); + assert_eq!(token_client.balance(&lender), 7_000); +} + +#[test] +#[should_panic(expected = "Insufficient available liquidity")] +fn test_remove_liquidity_too_much() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, _, token_admin_client) = setup_token(&env, &token_admin); + + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &5_000); + client.remove_liquidity(&lender, &token_id, &10_000); +} + +#[test] +fn test_calculate_fee() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + + let fee = client.calculate_fee(&10_000); + assert_eq!(fee, 10); +} + +#[test] +fn test_set_fee_bps() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.set_fee_bps(&admin, &20); + + let config = client.get_config(); + assert_eq!(config.fee_bps, 20); + + let fee = client.calculate_fee(&10_000); + assert_eq!(fee, 20); +} + +#[test] +fn test_set_max_loan_ratio() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.set_max_loan_ratio(&admin, &5000); + + let config = client.get_config(); + assert_eq!(config.max_loan_ratio, 5000); +} + +#[test] +fn test_pause_unpause() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.pause(&admin); + + let config = client.get_config(); + assert_eq!(config.paused, true); + + client.unpause(&admin); + let config = client.get_config(); + assert_eq!(config.paused, false); +} + +#[test] +fn test_get_analytics() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + + let analytics = client.get_analytics(); + assert_eq!(analytics.total_loans, 0); + assert_eq!(analytics.total_volume_borrowed, 0); + assert_eq!(analytics.total_fees_collected, 0); + assert_eq!(analytics.total_repaid, 0); + assert_eq!(analytics.defaulted_loans, 0); +} + +#[test] +fn test_get_all_pools() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, _, token_admin_client) = setup_token(&env, &token_admin); + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &5_000); + + let pools = client.get_all_pools(); + assert_eq!(pools.len(), 1); + assert_eq!(pools.get(0).unwrap(), token_id); +} + +#[test] +fn test_get_lender_position() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, _, token_admin_client) = setup_token(&env, &token_admin); + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.add_liquidity(&lender, &token_id, &5_000); + + let position = client.get_lender_position(&token_id, &lender).unwrap(); + assert_eq!(position.amount, 5_000); + assert_eq!(position.lender, lender); +} + +#[test] +#[should_panic(expected = "Admin only")] +fn test_non_admin_set_fee() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.set_fee_bps(&non_admin, &20); +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_add_liquidity() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let lender = Address::generate(&env); + let token_admin = Address::generate(&env); + + let (token_id, _, token_admin_client) = setup_token(&env, &token_admin); + token_admin_client.mint(&lender, &10_000); + + let contract_id = env.register_contract(None, FlashLoanContract); + let client = FlashLoanContractClient::new(&env, &contract_id); + + client.initialize(&admin, &10); + client.pause(&admin); + client.add_liquidity(&lender, &token_id, &5_000); +}