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
1 change: 1 addition & 0 deletions .freebuff/project-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5fc15649-80f3-41ea-86a7-a1c5e24c5382
27 changes: 27 additions & 0 deletions Contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
32 changes: 32 additions & 0 deletions Contract/lending/src/authorization.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
80 changes: 78 additions & 2 deletions Contract/lending/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
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,
RateLimit, Role, ShareBalance,
};
use shared::utils::{FixedMath, SafeMath, TimeHelper, ValidationHelper};

mod authorization;
mod state;

/// Storage keys for lending contract
#[derive(Clone)]
#[contracttype]
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Address, Error> {
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);
Expand Down
31 changes: 31 additions & 0 deletions Contract/lending/src/state.rs
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading