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);
+}