From 4c981f4f69764c8d554ca2840d2525ab3a9b92c3 Mon Sep 17 00:00:00 2001 From: rabsqueen Date: Wed, 26 Aug 2026 16:11:30 +0100 Subject: [PATCH 1/2] feat(guards): implement flash-loan reentrancy guard using block number verification (#729) --- contracts/flash_loan_guard/src/lib.rs | 36 +++++++++++++++++++++++++ contracts/flash_loan_guard/src/test.rs | 37 ++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 contracts/flash_loan_guard/src/lib.rs create mode 100644 contracts/flash_loan_guard/src/test.rs diff --git a/contracts/flash_loan_guard/src/lib.rs b/contracts/flash_loan_guard/src/lib.rs new file mode 100644 index 00000000..0a2d130c --- /dev/null +++ b/contracts/flash_loan_guard/src/lib.rs @@ -0,0 +1,36 @@ +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + DepositBlock(Address), +} + +@contract +pub struct FlashLoanGuardContract; + +@contractimpl +impl FlashLoanGuardContract { + pub fn deposit(env: Env, user: Address) { + user.require_auth(); + let current_block = env.ledger().sequence(); + env.storage().persistent().set(&DataKey::DepositBlock(user.clone()), ¤t_block); + } + + pub fn withdraw(env: Env, user: Address) { + user.require_auth(); + let current_block = env.ledger().sequence(); + + let deposit_block: u32 = env.storage() + .persistent() + .get(&DataKey::DepositBlock(user.clone())) + .unwrap_or(0); + + if current_block <= deposit_block { + panic!("FlashLoanReentrancy: withdrawal prohibited in the same ledger block as deposit"); + } + + // Proceed with withdrawal logic... + env.storage().persistent().remove(&DataKey::DepositBlock(user)); + } +} \ No newline at end of file diff --git a/contracts/flash_loan_guard/src/test.rs b/contracts/flash_loan_guard/src/test.rs new file mode 100644 index 00000000..d8a7ee49 --- /dev/null +++ b/contracts/flash_loan_guard/src/test.rs @@ -0,0 +1,37 @@ +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{Env, Address}; + + #[test] + #[should_panic(expected = "FlashLoanReentrancy")] + fn test_prevents_same_block_withdrawal() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(FlashLoanGuardContract, ()); + let client = FlashLoanGuardContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + env.ledger().set_sequence_number(100); + client.deposit(&user); + + // Attempt withdrawal in the same block (100) -> should panic + client.withdraw(&user); + } + + #[test] + fn test_allows_subsequent_block_withdrawal() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(FlashLoanGuardContract, ()); + let client = FlashLoanGuardContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + env.ledger().set_sequence_number(100); + client.deposit(&user); + + // Advance ledger block + env.ledger().set_sequence_number(101); + client.withdraw(&user); // Should succeed without panicking + } +} \ No newline at end of file From b06395834da1f569812945feeaaa8e9d10358ffb Mon Sep 17 00:00:00 2001 From: rabsqueen Date: Wed, 26 Aug 2026 16:16:53 +0100 Subject: [PATCH 2/2] feat(math): add slippage protection for deposit and withdrawal with min_shares_out and min_tokens_out (#728) --- contracts/yield_vault/src/lib.rs | 45 +++++++++++++++++++++++++++++++ contracts/yield_vault/src/test.rs | 30 +++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 contracts/yield_vault/src/lib.rs create mode 100644 contracts/yield_vault/src/test.rs diff --git a/contracts/yield_vault/src/lib.rs b/contracts/yield_vault/src/lib.rs new file mode 100644 index 00000000..92461fbf --- /dev/null +++ b/contracts/yield_vault/src/lib.rs @@ -0,0 +1,45 @@ +use soroban_sdk::{contract, contractimpl, Address, Env}; + +@contract +pub struct YieldVaultContract; + +@contractimpl +impl YieldVaultContract { + pub fn deposit(env: Env, user: Address, token_amount: i128, min_shares_out: i128) -> i128 { + user.require_auth(); + + // Calculate shares to mint based on current vault exchange rate + let shares_out = Self::calculate_shares_out(&env, token_amount); + + if shares_out < min_shares_out { + panic!("SlippageExceeded: minted shares are less than min_shares_out"); + } + + // Perform deposit accounting and token transfer... + shares_out + } + + pub fn withdraw(env: Env, user: Address, shares_in: i128, min_tokens_out: i128) -> i128 { + user.require_auth(); + + // Calculate tokens to return based on current vault exchange rate + let tokens_out = Self::calculate_tokens_out(&env, shares_in); + + if tokens_out < min_tokens_out { + panic!("SlippageExceeded: returned tokens are less than min_tokens_out"); + } + + // Perform withdrawal accounting and token transfer... + tokens_out + } + + fn calculate_shares_out(_env: &Env, token_amount: i128) -> i128 { + // Mock exchange rate logic: 1:1 ratio for demonstration + token_amount + } + + fn calculate_tokens_out(_env: &Env, shares_in: i128) -> i128 { + // Mock exchange rate logic: 1:1 ratio for demonstration + shares_in + } +} \ No newline at end of file diff --git a/contracts/yield_vault/src/test.rs b/contracts/yield_vault/src/test.rs new file mode 100644 index 00000000..d39b8a30 --- /dev/null +++ b/contracts/yield_vault/src/test.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{Env, Address}; + + #[test] + fn test_deposit_slippage_success() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + let shares = client.deposit(&user, &1000, &950); + assert_eq!(shares, 1000); + } + + #[test] + #[should_panic(expected = "SlippageExceeded")] + fn test_deposit_slippage_revert() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + // Expecting 1050 shares out when deposit yields 1000 -> should panic + client.deposit(&user, &1000, &1050); + } +} \ No newline at end of file