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
9 changes: 8 additions & 1 deletion stellar-lend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions stellar-lend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"benchmarks",
"client",
"packages/oracle-client",
"contracts/amm",
"contracts/bridge",
"contracts/common",
Expand Down
2 changes: 1 addition & 1 deletion stellar-lend/contracts/hello-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ doctest = false
soroban-sdk = { workspace = true }
soroban-token-sdk = { workspace = true }
stellarlend-amm = { path = "../amm" }
stellarlend-shared-deadline = { path = "../shared-deadline" }
stellarlend-oracle-client = { path = "../../packages/oracle-client" }
stellar-contract-utils = { version = "0.6.0" }
stellar-macros = { version = "0.6.0" }

Expand Down
85 changes: 4 additions & 81 deletions stellar-lend/contracts/hello-world/src/cross_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ pub enum CrossAssetError {
const ADMIN: Symbol = symbol_short!("admin");

/// Storage key for the map of asset configurations: Map<AssetKey, AssetConfig>
const ASSET_CONFIGS: Symbol = symbol_short!("configs");
pub(crate) const ASSET_CONFIGS: Symbol = symbol_short!("configs");

/// Storage key for the map of user positions: Map<UserAssetKey, AssetPosition>
const USER_POSITIONS: Symbol = symbol_short!("positions");
pub(crate) const USER_POSITIONS: Symbol = symbol_short!("positions");

/// Storage key for the map of total supplies per asset: Map<AssetKey, i128>
const TOTAL_SUPPLIES: Symbol = symbol_short!("supplies");
Expand All @@ -167,7 +167,7 @@ const TOTAL_SUPPLIES: Symbol = symbol_short!("supplies");
const TOTAL_BORROWS: Symbol = symbol_short!("borrows");

/// Storage key for the global list of registered assets: Vec<AssetKey>
const ASSET_LIST: Symbol = symbol_short!("assets");
pub(crate) const ASSET_LIST: Symbol = symbol_short!("assets");

/// Initialize the cross-asset lending module.
///
Expand Down Expand Up @@ -457,84 +457,7 @@ pub fn get_user_position_summary(
env: &Env,
user: &Address,
) -> Result<UserPositionSummary, CrossAssetError> {
let asset_list: Vec<AssetKey> = env
.storage()
.persistent()
.get(&ASSET_LIST)
.unwrap_or(Vec::new(env));

let configs: Map<AssetKey, AssetConfig> = env
.storage()
.persistent()
.get(&ASSET_CONFIGS)
.unwrap_or(Map::new(env));

let mut total_collateral_value: i128 = 0;
let mut weighted_collateral_value: i128 = 0;
let mut total_debt_value: i128 = 0;
let mut weighted_debt_value: i128 = 0;

for i in 0..asset_list.len() {
let asset_key = asset_list.get(i).unwrap();

if let Some(config) = configs.get(asset_key.clone()) {
let asset_option = asset_key.to_option();
let position = get_user_asset_position(env, user, asset_option);

if position.collateral == 0 && position.debt_principal == 0 {
continue;
}

let current_time = env.ledger().timestamp();
if current_time > config.price_updated_at
&& current_time - config.price_updated_at > 3600
{
return Err(CrossAssetError::PriceStale);
}

let collateral_value = (position.collateral * config.price) / 10_000_000;
total_collateral_value += collateral_value;

if config.can_collateralize {
weighted_collateral_value +=
(collateral_value * config.liquidation_threshold) / 10_000;
}

let total_debt = position.debt_principal + position.accrued_interest;
let debt_value = (total_debt * config.price) / 10_000_000;
total_debt_value += debt_value;

weighted_debt_value += debt_value;
}
}

// Calculate health factor (weighted_collateral / weighted_debt * 10000)
// Health factor of 1.0 = 10000, below 1.0 can be liquidated
let health_factor = if weighted_debt_value > 0 {
(weighted_collateral_value * 10_000) / weighted_debt_value
} else {
i128::MAX // No debt = infinite health
};

// Position is liquidatable if health factor < 1.0 (10000)
let is_liquidatable = health_factor < 10_000 && weighted_debt_value > 0;

// Calculate remaining borrow capacity
let borrow_capacity = if weighted_collateral_value > weighted_debt_value {
weighted_collateral_value - weighted_debt_value
} else {
0
};

Ok(UserPositionSummary {
total_collateral_value,
weighted_collateral_value,
total_debt_value,
weighted_debt_value,
health_factor,
is_liquidatable,
borrow_capacity,
})
crate::health::get_batched_user_position_summary(env, user)
}

/// Deposit collateral for a specific asset.
Expand Down
136 changes: 136 additions & 0 deletions stellar-lend/contracts/hello-world/src/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use soroban_sdk::{contracttype, Address, Env, Map, Vec};

use crate::cross_asset::{
AssetConfig, AssetKey, AssetPosition, CrossAssetError, UserAssetKey, UserPositionSummary,
ASSET_CONFIGS, ASSET_LIST, USER_POSITIONS,
};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BatchedPositionData {
pub asset: AssetKey,
pub config: AssetConfig,
pub position: AssetPosition,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HealthBatch {
pub positions: Vec<BatchedPositionData>,
pub storage_reads: u32,
pub price_reads: u32,
}

pub fn batch_read_health_data(env: &Env, user: &Address) -> HealthBatch {
let asset_list: Vec<AssetKey> = env
.storage()
.persistent()
.get(&ASSET_LIST)
.unwrap_or(Vec::new(env));
let configs: Map<AssetKey, AssetConfig> = env
.storage()
.persistent()
.get(&ASSET_CONFIGS)
.unwrap_or(Map::new(env));
let user_positions: Map<UserAssetKey, AssetPosition> = env
.storage()
.persistent()
.get(&USER_POSITIONS)
.unwrap_or(Map::new(env));

let mut positions = Vec::new(env);
let mut storage_reads = 3u32;
let mut price_reads = 0u32;

for i in 0..asset_list.len() {
let asset = asset_list.get(i).unwrap();
if let Some(config) = configs.get(asset.clone()) {
let position = user_positions
.get(UserAssetKey::new(user.clone(), asset.clone().to_option()))
.unwrap_or(AssetPosition {
collateral: 0,
debt_principal: 0,
accrued_interest: 0,
last_updated: env.ledger().timestamp(),
});

if position.collateral != 0 || position.debt_principal != 0 {
price_reads += 1;
positions.push_back(BatchedPositionData {
asset,
config,
position,
});
}
}
}

HealthBatch {
positions,
storage_reads,
price_reads,
}
}

pub fn calculate_health_from_batch(
env: &Env,
batch: &HealthBatch,
) -> Result<UserPositionSummary, CrossAssetError> {
let mut total_collateral_value: i128 = 0;
let mut weighted_collateral_value: i128 = 0;
let mut total_debt_value: i128 = 0;
let mut weighted_debt_value: i128 = 0;

for i in 0..batch.positions.len() {
let item = batch.positions.get(i).unwrap();
let current_time = env.ledger().timestamp();
if current_time > item.config.price_updated_at
&& current_time - item.config.price_updated_at > 3600
{
return Err(CrossAssetError::PriceStale);
}

let collateral_value = (item.position.collateral * item.config.price) / 10_000_000;
total_collateral_value += collateral_value;

if item.config.can_collateralize {
weighted_collateral_value +=
(collateral_value * item.config.liquidation_threshold) / 10_000;
}

let total_debt = item.position.debt_principal + item.position.accrued_interest;
let debt_value = (total_debt * item.config.price) / 10_000_000;
total_debt_value += debt_value;
weighted_debt_value += debt_value;
}

let health_factor = if weighted_debt_value > 0 {
(weighted_collateral_value * 10_000) / weighted_debt_value
} else {
i128::MAX
};
let is_liquidatable = health_factor < 10_000 && weighted_debt_value > 0;
let borrow_capacity = if weighted_collateral_value > weighted_debt_value {
weighted_collateral_value - weighted_debt_value
} else {
0
};

Ok(UserPositionSummary {
total_collateral_value,
weighted_collateral_value,
total_debt_value,
weighted_debt_value,
health_factor,
is_liquidatable,
borrow_capacity,
})
}

pub fn get_batched_user_position_summary(
env: &Env,
user: &Address,
) -> Result<UserPositionSummary, CrossAssetError> {
let batch = batch_read_health_data(env, user);
calculate_health_from_batch(env, &batch)
}
18 changes: 18 additions & 0 deletions stellar-lend/contracts/hello-world/src/interest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use soroban_sdk::Env;

use crate::interest_rate::InterestRateError;
use crate::traits::InterestModel;

pub struct DefaultInterestModel;

impl InterestModel for DefaultInterestModel {
type Error = InterestRateError;

fn utilization(&self, env: &Env) -> Result<i128, Self::Error> {
crate::interest_rate::calculate_utilization(env)
}

fn borrow_rate(&self, env: &Env) -> Result<i128, Self::Error> {
crate::interest_rate::calculate_borrow_rate(env)
}
}
64 changes: 64 additions & 0 deletions stellar-lend/contracts/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ pub mod errors;
pub mod events;
pub mod flash_loan;
pub mod governance;
pub mod health;
pub mod intents;
pub mod interest;
pub mod interest_rate;
pub mod liquidate;
pub mod liquidation;
pub mod liquidation_queue;
pub mod mev_protection;
pub mod multi_collateral;
Expand All @@ -32,12 +35,14 @@ pub mod recovery;
pub mod reentrancy;
pub mod repay;
pub mod reserve;
pub mod reserve_factor;
pub mod risk_management;
pub mod risk_params;
pub mod safe_math;
pub mod storage;
pub mod timelock;
pub mod treasury;
pub mod traits;
pub mod test_utils;
pub mod tests;
pub mod types;
Expand Down Expand Up @@ -434,6 +439,65 @@ impl HelloContract {
reserve::set_reserve_factor(&env, caller, asset, reserve_factor_bps).map_err(Into::into)
}

pub fn set_reserve_factor_curve(
env: Env,
caller: Address,
asset: Option<Address>,
curve: reserve_factor::ReserveFactorCurve,
) -> Result<(), LendingError> {
reserve_factor::set_reserve_factor_curve(&env, caller, asset, curve).map_err(Into::into)
}

pub fn set_reserve_factor_bounds(
env: Env,
caller: Address,
asset: Option<Address>,
min_bps: i128,
max_bps: i128,
) -> Result<(), LendingError> {
reserve_factor::set_reserve_factor_bounds(&env, caller, asset, min_bps, max_bps)
.map_err(Into::into)
}

pub fn get_reserve_factor_curve(
env: Env,
asset: Option<Address>,
) -> reserve_factor::ReserveFactorCurve {
reserve_factor::get_reserve_factor_curve(&env, asset)
}

pub fn preview_reserve_factor(
env: Env,
asset: Option<Address>,
utilization_bps: Option<i128>,
) -> Result<reserve_factor::ReserveFactorPreview, LendingError> {
reserve_factor::preview_reserve_factor(&env, asset, utilization_bps).map_err(Into::into)
}

pub fn get_health_factor_batch(
env: Env,
user: Address,
) -> health::HealthBatch {
health::batch_read_health_data(&env, &user)
}

pub fn get_batched_health_summary(
env: Env,
user: Address,
) -> Result<cross_asset::UserPositionSummary, LendingError> {
health::get_batched_user_position_summary(&env, &user).map_err(Into::into)
}

pub fn registered_pool_modules(env: Env) -> (String, String, String, String) {
let modules = traits::registered_modules();
(
String::from_str(&env, modules.0),
String::from_str(&env, modules.1),
String::from_str(&env, modules.2),
String::from_str(&env, modules.3),
)
}

/// Set treasury address (admin only)
pub fn set_treasury_address(
env: Env,
Expand Down
Loading
Loading