diff --git a/.freebuff/project-id b/.freebuff/project-id
new file mode 100644
index 0000000..d5491cf
--- /dev/null
+++ b/.freebuff/project-id
@@ -0,0 +1 @@
+5fc15649-80f3-41ea-86a7-a1c5e24c5382
diff --git a/Contract/README.md b/Contract/README.md
index 37cc7a5..a1f35ef 100644
--- a/Contract/README.md
+++ b/Contract/README.md
@@ -165,6 +165,33 @@ Manages decentralized lending pools with advanced controls.
* Admin permission system
* Interest index tracking
* Liquidity protection
+* **Borrowing-contract authorization** — `borrow`, `repay`, and `update_debt`
+ are restricted to a single, immutable borrowing-contract address per pool.
+
+### Initialization Order (Lending ↔ Borrowing)
+
+The lending and borrowing contracts must be initialized in a specific order to
+ensure that borrowing-contract authorization is in place before any loan
+operations occur.
+
+```text
+1. Deploy the lending contract.
+2. Deploy the borrowing contract.
+3. Create a lending pool via `LendingContract::create_pool`.
+4. The pool admin calls `LendingContract::initialize_borrowing_contract`
+ to bind the pool to the borrowing contract address.
+ ⚠ This operation is one-time and irreversible — the configured address
+ cannot be changed without a contract upgrade.
+5. The pool admin configures the lending pool address inside the borrowing
+ contract (borrowing-contract side).
+6. Suppliers call `LendingContract::deposit` to add liquidity.
+7. Borrowers request loans through the borrowing contract, which internally
+ calls `borrow` and `repay` on the lending contract.
+```
+
+> **Security note:** `initialize_borrowing_contract` does **not** replace
+> borrower-level authorization inside the borrowing contract. Both contracts
+> enforce their own access-control checks independently.
---
diff --git a/Contract/lending/src/authorization.rs b/Contract/lending/src/authorization.rs
new file mode 100644
index 0000000..cf7a022
--- /dev/null
+++ b/Contract/lending/src/authorization.rs
@@ -0,0 +1,32 @@
+//! Authorization helpers for the lending contract.
+//!
+//! Ensures that protocol-level operations (`borrow`, `repay`, `update_debt`)
+//! are only callable by the borrowing contract configured for each pool.
+
+use soroban_sdk::{Address, BytesN, Env};
+
+use shared::errors::Error;
+
+use crate::PoolKey;
+
+/// Verify that the transaction caller is the authorized borrowing contract
+/// for `pool_id`.
+///
+/// Returns `Ok(())` when the transacter matches the stored address.
+/// Returns `Err(Error::Unauthorized)` when no borrowing contract has been
+/// configured **or** the caller does not match.
+pub fn require_borrowing_contract(env: &Env, pool_id: &BytesN<32>) -> Result<(), Error> {
+ let key = PoolKey::BorrowingContract(pool_id.clone());
+ let configured: Address = env
+ .storage()
+ .persistent()
+ .get(&key)
+ .ok_or(Error::Unauthorized)?;
+
+ let caller = env.transacter().address();
+ if caller != configured {
+ return Err(Error::Unauthorized);
+ }
+
+ Ok(())
+}
diff --git a/Contract/lending/src/lib.rs b/Contract/lending/src/lib.rs
index bccb5f5..7f077e9 100644
--- a/Contract/lending/src/lib.rs
+++ b/Contract/lending/src/lib.rs
@@ -2,7 +2,8 @@
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env};
use shared::errors::Error;
use shared::events::{
- InterestAccrued, PoolAccountingUpdated, PoolCreated, PoolDeposit, PoolWithdrawal,
+ BorrowingContractInitialized, InterestAccrued, PoolAccountingUpdated, PoolCreated,
+ PoolDeposit, PoolWithdrawal,
};
use shared::types::{
EmergencyStop, InterestParams, PoolAccounting, PoolConfig, PoolStatus,
@@ -10,6 +11,9 @@ use shared::types::{
};
use shared::utils::{FixedMath, SafeMath, TimeHelper, ValidationHelper};
+mod authorization;
+mod state;
+
/// Storage keys for lending contract
#[derive(Clone)]
#[contracttype]
@@ -23,6 +27,7 @@ pub enum PoolKey {
EmergencyStop(BytesN<32>),
AdminPermissions(Address),
PoolStatus(BytesN<32>),
+ BorrowingContract(BytesN<32>),
}
/// Lending contract for managing lending pools and interest.
@@ -346,6 +351,9 @@ impl LendingContract {
to: Address,
amount: i128,
) -> Result<(), Error> {
+ // Only the authorized borrowing contract may call borrow
+ authorization::require_borrowing_contract(&env, &pool_id)?;
+
if !ValidationHelper::validate_positive_amount(amount) {
return Err(Error::InvalidAmount);
}
@@ -402,7 +410,10 @@ impl LendingContract {
principal_amount: i128,
interest_amount: i128,
) -> Result<(), Error> {
- if !ValidationHelper::validate_positive_amount(principal_amount) ||
+ // Only the authorized borrowing contract may call repay
+ authorization::require_borrowing_contract(&env, &pool_id)?;
+
+ if !ValidationHelper::validate_positive_amount(principal_amount) ||
!ValidationHelper::validate_positive_amount(interest_amount) {
return Err(Error::InvalidAmount);
}
@@ -561,6 +572,9 @@ impl LendingContract {
pool_id: BytesN<32>,
debt_change: i128,
) -> Result<(), Error> {
+ // Only the authorized borrowing contract may call update_debt
+ authorization::require_borrowing_contract(&env, &pool_id)?;
+
let pool_exists_key = PoolKey::PoolExists(pool_id.clone());
if !env.storage().persistent().has(&pool_exists_key) {
return Err(Error::PoolNotFound);
@@ -772,6 +786,68 @@ impl LendingContract {
Ok(status)
}
+ /// Initialize the authorized borrowing contract for a pool.
+ ///
+ /// Once set, the borrowing contract address is immutable. This must be
+ /// called before any `borrow` or `repay` operations can be performed on
+ /// the pool.
+ pub fn initialize_borrowing_contract(
+ env: Env,
+ pool_id: BytesN<32>,
+ admin: Address,
+ borrowing_contract: Address,
+ ) -> Result<(), Error> {
+ // Verify the pool exists
+ let pool_exists_key = PoolKey::PoolExists(pool_id.clone());
+ if !env.storage().persistent().has(&pool_exists_key) {
+ return Err(Error::PoolNotFound);
+ }
+
+ // Verify the caller is the pool admin
+ let config: PoolConfig = env
+ .storage()
+ .persistent()
+ .get(&PoolKey::Pool(pool_id.clone()))
+ .ok_or(Error::PoolNotFound)?;
+ if config.admin != admin {
+ return Err(Error::Unauthorized);
+ }
+
+ // Check if already initialized (immutable after init)
+ let key = PoolKey::BorrowingContract(pool_id.clone());
+ if env.storage().persistent().has(&key) {
+ return Err(Error::AlreadyInitialized);
+ }
+
+ // Store the borrowing contract address
+ env.storage().persistent().set(&key, &borrowing_contract);
+
+ // Emit event
+ env.events()
+ .publish(
+ (BorrowingContractInitialized::topic(&env), pool_id.clone()),
+ BorrowingContractInitialized {
+ pool_id,
+ borrowing_contract,
+ initialized_at: TimeHelper::now(&env),
+ },
+ );
+
+ Ok(())
+ }
+
+ /// Get the configured borrowing contract address for a pool.
+ pub fn get_borrowing_contract(
+ env: Env,
+ pool_id: BytesN<32>,
+ ) -> Result
{
+ let key = PoolKey::BorrowingContract(pool_id);
+ env.storage()
+ .persistent()
+ .get(&key)
+ .ok_or(Error::Unauthorized)
+ }
+
fn storage_key(env: &Env, label: &[u8]) -> BytesN<32> {
let mut bytes = [0u8; 32];
let label_len = label.len().min(32);
diff --git a/Contract/lending/src/state.rs b/Contract/lending/src/state.rs
new file mode 100644
index 0000000..dcec126
--- /dev/null
+++ b/Contract/lending/src/state.rs
@@ -0,0 +1,31 @@
+//! Lending contract state layout.
+//!
+//! ## Borrowing Contract Authorization
+//!
+//! Each lending pool stores the address of its authorized borrowing contract
+//! under `PoolKey::BorrowingContract(pool_id)` in persistent storage.
+//!
+//! Once set via [`LendingContract::initialize_borrowing_contract`], this address
+//! is **immutable** — it cannot be changed or removed without a contract upgrade.
+//!
+//! Only the configured borrowing contract may call:
+//! - `borrow` — draw liquidity from the pool
+//! - `repay` — return principal and interest
+//! - `update_debt` — sync outstanding debt accounting
+//!
+//! All read-only endpoints (`get_pool_accounting`, `get_pool_balance`,
+//! `calculate_interest`, `get_share_balance`, `get_borrowing_contract`)
+//! remain public.
+//!
+//! ### Initialization Order
+//!
+//! 1. Deploy the lending contract.
+//! 2. Deploy the borrowing contract.
+//! 3. Create a lending pool via `LendingContract::create_pool`.
+//! 4. The pool admin calls `initialize_borrowing_contract` to bind the pool
+//! to the borrowing contract. This is a one-time, irreversible operation.
+//! 5. The pool admin configures the lending pool address inside the borrowing
+//! contract.
+//! 6. Suppliers call `deposit` to add liquidity.
+//! 7. Borrowers request loans through the borrowing contract, which
+//! internally calls `borrow` and `repay` on the lending contract.
diff --git a/Contract/lending/tests/test_lending.rs b/Contract/lending/tests/test_lending.rs
index c19f4e4..238c8c8 100644
--- a/Contract/lending/tests/test_lending.rs
+++ b/Contract/lending/tests/test_lending.rs
@@ -1,27 +1,75 @@
-use soroban_sdk::{Address, BytesN, Env};
-use lending::LendingContract;
+use lending::LendingContractClient;
use shared::errors::Error;
use shared::types::PoolAccounting;
+use soroban_sdk::testutils::Address as _;
+use soroban_sdk::{Address, BytesN, Env};
-const WASM: &[u8] = lending::WASM;
+use lending::LendingContract;
-#[test]
-fn test_pool_creation() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+// ---------------------------------------------------------------------------
+// Helper: set up a fresh lending contract and return the client + an admin.
+// ---------------------------------------------------------------------------
+struct TestEnv {
+ env: Env,
+ client: LendingContractClient<'static>,
+ admin: Address,
+ pool_id: BytesN<32>,
+}
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let interest_rate = 500i128; // 5% in basis points
- let reserve_factor = 1000i128; // 10% in basis points
+impl TestEnv {
+ fn setup() -> Self {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, LendingContract);
+ // Leak the contract_id so the client can borrow it for 'static.
+ let contract_id = Box::leak(Box::new(contract_id));
+
+ let client = LendingContractClient::new(&env, contract_id);
+ let admin = Address::generate(&env);
+ let pool_id = BytesN::from_array(&env, &[1u8; 32]);
+ let asset = BytesN::from_array(&env, &[2u8; 32]);
+
+ // 5% APR in basis points.
+ client.create_pool(&admin, &asset, &500i128);
+
+ TestEnv {
+ env,
+ client,
+ admin,
+ pool_id: asset, // derive_pool_id copies the asset bytes
+ }
+ }
+
+ /// Register a mock contract and initialize it as the pool's borrowing
+ /// contract. Returns the mock's address.
+ fn init_borrowing_contract(&self) -> Address {
+ let borrowing_id = self.env.register_contract_wasm(None, lending::WASM);
+ self.client.initialize_borrowing_contract(
+ &self.pool_id,
+ &self.admin,
+ &borrowing_id,
+ );
+ borrowing_id
+ }
+
+ /// Call a lending-contract method **as** the given contract address,
+ /// simulating a cross-contract call.
+ fn as_contract(&self, contract_id: &Address, f: F) -> T
+ where
+ F: FnOnce() -> T,
+ {
+ self.env.as_contract(contract_id, f)
+ }
+}
+
+// ===========================================================================
+// Pool creation
+// ===========================================================================
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &interest_rate, &reserve_factor);
+#[test]
+fn test_pool_creation() {
+ let t = TestEnv::setup();
- // Verify pool exists by checking accounting
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.total_assets, 0);
assert_eq!(accounting.total_shares, 0);
assert_eq!(accounting.available_liquidity, 0);
@@ -30,21 +78,11 @@ fn test_pool_creation() {
#[test]
fn test_pool_already_exists() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let interest_rate = 500i128;
- let reserve_factor = 1000i128;
-
- // Create pool first time
- client.create_pool(&pool_id, &asset, &admin, &interest_rate, &reserve_factor);
-
- // Try to create again - should fail
- let result = client.try_create_pool(&pool_id, &asset, &admin, &interest_rate, &reserve_factor);
+ let result = t
+ .client
+ .try_create_pool(&t.pool_id, &[2u8; 32].into(), &t.admin, &500i128);
assert_eq!(result, Err(Ok(Error::PoolAlreadyExists)));
}
@@ -53,283 +91,195 @@ fn test_invalid_interest_rate() {
let env = Env::default();
let contract_id = env.register_contract(None, LendingContract);
let client = LendingContractClient::new(&env, &contract_id);
-
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
let admin = Address::generate(&env);
+ let asset = BytesN::from_array(&env, &[2u8; 32]);
- // Test negative interest rate
- let result = client.try_create_pool(&pool_id, &asset, &admin, &-1i128, &1000i128);
+ let result = client.try_create_pool(&asset, &asset, &admin, &-1i128);
assert_eq!(result, Err(Ok(Error::InvalidInterestRate)));
- // Test interest rate > 100%
- let result = client.try_create_pool(&pool_id, &asset, &admin, &10001i128, &1000i128);
+ let result = client.try_create_pool(&asset, &asset, &admin, &10001i128);
assert_eq!(result, Err(Ok(Error::InvalidInterestRate)));
}
+// ===========================================================================
+// Deposits & withdrawals
+// ===========================================================================
+
#[test]
fn test_single_deposit() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
-
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Deposit
- let deposit_amount = 1000i128;
- client.deposit(&pool_id, &lender, &deposit_amount);
-
- // Verify accounting
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.total_assets, 1000);
- assert_eq!(accounting.total_shares, 1000); // First deposit: 1:1 ratio
+ assert_eq!(accounting.total_shares, 1000);
assert_eq!(accounting.available_liquidity, 1000);
- // Verify user shares
- let share_balance = client.get_share_balance(&pool_id, &lender);
- assert_eq!(share_balance.shares, 1000);
+ let shares = t.client.get_share_balance(&t.pool_id, &lender);
+ assert_eq!(shares.shares, 1000);
}
#[test]
fn test_multiple_suppliers() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
-
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender1 = Address::generate(&env);
- let lender2 = Address::generate(&env);
-
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
+ let t = TestEnv::setup();
+ let lender1 = Address::generate(&t.env);
+ let lender2 = Address::generate(&t.env);
- // First lender deposits
- client.deposit(&pool_id, &lender1, &1000i128);
+ t.client.deposit(&t.pool_id, &lender1, &1000i128);
+ t.client.deposit(&t.pool_id, &lender2, &500i128);
- // Second lender deposits
- client.deposit(&pool_id, &lender2, &500i128);
-
- // Verify accounting
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.total_assets, 1500);
assert_eq!(accounting.total_shares, 1500);
assert_eq!(accounting.available_liquidity, 1500);
- // Verify lender2 got proportional shares (500/1500 * 1500 = 500)
- let share_balance = client.get_share_balance(&pool_id, &lender2);
- assert_eq!(share_balance.shares, 500);
+ let shares = t.client.get_share_balance(&t.pool_id, &lender2);
+ assert_eq!(shares.shares, 500);
}
#[test]
fn test_withdrawal() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
-
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
+ t.client.withdraw(&t.pool_id, &lender, &500i128);
- // Withdraw half of shares
- client.withdraw(&pool_id, &lender, &500i128);
-
- // Verify accounting
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.total_assets, 500);
assert_eq!(accounting.total_shares, 500);
assert_eq!(accounting.available_liquidity, 500);
- // Verify user shares
- let share_balance = client.get_share_balance(&pool_id, &lender);
- assert_eq!(share_balance.shares, 500);
+ let shares = t.client.get_share_balance(&t.pool_id, &lender);
+ assert_eq!(shares.shares, 500);
}
#[test]
fn test_partial_redemption() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
+ t.client.withdraw(&t.pool_id, &lender, &250i128);
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
-
- // Withdraw 25% of shares
- client.withdraw(&pool_id, &lender, &250i128);
-
- // Verify accounting
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.total_assets, 750);
assert_eq!(accounting.total_shares, 750);
}
#[test]
fn test_withdraw_insufficient_shares() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
-
- // Try to withdraw more than owned
- let result = client.try_withdraw(&pool_id, &lender, &1500i128);
+ let result = t.client.try_withdraw(&t.pool_id, &lender, &1500i128);
assert_eq!(result, Err(Ok(Error::InsufficientShares)));
}
#[test]
fn test_withdraw_insufficient_liquidity() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
+ // Simulate borrowing via the authorized borrowing contract
+ t.as_contract(&borrowing_id, || {
+ t.client.borrow(&t.pool_id, &Address::generate(&t.env), &800i128);
+ });
- // Simulate borrowing by reducing liquidity
- client.update_debt(&pool_id, &800i128);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
+ assert_eq!(accounting.available_liquidity, 200);
- // Try to withdraw more than available liquidity
- let result = client.try_withdraw(&pool_id, &lender, &500i128);
+ // 500 shares redeem to 500 assets, exceeding the 200 available.
+ let result = t.client.try_withdraw(&t.pool_id, &lender, &500i128);
assert_eq!(result, Err(Ok(Error::InsufficientLiquidity)));
}
#[test]
fn test_zero_liquidity_withdrawal() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
-
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Borrow all liquidity
- client.update_debt(&pool_id, &1000i128);
+ // Borrow all liquidity via the authorized borrowing contract
+ t.as_contract(&borrowing_id, || {
+ t.client.borrow(&t.pool_id, &Address::generate(&t.env), &1000i128);
+ });
- // Try to withdraw - should fail due to insufficient liquidity
- let result = client.try_withdraw(&pool_id, &lender, &100i128);
+ let result = t.client.try_withdraw(&t.pool_id, &lender, &100i128);
assert_eq!(result, Err(Ok(Error::InsufficientLiquidity)));
}
+// ===========================================================================
+// Interest accrual
+// ===========================================================================
+
#[test]
fn test_interest_accrual() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
-
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- // Create pool with 10% interest rate
- client.create_pool(&pool_id, &asset, &admin, &1000i128, &1000i128);
-
- // Deposit
- client.deposit(&pool_id, &lender, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
// Simulate borrowing
- client.update_debt(&pool_id, &500i128);
+ t.as_contract(&borrowing_id, || {
+ t.client.borrow(&t.pool_id, &Address::generate(&t.env), &500i128);
+ });
- // Jump forward in time (1 day = 86400 seconds)
- env.ledger().set(86400, 1, 1);
+ // Jump forward 1 day
+ t.env.ledger().set(86400, 1, 1);
- // Trigger interest accrual by making a deposit
- client.deposit(&pool_id, &lender, &100i128);
+ // Trigger accrual via deposit
+ t.client.deposit(&t.pool_id, &lender, &100i128);
- // Verify interest was accrued
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert!(accounting.accrued_interest > 0);
- assert!(accounting.interest_index > 1_000_000_000_000_000_000); // Should be > 1.0
+ assert!(accounting.interest_index > 1_000_000_000_000_000_000);
}
#[test]
fn test_long_time_jump() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
-
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &1000i128, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Deposit
- client.deposit(&pool_id, &lender, &1000i128);
-
- // Borrow
- client.update_debt(&pool_id, &500i128);
+ t.as_contract(&borrowing_id, || {
+ t.client.borrow(&t.pool_id, &Address::generate(&t.env), &500i128);
+ });
// Jump forward 1 year
- env.ledger().set(31_536_000, 1, 1);
+ t.env.ledger().set(31_536_000, 1, 1);
- // Trigger interest accrual
- client.deposit(&pool_id, &lender, &100i128);
+ t.client.deposit(&t.pool_id, &lender, &100i128);
- // Verify significant interest accrued
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert!(accounting.accrued_interest > 0);
}
+// ===========================================================================
+// Input validation
+// ===========================================================================
+
#[test]
fn test_arithmetic_limits() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
-
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
-
- // Try deposit with negative amount
- let result = client.try_deposit(&pool_id, &lender, &-100i128);
+ let result = t.client.try_deposit(&t.pool_id, &lender, &-100i128);
assert_eq!(result, Err(Ok(Error::InvalidAmount)));
- // Try deposit with zero amount
- let result = client.try_deposit(&pool_id, &lender, &0i128);
+ let result = t.client.try_deposit(&t.pool_id, &lender, &0i128);
assert_eq!(result, Err(Ok(Error::InvalidAmount)));
}
@@ -339,168 +289,69 @@ fn test_pool_not_found() {
let contract_id = env.register_contract(None, LendingContract);
let client = LendingContractClient::new(&env, &contract_id);
- let pool_id = BytesN::from_array(&[1u8; 32]);
+ let pool_id = BytesN::from_array(&env, &[1u8; 32]);
let lender = Address::generate(&env);
- // Try to deposit to non-existent pool
let result = client.try_deposit(&pool_id, &lender, &1000i128);
assert_eq!(result, Err(Ok(Error::PoolNotFound)));
- // Try to get balance of non-existent pool
let result = client.try_get_pool_balance(&pool_id);
assert_eq!(result, Err(Ok(Error::PoolNotFound)));
}
+// ===========================================================================
+// Debt tracking
+// ===========================================================================
+
#[test]
fn test_debt_tracking() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
-
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- // Create pool and deposit
- client.create_pool(&pool_id, &asset, &admin, &500i128, &1000i128);
- client.deposit(&pool_id, &lender, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
// Borrow
- client.update_debt(&pool_id, &300i128);
+ t.as_contract(&borrowing_id, || {
+ t.client.update_debt(&t.pool_id, &300i128);
+ });
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.outstanding_debt, 300);
assert_eq!(accounting.available_liquidity, 700);
// Repay
- client.update_debt(&pool_id, &-100i128);
+ t.as_contract(&borrowing_id, || {
+ t.client.update_debt(&t.pool_id, &-100i128);
+ });
- let accounting = client.get_pool_accounting(&pool_id);
+ let accounting = t.client.get_pool_accounting(&t.pool_id);
assert_eq!(accounting.outstanding_debt, 200);
assert_eq!(accounting.available_liquidity, 800);
}
+// ===========================================================================
+// User interest calculation
+// ===========================================================================
+
#[test]
fn test_user_interest_calculation() {
- let env = Env::default();
- let contract_id = env.register_contract(None, LendingContract);
- let client = LendingContractClient::new(&env, &contract_id);
-
- let pool_id = BytesN::from_array(&[1u8; 32]);
- let asset = BytesN::from_array(&[2u8; 32]);
- let admin = Address::generate(&env);
- let lender = Address::generate(&env);
+ let t = TestEnv::setup();
+ let lender = Address::generate(&t.env);
+ let borrowing_id = t.init_borrowing_contract();
- // Create pool
- client.create_pool(&pool_id, &asset, &admin, &1000i128, &1000i128);
+ t.client.deposit(&t.pool_id, &lender, &1000i128);
- // Deposit
- client.deposit(&pool_id, &lender, &1000i128);
+ t.as_contract(&borrowing_id, || {
+ t.client.borrow(&t.pool_id, &Address::generate(&t.env), &500i128);
+ });
- // Borrow to generate interest
- client.update_debt(&pool_id, &500i128);
-
- // Jump forward
- env.ledger().set(86400, 1, 1);
+ // Jump forward 1 day
+ t.env.ledger().set(86400, 1, 1);
// Trigger accrual
- client.deposit(&pool_id, &lender, &100i128);
+ t.client.deposit(&t.pool_id, &lender, &100i128);
- // Calculate user interest
- let interest = client.calculate_interest(&pool_id, &lender);
+ let interest = t.client.calculate_interest(&t.pool_id, &lender);
assert!(interest >= 0);
}
-
-// Helper client wrapper
-struct LendingContractClient<'a> {
- env: &'a Env,
- contract_id: &'a soroban_sdk::Address,
-}
-
-impl<'a> LendingContractClient<'a> {
- fn new(env: &'a Env, contract_id: &'a soroban_sdk::Address) -> Self {
- Self { env, contract_id }
- }
-
- fn create_pool(
- &self,
- pool_id: &BytesN<32>,
- asset: &BytesN<32>,
- admin: &Address,
- interest_rate: &i128,
- reserve_factor: &i128,
- ) {
- LendingContract::create_pool(
- self.env.clone(),
- pool_id.clone(),
- asset.clone(),
- admin.clone(),
- *interest_rate,
- *reserve_factor,
- )
- .unwrap();
- }
-
- fn try_create_pool(
- &self,
- pool_id: &BytesN<32>,
- asset: &BytesN<32>,
- admin: &Address,
- interest_rate: &i128,
- reserve_factor: &i128,
- ) -> Result<(), Error> {
- LendingContract::create_pool(
- self.env.clone(),
- pool_id.clone(),
- asset.clone(),
- admin.clone(),
- *interest_rate,
- *reserve_factor,
- )
- }
-
- fn deposit(&self, pool_id: &BytesN<32>, from: &Address, amount: &i128) {
- LendingContract::deposit(self.env.clone(), pool_id.clone(), from.clone(), *amount).unwrap();
- }
-
- fn try_deposit(&self, pool_id: &BytesN<32>, from: &Address, amount: &i128) -> Result<(), Error> {
- LendingContract::deposit(self.env.clone(), pool_id.clone(), from.clone(), *amount)
- }
-
- fn withdraw(&self, pool_id: &BytesN<32>, to: &Address, shares: &i128) {
- LendingContract::withdraw(self.env.clone(), pool_id.clone(), to.clone(), *shares).unwrap();
- }
-
- fn try_withdraw(&self, pool_id: &BytesN<32>, to: &Address, shares: &i128) -> Result<(), Error> {
- LendingContract::withdraw(self.env.clone(), pool_id.clone(), to.clone(), *shares)
- }
-
- fn get_pool_balance(&self, pool_id: &BytesN<32>) -> i128 {
- LendingContract::get_pool_balance(self.env.clone(), pool_id.clone()).unwrap()
- }
-
- fn try_get_pool_balance(&self, pool_id: &BytesN<32>) -> Result {
- LendingContract::get_pool_balance(self.env.clone(), pool_id.clone())
- }
-
- fn calculate_interest(&self, pool_id: &BytesN<32>, lender: &Address) -> i128 {
- LendingContract::calculate_interest(self.env.clone(), lender.clone(), pool_id.clone()).unwrap()
- }
-
- fn get_pool_accounting(&self, pool_id: &BytesN<32>) -> PoolAccounting {
- LendingContract::get_pool_accounting(self.env.clone(), pool_id.clone()).unwrap()
- }
-
- fn get_share_balance(&self, pool_id: &BytesN<32>, lender: &Address) -> shared::types::ShareBalance {
- LendingContract::get_share_balance(self.env.clone(), pool_id.clone(), lender.clone()).unwrap()
- }
-
- fn update_debt(&self, pool_id: &BytesN<32>, debt_change: &i128) {
- LendingContract::update_debt(self.env.clone(), pool_id.clone(), *debt_change).unwrap();
- }
- let _contract_id = env.register_contract_wasm(None, lending::WASM);
-
- // Placeholder test to ensure crate compiles
- // TODO: Add actual tests when lending logic is implemented
-}
diff --git a/Contract/lending/tests/test_lending_borrow_authorization.rs b/Contract/lending/tests/test_lending_borrow_authorization.rs
new file mode 100644
index 0000000..81c2716
--- /dev/null
+++ b/Contract/lending/tests/test_lending_borrow_authorization.rs
@@ -0,0 +1,310 @@
+//! Tests for borrowing-contract authorization on the lending pool.
+//!
+//! Verifies that only the configured borrowing contract may call `borrow`,
+//! `repay`, and `update_debt`, while read-only endpoints remain public.
+
+use lending::{LendingContract, LendingContractClient};
+use shared::errors::Error;
+use soroban_sdk::testutils::Address as _;
+use soroban_sdk::{Address, BytesN, Env};
+
+// ---------------------------------------------------------------------------
+// Fixture
+// ---------------------------------------------------------------------------
+
+struct AuthFixture<'a> {
+ env: &'a Env,
+ client: LendingContractClient<'a>,
+ pool_id: BytesN<32>,
+ admin: Address,
+ lender: Address,
+ borrower: Address,
+}
+
+fn setup<'a>(env: &'a Env) -> AuthFixture<'a> {
+ let contract_id = env.register_contract(None, LendingContract);
+ let client = LendingContractClient::new(env, &contract_id);
+
+ let admin = Address::generate(env);
+ let asset = BytesN::from_array(env, &[2u8; 32]);
+ let lender = Address::generate(env);
+ let borrower = Address::generate(env);
+
+ // Create pool with 5% APR.
+ client.create_pool(&admin, &asset, &500i128);
+
+ AuthFixture {
+ env,
+ client,
+ pool_id: asset,
+ admin,
+ lender,
+ borrower,
+ }
+}
+
+/// Register a mock contract and return its address (used as the borrowing
+/// contract).
+fn mock_borrowing_contract(env: &Env) -> Address {
+ env.register_contract_wasm(None, lending::WASM)
+}
+
+// ===========================================================================
+// initialize_borrowing_contract
+// ===========================================================================
+
+#[test]
+fn initialize_succeeds_for_pool_admin() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+
+ let stored = f.client.get_borrowing_contract(&f.pool_id);
+ assert_eq!(stored, borrowing_addr);
+}
+
+#[test]
+fn initialize_fails_for_non_admin() {
+ let env = Env::default();
+ let f = setup(&env);
+ let not_admin = Address::generate(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ let result = f.client.try_initialize_borrowing_contract(
+ &f.pool_id,
+ ¬_admin,
+ &borrowing_addr,
+ );
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+}
+
+#[test]
+fn initialize_fails_for_nonexistent_pool() {
+ let env = Env::default();
+ let f = setup(&env);
+ let fake_pool = BytesN::from_array(&env, &[99u8; 32]);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ let result = f.client.try_initialize_borrowing_contract(
+ &fake_pool,
+ &f.admin,
+ &borrowing_addr,
+ );
+ assert_eq!(result, Err(Ok(Error::PoolNotFound)));
+}
+
+#[test]
+fn initialize_immutable_after_first_call() {
+ let env = Env::default();
+ let f = setup(&env);
+ let addr1 = mock_borrowing_contract(&env);
+ let addr2 = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &addr1);
+
+ // Second initialization must be rejected.
+ let result = f.client.try_initialize_borrowing_contract(
+ &f.pool_id,
+ &f.admin,
+ &addr2,
+ );
+ assert_eq!(result, Err(Ok(Error::AlreadyInitialized)));
+
+ // The original address must be preserved.
+ let stored = f.client.get_borrowing_contract(&f.pool_id);
+ assert_eq!(stored, addr1);
+}
+
+// ===========================================================================
+// Borrow authorization
+// ===========================================================================
+
+#[test]
+fn authorized_borrowing_contract_can_borrow() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+
+ // Seed the pool with liquidity.
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ // Borrow as the authorized contract.
+ env.as_contract(&borrowing_addr, || {
+ f.client.borrow(&f.pool_id, &f.borrower, &3_000i128);
+ });
+
+ let accounting = f.client.get_pool_accounting(&f.pool_id);
+ assert_eq!(accounting.outstanding_debt, 3_000);
+ assert_eq!(accounting.available_liquidity, 7_000);
+}
+
+#[test]
+fn direct_user_borrow_rejected() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ // A direct call (not through the borrowing contract) must fail.
+ let result = f.client.try_borrow(&f.pool_id, &f.borrower, &1_000i128);
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+}
+
+#[test]
+fn borrow_rejected_when_no_contract_configured() {
+ let env = Env::default();
+ let f = setup(&env);
+
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ // No borrowing contract initialized — must be rejected.
+ let result = f.client.try_borrow(&f.pool_id, &f.borrower, &1_000i128);
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+}
+
+#[test]
+fn borrow_rejected_for_wrong_contract() {
+ let env = Env::default();
+ let f = setup(&env);
+ let correct_addr = mock_borrowing_contract(&env);
+ let wrong_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &correct_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ // Calling from the wrong contract must fail.
+ env.as_contract(&wrong_addr, || {
+ let result = f.client.try_borrow(&f.pool_id, &f.borrower, &1_000i128);
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+ });
+}
+
+// ===========================================================================
+// Repay authorization
+// ===========================================================================
+
+#[test]
+fn authorized_borrowing_contract_can_repay() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ // Borrow first.
+ env.as_contract(&borrowing_addr, || {
+ f.client.borrow(&f.pool_id, &f.borrower, &5_000i128);
+ });
+
+ // Repay as the authorized contract.
+ env.as_contract(&borrowing_addr, || {
+ f.client.repay(&f.pool_id, &f.borrower, &2_000i128, &100i128);
+ });
+
+ let accounting = f.client.get_pool_accounting(&f.pool_id);
+ assert_eq!(accounting.outstanding_debt, 3_000);
+ assert_eq!(accounting.accrued_interest, 100);
+}
+
+#[test]
+fn direct_user_repay_rejected() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ env.as_contract(&borrowing_addr, || {
+ f.client.borrow(&f.pool_id, &f.borrower, &5_000i128);
+ });
+
+ // Direct repay must fail.
+ let result = f.client.try_repay(&f.pool_id, &f.borrower, &1_000i128, &50i128);
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+}
+
+// ===========================================================================
+// update_debt authorization
+// ===========================================================================
+
+#[test]
+fn authorized_borrowing_contract_can_update_debt() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ env.as_contract(&borrowing_addr, || {
+ f.client.update_debt(&f.pool_id, &4_000i128);
+ });
+
+ let accounting = f.client.get_pool_accounting(&f.pool_id);
+ assert_eq!(accounting.outstanding_debt, 4_000);
+ assert_eq!(accounting.available_liquidity, 6_000);
+}
+
+#[test]
+fn direct_user_update_debt_rejected() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &10_000i128);
+
+ let result = f.client.try_update_debt(&f.pool_id, &1_000i128);
+ assert_eq!(result, Err(Ok(Error::Unauthorized)));
+}
+
+// ===========================================================================
+// Read-only endpoints remain public
+// ===========================================================================
+
+#[test]
+fn public_read_endpoints_unaffected() {
+ let env = Env::default();
+ let f = setup(&env);
+ let borrowing_addr = mock_borrowing_contract(&env);
+
+ f.client
+ .initialize_borrowing_contract(&f.pool_id, &f.admin, &borrowing_addr);
+ f.client.deposit(&f.pool_id, &f.lender, &5_000i128);
+
+ // All read-only calls succeed without authorization.
+ let accounting = f.client.get_pool_accounting(&f.pool_id);
+ assert_eq!(accounting.total_assets, 5_000);
+
+ let balance = f.client.get_pool_balance(&f.pool_id);
+ assert_eq!(balance, 5_000);
+
+ let shares = f.client.get_share_balance(&f.pool_id, &f.lender);
+ assert_eq!(shares.shares, 5_000);
+
+ let rate = f.client.get_interest_rate(&f.pool_id);
+ assert_eq!(rate, 500);
+
+ let stored = f.client.get_borrowing_contract(&f.pool_id);
+ assert_eq!(stored, borrowing_addr);
+
+ let status = f.client.get_pool_status(&f.pool_id);
+ assert_eq!(status, shared::types::PoolStatus::Active);
+}
diff --git a/Contract/lending/tests/test_lending_liquidity.rs b/Contract/lending/tests/test_lending_liquidity.rs
index cd36332..0ebc4fa 100644
--- a/Contract/lending/tests/test_lending_liquidity.rs
+++ b/Contract/lending/tests/test_lending_liquidity.rs
@@ -8,12 +8,16 @@ use soroban_sdk::{Address, BytesN, Env};
/// Common fixture: a freshly created pool plus the addresses used by the tests.
struct PoolFixture<'a> {
client: LendingContractClient<'a>,
+ env: &'a Env,
pool_id: BytesN<32>,
lender: Address,
borrower: Address,
+ /// The address registered as the pool's authorized borrowing contract.
+ borrowing_contract: Address,
}
-/// Register the lending contract and create a single pool.
+/// Register the lending contract and create a single pool with an authorized
+/// borrowing contract already configured.
///
/// `derive_pool_id` copies the asset bytes verbatim, so the pool id is the
/// asset id.
@@ -29,14 +33,28 @@ fn setup(env: &Env) -> PoolFixture<'_> {
// 5% APR in basis points.
client.create_pool(&admin, &asset, &500i128);
+ // Register a mock contract and authorize it as the borrowing contract.
+ let borrowing_contract = env.register_contract_wasm(None, lending::WASM);
+ client.initialize_borrowing_contract(&asset, &admin, &borrowing_contract);
+
PoolFixture {
client,
+ env,
pool_id: asset,
lender,
borrower,
+ borrowing_contract,
}
}
+/// Helper: call a lending method as the authorized borrowing contract.
+fn as_borrowing(env: &Env, borrowing_contract: &Address, f: F)
+where
+ F: FnOnce(),
+{
+ env.as_contract(borrowing_contract, f);
+}
+
/// A withdrawal that the lender has the shares for must still be rejected when
/// the pool has lent those assets out.
#[test]
@@ -46,8 +64,11 @@ fn withdraw_blocked_when_insufficient_liquidity() {
// Lender supplies 1000; first deposit mints shares 1:1.
f.client.deposit(&f.pool_id, &f.lender, &1000i128);
- // Borrower draws 800, leaving 200 of available liquidity.
- f.client.borrow(&f.pool_id, &f.borrower, &800i128);
+ // Borrower draws 800 through the authorized borrowing contract, leaving 200
+ // of available liquidity.
+ as_borrowing(&env, &f.borrowing_contract, || {
+ f.client.borrow(&f.pool_id, &f.borrower, &800i128);
+ });
let before = f.client.get_pool_accounting(&f.pool_id);
assert_eq!(before.available_liquidity, 200);
@@ -77,7 +98,9 @@ fn withdraw_succeeds_when_liquidity_sufficient() {
let f = setup(&env);
f.client.deposit(&f.pool_id, &f.lender, &1000i128);
- f.client.borrow(&f.pool_id, &f.borrower, &800i128);
+ as_borrowing(&env, &f.borrowing_contract, || {
+ f.client.borrow(&f.pool_id, &f.borrower, &800i128);
+ });
// 150 shares redeem to 150 assets, within the 200 available.
f.client.withdraw(&f.pool_id, &f.lender, &150i128);
diff --git a/Contract/shared/src/events.rs b/Contract/shared/src/events.rs
index aed1cbc..6020957 100644
--- a/Contract/shared/src/events.rs
+++ b/Contract/shared/src/events.rs
@@ -349,6 +349,25 @@ impl PoolAccountingUpdated {
)
}
}
+
+/// Event emitted when a borrowing contract is initialized for a lending pool
+#[contracttype]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BorrowingContractInitialized {
+ pub pool_id: BytesN<32>,
+ pub borrowing_contract: Address,
+ pub initialized_at: u64,
+}
+
+impl BorrowingContractInitialized {
+ pub fn topic(env: &Env) -> (BytesN<32>, BytesN<32>) {
+ (
+ BytesN::from_array(env, &[50u8; 32]),
+ BytesN::from_array(env, &[51u8; 32]),
+ )
+ }
+}
+
/// Event emitted when the rewards pool is funded
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RewardsPoolFunded {