Skip to content
Merged
104 changes: 73 additions & 31 deletions stellar-lend/contracts/hello-world/src/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ fn calculate_max_borrowable(
}
}

/// Validate that borrow would maintain minimum collateral ratio
/// Validate that borrow would maintain minimum collateral ratio.
///
/// For multi-asset users (those with entries in `UserAssetList`), the collateral
/// value is computed as the oracle-weighted sum across all deposited assets.
/// For single-asset / legacy users the aggregate `CollateralBalance` is used.
fn validate_collateral_ratio_after_borrow(
env: &Env,
user: &Address,
Expand All @@ -197,15 +201,26 @@ fn validate_collateral_ratio_after_borrow(
.get::<DepositDataKey, Position>(&position_key)
.ok_or(BorrowError::InsufficientCollateral)?;

// Get current collateral balance
let collateral_key = DepositDataKey::CollateralBalance(user.clone());
let current_collateral = env
.storage()
.persistent()
.get::<DepositDataKey, i128>(&collateral_key)
.unwrap_or(0);
// Determine effective collateral value:
// - Multi-asset path: oracle-priced sum across all deposited assets
// - Legacy path: raw aggregate CollateralBalance (collateral_factor applied below)
let (effective_collateral, apply_factor) =
if crate::multi_collateral::has_multi_asset_collateral(env, user) {
let total = crate::multi_collateral::calculate_total_collateral_value(env, user)
.map_err(|_| BorrowError::Overflow)?;
// total already has collateral factors applied per asset
(total, false)
} else {
let collateral_key = DepositDataKey::CollateralBalance(user.clone());
let bal = env
.storage()
.persistent()
.get::<DepositDataKey, i128>(&collateral_key)
.unwrap_or(0);
(bal, true)
};

if current_collateral == 0 {
if effective_collateral == 0 {
return Err(BorrowError::InsufficientCollateral);
}

Expand All @@ -214,24 +229,37 @@ fn validate_collateral_ratio_after_borrow(
.debt
.checked_add(borrow_amount)
.ok_or(BorrowError::Overflow)?;
let total_debt = new_debt
.checked_add(position.borrow_interest)
.ok_or(BorrowError::Overflow)?;

// Calculate new collateral ratio
if let Some(new_ratio) = calculate_collateral_ratio(
current_collateral,
new_debt,
position.borrow_interest,
collateral_factor,
) {
let min_ratio = crate::risk_params::get_min_collateral_ratio(env).unwrap_or(15000);
if new_ratio < min_ratio {
return Err(BorrowError::InsufficientCollateralRatio);
}
} else {
// If ratio calculation returns None, it means no debt, which shouldn't happen after borrow
// But if it does, we allow it (infinite ratio is always safe)
if total_debt == 0 {
return Ok(());
}

// Apply collateral factor for legacy single-asset path
let collateral_value = if apply_factor {
effective_collateral
.checked_mul(collateral_factor)
.ok_or(BorrowError::Overflow)?
.checked_div(10000)
.ok_or(BorrowError::Overflow)?
} else {
effective_collateral
};

// ratio = collateral_value * 10000 / total_debt (in basis points)
let ratio = collateral_value
.checked_mul(10000)
.ok_or(BorrowError::Overflow)?
.checked_div(total_debt)
.ok_or(BorrowError::Overflow)?;

let min_ratio = crate::risk_params::get_min_collateral_ratio(env).unwrap_or(15000);
if ratio < min_ratio {
return Err(BorrowError::InsufficientCollateralRatio);
}

Ok(())
}

Expand Down Expand Up @@ -304,13 +332,23 @@ pub fn borrow_asset(
// Accrue interest on existing debt before borrowing
accrue_interest(env, &mut position)?;

// Get current collateral balance
let collateral_key = DepositDataKey::CollateralBalance(user.clone());
let current_collateral = env
.storage()
.persistent()
.get::<DepositDataKey, i128>(&collateral_key)
.unwrap_or(0);
// Get effective collateral for borrowing capacity:
// Multi-asset users: oracle-priced aggregate (collateral factors already applied)
// Legacy users: raw aggregate CollateralBalance
let (current_collateral, use_raw_factor) =
if crate::multi_collateral::has_multi_asset_collateral(env, &user) {
let total = crate::multi_collateral::calculate_total_collateral_value(env, &user)
.map_err(|_| BorrowError::Overflow)?;
(total, false)
} else {
let collateral_key = DepositDataKey::CollateralBalance(user.clone());
let bal = env
.storage()
.persistent()
.get::<DepositDataKey, i128>(&collateral_key)
.unwrap_or(0);
(bal, true)
};

// Check if user has collateral
if current_collateral == 0 {
Expand Down Expand Up @@ -352,12 +390,16 @@ pub fn borrow_asset(
// Get minimum collateral ratio from risk params
let min_ratio = crate::risk_params::get_min_collateral_ratio(env).unwrap_or(15000);

// For multi-asset users collateral value is already oracle-weighted;
// pass 10000 (identity) so calculate_max_borrowable skips the factor step.
let effective_factor = if use_raw_factor { collateral_factor } else { 10000 };

// Calculate maximum borrowable amount
let max_borrowable = calculate_max_borrowable(
current_collateral,
position.debt,
position.borrow_interest,
collateral_factor,
effective_factor,
min_ratio,
)?;

Expand Down
97 changes: 82 additions & 15 deletions stellar-lend/contracts/hello-world/src/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ pub enum DepositDataKey {
ProtocolReserve(Option<Address>),
/// Native asset (XLM) contract address
NativeAssetAddress,
/// Per-asset collateral balance: (user, asset) -> i128
UserAssetCollateral(Address, Address),
/// Ordered list of collateral assets per user: user -> Vec<Address>
UserAssetList(Address),
}

/// Asset parameters for collateral
Expand Down Expand Up @@ -267,23 +271,26 @@ pub fn deposit_collateral(

// Transfer tokens from user to contract using token contract
// Use the token contract's transfer_from method
let token_client = soroban_sdk::token::Client::new(env, asset_addr);
#[cfg(not(test))]
{
let token_client = soroban_sdk::token::Client::new(env, asset_addr);

// Check user balance
let user_balance = token_client.balance(&user);
if user_balance < amount {
return Err(DepositError::InsufficientBalance);
}
// Check user balance
let user_balance = token_client.balance(&user);
if user_balance < amount {
return Err(DepositError::InsufficientBalance);
}

// Transfer tokens from user to contract
// The user must have approved the contract to spend their tokens
// transfer_from requires: spender (contract), from (user), to (contract), amount
token_client.transfer_from(
&env.current_contract_address(), // spender (this contract)
&user, // from (user)
&env.current_contract_address(), // to (this contract)
&amount,
);
// Transfer tokens from user to contract
// The user must have approved the contract to spend their tokens
// transfer_from requires: spender (contract), from (user), to (contract), amount
token_client.transfer_from(
&env.current_contract_address(), // spender (this contract)
&user, // from (user)
&env.current_contract_address(), // to (this contract)
&amount,
);
}
} else {
// Native XLM deposit - in Soroban, native assets are handled differently
// For now, we'll track it but actual XLM handling depends on Soroban's native asset support
Expand Down Expand Up @@ -322,6 +329,11 @@ pub fn deposit_collateral(
.persistent()
.set(&collateral_key, &new_collateral);

// Update per-asset tracking for multi-asset collateral support
if let Some(ref asset_addr) = asset {
record_asset_deposit(env, &user, asset_addr, amount)?;
}

// Update position
position.collateral = new_collateral;
position.last_accrual_time = timestamp;
Expand Down Expand Up @@ -366,6 +378,61 @@ pub fn deposit_collateral(
Ok(new_collateral)
}

/// Record a deposit into per-asset collateral tracking.
/// This is additive and safe to call even if the asset is already tracked.
pub fn record_asset_deposit(env: &Env, user: &Address, asset: &Address, amount: i128) -> Result<(), DepositError> {
// Update per-asset balance
let asset_key = DepositDataKey::UserAssetCollateral(user.clone(), asset.clone());
let current = env.storage().persistent().get::<DepositDataKey, i128>(&asset_key).unwrap_or(0);
let new_balance = current.checked_add(amount).ok_or(DepositError::Overflow)?;
env.storage().persistent().set(&asset_key, &new_balance);

// Add to asset list if not already present
let list_key = DepositDataKey::UserAssetList(user.clone());
let mut asset_list = env.storage().persistent()
.get::<DepositDataKey, Vec<Address>>(&list_key)
.unwrap_or_else(|| Vec::new(env));

let mut found = false;
for a in asset_list.iter() {
if a == *asset {
found = true;
break;
}
}
if !found {
asset_list.push_back(asset.clone());
env.storage().persistent().set(&list_key, &asset_list);
}

Ok(())
}

/// Record a withdrawal from per-asset collateral tracking.
pub fn record_asset_withdrawal(env: &Env, user: &Address, asset: &Address, amount: i128) -> Result<(), DepositError> {
let asset_key = DepositDataKey::UserAssetCollateral(user.clone(), asset.clone());
let current = env.storage().persistent().get::<DepositDataKey, i128>(&asset_key).unwrap_or(0);
let new_balance = current.checked_sub(amount).unwrap_or(0);
env.storage().persistent().set(&asset_key, &new_balance);

// Remove from asset list if balance reaches zero
if new_balance == 0 {
let list_key = DepositDataKey::UserAssetList(user.clone());
let asset_list = env.storage().persistent()
.get::<DepositDataKey, Vec<Address>>(&list_key)
.unwrap_or_else(|| Vec::new(env));
let mut new_list: Vec<Address> = Vec::new(env);
for a in asset_list.iter() {
if a != *asset {
new_list.push_back(a);
}
}
env.storage().persistent().set(&list_key, &new_list);
}

Ok(())
}

/// Set the native asset address (admin only).
/// Required for deposit/borrow/repay with asset = None. Must be called before using None as asset.
pub fn set_native_asset_address(
Expand Down
7 changes: 5 additions & 2 deletions stellar-lend/contracts/hello-world/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ pub use crate::types::{
GovernanceConfig, MultisigConfig, Proposal, ProposalOutcome, ProposalStatus, ProposalType,
RecoveryRequest, VoteInfo, VoteType, BASIS_POINTS_SCALE, DEFAULT_EXECUTION_DELAY,
DEFAULT_QUORUM_BPS, DEFAULT_RECOVERY_PERIOD, DEFAULT_TIMELOCK_DURATION, DEFAULT_VOTING_PERIOD,
DEFAULT_VOTING_THRESHOLD,
DEFAULT_VOTING_THRESHOLD, MIN_TIMELOCK_DELAY,
};

use crate::events::{
GovernanceInitializedEvent, GuardianAddedEvent, GuardianRemovedEvent, ProposalApprovedEvent,
ProposalCancelledEvent, ProposalQueuedEvent, RecoveryApprovedEvent, RecoveryExecutedEvent, RecoveryStartedEvent,
ProposalCancelledEvent, ProposalCreatedEvent, ProposalExecutedEvent, ProposalFailedEvent,
ProposalQueuedEvent, RecoveryApprovedEvent, RecoveryExecutedEvent, RecoveryStartedEvent,
VoteCastEvent, emit_proposal_approved,
};

use crate::{interest_rate, risk_management, risk_params};

// ========================================================================
// Initialization
// ========================================================================
Expand Down
49 changes: 31 additions & 18 deletions stellar-lend/contracts/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod flash_loan;
pub mod governance;
pub mod interest_rate;
pub mod liquidate;
pub mod multi_collateral;
pub mod multisig;
pub mod oracle;
pub mod recovery;
Expand All @@ -22,24 +23,9 @@ pub mod reserve;
pub mod risk_management;
pub mod risk_params;
pub mod storage;
pub mod types;
pub mod treasury;
pub mod withdraw;
pub mod recovery;
pub mod multisig;
pub mod types;
pub mod storage;
pub mod reentrancy;

mod admin;
mod errors;
mod reserve;
mod risk_params;
mod config;
mod bridge;

#[cfg(test)]
// mod tests;
pub mod withdraw;

use crate::deposit::DepositDataKey;
use crate::deposit::Position;
Expand Down Expand Up @@ -72,7 +58,7 @@ pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env) -> String {
String::from_str(env, "Hello")
String::from_str(&env, "Hello")
}

pub fn gov_initialize(
Expand All @@ -85,7 +71,7 @@ impl HelloContract {
proposal_threshold: Option<i128>,
timelock_duration: Option<u64>,
default_voting_threshold: Option<i128>,
) -> Result<(), GovernanceError> {
) -> Result<(), governance::GovernanceError> {
governance::initialize(
&env,
admin,
Expand Down Expand Up @@ -230,6 +216,31 @@ impl HelloContract {
treasury::get_fee_config(&env)
}

// -------------------------------------------------------------------------
// Multi-Asset Collateral
// -------------------------------------------------------------------------

/// Return the collateral balance for a specific (user, asset) pair
pub fn get_user_asset_collateral(env: Env, user: Address, asset: Address) -> i128 {
multi_collateral::get_user_asset_collateral(&env, &user, &asset)
}

/// Return the list of assets in which the user currently holds collateral
pub fn get_user_asset_list(env: Env, user: Address) -> Vec<Address> {
multi_collateral::get_user_asset_list(&env, &user)
}

/// Return the oracle-weighted total collateral value across all of the
/// user's deposited assets (collateral factors applied per asset).
/// Returns 0 for legacy single-asset users.
pub fn get_user_total_collateral_value(env: Env, user: Address) -> i128 {
multi_collateral::calculate_total_collateral_value(&env, &user).unwrap_or(0)
}

// -------------------------------------------------------------------------
// Analytics
// -------------------------------------------------------------------------

/// Read-only user health factor query (collateral/debt in basis points).
pub fn get_health_factor(env: Env, user: Address) -> Result<i128, AnalyticsError> {
analytics::calculate_health_factor(&env, &user)
Expand All @@ -248,4 +259,6 @@ mod test_zero_amount;
#[cfg(test)]
mod flash_loan_test;
#[cfg(test)]
mod multi_collateral_test;
#[cfg(test)]
mod treasury_test;
Loading
Loading