Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions contracts/lending_pool/src/flash_loan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! # Flash-Loan Provider for Lending Pool (SC-031)
//!
//! Provides uncollateralized flash loans of pool assets to any borrower contract,
//! requiring full repayment plus a 0.09% (9 BPS) fee within the exact same
//! execution frame / transaction envelope.
//! Accrues fee revenue directly to the pool reserves.

use soroban_sdk::{
panic_with_error, symbol_short, token, Address, Env, IntoVal, Symbol,
};
use crate::{LPError, BPS};

/// Protocol flash loan fee in basis points (0.09% = 9 bps)
pub const FLASH_LOAN_FEE_BPS: i128 = 9;

/// Storage key for tracking active flash loan reentrancy
const FLASH_LOAN_LOCK: Symbol = symbol_short!("fl_lock");

/// Interface expected for flash-loan receivers
pub trait FlashLoanReceiver {
fn execute_operation(
env: Env,
caller: Address,
token: Address,
amount: i128,
fee: i128,
params: soroban_sdk::Bytes,
);
}

pub struct FlashLoan;

impl FlashLoan {
/// Executes an uncollateralized flash loan:
/// 1. Verifies the amount is positive.
/// 2. Sets a transaction-scoped flash-loan lock in temporary storage.
/// 3. Records pre-loan pool balance of the token.
/// 4. Transfers `amount` to `receiver`.
/// 5. Invokes `receiver.execute_operation(...)`.
/// 6. Verifies that the pool balance is now >= pre-balance + fee (0.09%).
/// 7. Clears lock and records fee revenue.
pub fn execute_flash_loan(
env: &Env,
receiver: Address,
token: Address,
amount: i128,
params: soroban_sdk::Bytes,
) -> i128 {
if amount <= 0 {
panic_with_error!(env, LPError::ZeroAmount);
}

// Prevent nested flash loans in the same frame
if env.storage().temporary().has(&FLASH_LOAN_LOCK) {
panic_with_error!(env, LPError::ReentrancyGuardTriggered);
}
env.storage().temporary().set(&FLASH_LOAN_LOCK, &true);

let token_client = token::Client::new(env, &token);
let contract_addr = env.current_contract_address();

let balance_before = token_client.balance(&contract_addr);
if balance_before < amount {
env.storage().temporary().remove(&FLASH_LOAN_LOCK);
panic_with_error!(env, LPError::InsufficientBal);
}

// Fee = ceil(amount * 9 / 10,000)
let fee = match amount
.checked_mul(FLASH_LOAN_FEE_BPS)
.and_then(|prod| prod.checked_add(BPS - 1))
.and_then(|num| num.checked_div(BPS))
{
Some(f) => f.max(1),
None => {
env.storage().temporary().remove(&FLASH_LOAN_LOCK);
panic_with_error!(env, LPError::Overflow);
}
};

// Transfer funds to receiver
token_client.transfer(&contract_addr, &receiver, &amount);

// Call borrower's contract hook
let receiver_args = (
contract_addr.clone(),
token.clone(),
amount,
fee,
params,
);
env.invoke_contract::<()>(
&receiver,
&Symbol::new(env, "execute_operation"),
receiver_args.into_val(env),
);

// Verification: ensure full repayment + fee
let balance_after = token_client.balance(&contract_addr);
let required_balance = match balance_before.checked_add(fee) {
Some(req) => req,
None => {
env.storage().temporary().remove(&FLASH_LOAN_LOCK);
panic_with_error!(env, LPError::Overflow);
}
};

if balance_after < required_balance {
env.storage().temporary().remove(&FLASH_LOAN_LOCK);
panic_with_error!(env, LPError::InsufficientBal);
}

// Clear temporary lock
env.storage().temporary().remove(&FLASH_LOAN_LOCK);

// Emit flash-loan event
env.events().publish(
(symbol_short!("flash"), token, receiver),
(amount, fee),
);

fee
}
}
134 changes: 131 additions & 3 deletions contracts/lending_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ use soroban_sdk::{
Address, Env, IntoVal, Symbol,
};

pub mod flash_loan;
pub mod math;
pub mod staking_rewards;

// ── Constants ─────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -476,6 +478,65 @@ impl LendingPool {
env.events().publish((symbol_short!("lp_pause"),), paused);
}

// ── Flash Loan (SC-031) ───────────────────────────────────────────────────

/// Executes an uncollateralized flash loan requiring 0.09% fee and full repayment.
pub fn flash_loan(
env: Env,
receiver: Address,
token: Address,
amount: i128,
params: soroban_sdk::Bytes,
) -> i128 {
Self::check_not_paused(&env);
Self::check_supported(&env, &token);
flash_loan::FlashLoan::execute_flash_loan(&env, receiver, token, amount, params)
}

// ── Continuous Staking Rewards (SC-032) ───────────────────────────────────

/// Initialize continuous yield farming rewards.
pub fn init_staking(
env: Env,
staking_token: Address,
reward_token: Address,
reward_rate: u128,
duration_seconds: u64,
) {
Self::only_admin(&env);
staking_rewards::StakingRewards::initialize(
&env,
staking_token,
reward_token,
reward_rate,
duration_seconds,
);
}

/// Stake tokens into the yield farming pool.
pub fn stake(env: Env, user: Address, amount: u128) {
user.require_auth();
Self::check_not_paused(&env);
staking_rewards::StakingRewards::stake(&env, user, amount);
}

/// Unstake tokens from the yield farming pool.
pub fn unstake(env: Env, user: Address, amount: u128) {
user.require_auth();
staking_rewards::StakingRewards::unstake(&env, user, amount);
}

/// Claim accumulated yield farming rewards.
pub fn claim_reward(env: Env, user: Address) -> u128 {
user.require_auth();
staking_rewards::StakingRewards::claim_reward(&env, user)
}

/// View earned rewards for a user.
pub fn earned(env: Env, user: Address) -> u128 {
staking_rewards::StakingRewards::earned(&env, &user)
}

// ── Views ─────────────────────────────────────────────────────────────────

pub fn collateral_of(env: Env, user: Address, token: Address) -> i128 {
Expand Down Expand Up @@ -723,7 +784,7 @@ impl LendingPool {
#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, Env};
use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Env};

/// Minimal mock oracle: always returns SCALE for any token.
#[contract]
Expand All @@ -735,12 +796,20 @@ mod tests {
}
}

/// Minimal mock token: transfer is a no-op (avoids balance accounting).
/// Mock token tracking balances.
#[contract]
struct MockToken;
#[contractimpl]
impl MockToken {
pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) {}
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
let from_bal: i128 = env.storage().instance().get(&from).unwrap_or(10_000_000_000);
let to_bal: i128 = env.storage().instance().get(&to).unwrap_or(10_000_000_000);
env.storage().instance().set(&from, &(from_bal - amount));
env.storage().instance().set(&to, &(to_bal + amount));
}
pub fn balance(env: Env, id: Address) -> i128 {
env.storage().instance().get(&id).unwrap_or(10_000_000_000)
}
}

fn setup(env: &Env) -> (LendingPoolClient<'static>, Address, Address) {
Expand Down Expand Up @@ -859,6 +928,65 @@ mod tests {
client.deposit_collateral(&user, &token, &1_000);
}

#[contract]
struct MockReceiver;
#[contractimpl]
impl MockReceiver {
pub fn execute_operation(
env: Env,
caller: Address,
token: Address,
amount: i128,
fee: i128,
_params: soroban_sdk::Bytes,
) {
token::Client::new(&env, &token).transfer(&env.current_contract_address(), &caller, &(amount + fee));
}
}

#[test]
fn flash_loan_executes_and_charges_fee() {
let env = Env::default();
env.mock_all_auths();
let (client, _, _) = setup(&env);
let token = add_token(&env, &client);
let receiver = env.register(MockReceiver, ());

let fee = client.flash_loan(&receiver, &token, &10_000, &soroban_sdk::Bytes::new(&env));
// 10_000 * 9 / 10_000 = 9
assert_eq!(fee, 9);
}

#[test]
fn staking_pool_accumulates_rewards() {
let env = Env::default();
env.mock_all_auths();
let (client, admin, _) = setup(&env);
let staking_token = add_token(&env, &client);
let reward_token = add_token(&env, &client);
let user1 = Address::generate(&env);

let reward_rate: u128 = 100_000_000_000_000_000; // 0.1 tokens/sec
client.init_staking(&staking_token, &reward_token, &reward_rate, &3600);

client.stake(&user1, &100_000_000_000_000_000_000); // 100 tokens
assert_eq!(client.earned(&user1), 0);

// Advance ledger timestamp by 10 seconds
let current_time = env.ledger().timestamp();
env.ledger().set_timestamp(current_time + 10);

let earned = client.earned(&user1);
// 10s * 0.1 = 1 token (1e18)
assert_eq!(earned, 1_000_000_000_000_000_000);

let claimed = client.claim_reward(&user1);
assert_eq!(claimed, 1_000_000_000_000_000_000);
assert_eq!(client.earned(&user1), 0);

client.unstake(&user1, &100_000_000_000_000_000_000);
}

use proptest::prelude::*;

proptest! {
Expand Down
Loading
Loading