use crate::error::{CoreError, Result};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollateralConfig {
pub symbol: String,
pub collateral_factor: Decimal,
pub liquidation_threshold: Decimal,
pub haircut: Decimal,
pub min_deposit: Decimal,
pub max_deposit: Option<Decimal>,
pub enabled: bool,
pub category: AssetCategory,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AssetCategory {
Stablecoin,
MajorCrypto,
Altcoin,
PlatformToken,
LiquidityToken,
}
impl AssetCategory {
pub fn default_collateral_factor(&self) -> Decimal {
match self {
AssetCategory::Stablecoin => dec!(0.95),
AssetCategory::MajorCrypto => dec!(0.80),
AssetCategory::Altcoin => dec!(0.60),
AssetCategory::PlatformToken => dec!(0.70),
AssetCategory::LiquidityToken => dec!(0.50),
}
}
pub fn default_liquidation_threshold(&self) -> Decimal {
match self {
AssetCategory::Stablecoin => dec!(0.98),
AssetCategory::MajorCrypto => dec!(0.85),
AssetCategory::Altcoin => dec!(0.75),
AssetCategory::PlatformToken => dec!(0.80),
AssetCategory::LiquidityToken => dec!(0.70),
}
}
pub fn default_haircut(&self) -> Decimal {
match self {
AssetCategory::Stablecoin => dec!(0.02), AssetCategory::MajorCrypto => dec!(0.10), AssetCategory::Altcoin => dec!(0.20), AssetCategory::PlatformToken => dec!(0.15), AssetCategory::LiquidityToken => dec!(0.25), }
}
}
impl CollateralConfig {
pub fn new(symbol: String, category: AssetCategory) -> Self {
Self {
symbol,
collateral_factor: category.default_collateral_factor(),
liquidation_threshold: category.default_liquidation_threshold(),
haircut: category.default_haircut(),
min_deposit: dec!(10),
max_deposit: None,
enabled: true,
category,
updated_at: Utc::now(),
}
}
pub fn effective_value(&self, amount: Decimal, price: Decimal) -> Decimal {
let gross_value = amount * price;
gross_value * (Decimal::ONE - self.haircut)
}
pub fn borrowing_power(&self, amount: Decimal, price: Decimal) -> Decimal {
let effective_value = self.effective_value(amount, price);
effective_value * self.collateral_factor
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollateralAssetDeposit {
pub deposit_id: String,
pub user_id: String,
pub symbol: String,
pub amount: Decimal,
pub deposit_price: Decimal,
pub current_price: Decimal,
pub locked: bool,
pub lock_expiry: Option<DateTime<Utc>>,
pub deposited_at: DateTime<Utc>,
}
impl CollateralAssetDeposit {
pub fn new(
deposit_id: String,
user_id: String,
symbol: String,
amount: Decimal,
price: Decimal,
) -> Self {
Self {
deposit_id,
user_id,
symbol,
amount,
deposit_price: price,
current_price: price,
locked: false,
lock_expiry: None,
deposited_at: Utc::now(),
}
}
pub fn current_value(&self) -> Decimal {
self.amount * self.current_price
}
pub fn unrealized_pnl(&self) -> Decimal {
self.amount * (self.current_price - self.deposit_price)
}
pub fn update_price(&mut self, price: Decimal) {
self.current_price = price;
}
pub fn is_locked(&self) -> bool {
if !self.locked {
return false;
}
if let Some(expiry) = self.lock_expiry {
Utc::now() < expiry
} else {
true
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollateralPortfolio {
pub user_id: String,
pub deposits: Vec<CollateralAssetDeposit>,
pub total_borrowed: Decimal,
pub updated_at: DateTime<Utc>,
}
impl CollateralPortfolio {
pub fn new(user_id: String) -> Self {
Self {
user_id,
deposits: Vec::new(),
total_borrowed: Decimal::ZERO,
updated_at: Utc::now(),
}
}
pub fn add_deposit(&mut self, deposit: CollateralAssetDeposit) {
self.deposits.push(deposit);
self.updated_at = Utc::now();
}
pub fn remove_deposit(&mut self, deposit_id: &str) -> Result<CollateralAssetDeposit> {
let idx = self
.deposits
.iter()
.position(|d| d.deposit_id == deposit_id)
.ok_or_else(|| CoreError::NotFound(format!("Deposit {} not found", deposit_id)))?;
let deposit = self.deposits.remove(idx);
if deposit.is_locked() {
return Err(CoreError::Validation(
"Cannot withdraw locked collateral".to_string(),
));
}
self.updated_at = Utc::now();
Ok(deposit)
}
pub fn total_collateral_value(&self) -> Decimal {
self.deposits.iter().map(|d| d.current_value()).sum()
}
pub fn effective_collateral_value(
&self,
configs: &HashMap<String, CollateralConfig>,
) -> Decimal {
self.deposits
.iter()
.filter_map(|d| {
configs
.get(&d.symbol)
.map(|config| config.effective_value(d.amount, d.current_price))
})
.sum()
}
pub fn total_borrowing_power(&self, configs: &HashMap<String, CollateralConfig>) -> Decimal {
self.deposits
.iter()
.filter_map(|d| {
configs
.get(&d.symbol)
.map(|config| config.borrowing_power(d.amount, d.current_price))
})
.sum()
}
pub fn available_borrowing_power(
&self,
configs: &HashMap<String, CollateralConfig>,
) -> Decimal {
let total_power = self.total_borrowing_power(configs);
(total_power - self.total_borrowed).max(Decimal::ZERO)
}
pub fn health_factor(&self, configs: &HashMap<String, CollateralConfig>) -> Decimal {
if self.total_borrowed.is_zero() {
return Decimal::MAX;
}
let liquidation_value: Decimal = self
.deposits
.iter()
.filter_map(|d| {
configs.get(&d.symbol).map(|config| {
let value = d.current_value();
value * config.liquidation_threshold
})
})
.sum();
liquidation_value / self.total_borrowed
}
pub fn is_healthy(&self, configs: &HashMap<String, CollateralConfig>) -> bool {
self.health_factor(configs) > Decimal::ONE
}
pub fn update_prices(&mut self, prices: &HashMap<String, Decimal>) {
for deposit in &mut self.deposits {
if let Some(&price) = prices.get(&deposit.symbol) {
deposit.update_price(price);
}
}
self.updated_at = Utc::now();
}
pub fn add_borrowing(&mut self, amount: Decimal) {
self.total_borrowed += amount;
self.updated_at = Utc::now();
}
pub fn repay_borrowing(&mut self, amount: Decimal) {
self.total_borrowed = (self.total_borrowed - amount).max(Decimal::ZERO);
self.updated_at = Utc::now();
}
pub fn breakdown(&self) -> HashMap<String, Decimal> {
let mut breakdown = HashMap::new();
for deposit in &self.deposits {
*breakdown
.entry(deposit.symbol.clone())
.or_insert(Decimal::ZERO) += deposit.current_value();
}
breakdown
}
}
pub struct CollateralManager {
configs: HashMap<String, CollateralConfig>,
portfolios: HashMap<String, CollateralPortfolio>,
}
impl CollateralManager {
pub fn new() -> Self {
Self {
configs: HashMap::new(),
portfolios: HashMap::new(),
}
}
pub fn add_config(&mut self, config: CollateralConfig) {
self.configs.insert(config.symbol.clone(), config);
}
pub fn get_config(&self, symbol: &str) -> Option<&CollateralConfig> {
self.configs.get(symbol)
}
pub fn get_or_create_portfolio(&mut self, user_id: &str) -> &mut CollateralPortfolio {
self.portfolios
.entry(user_id.to_string())
.or_insert_with(|| CollateralPortfolio::new(user_id.to_string()))
}
pub fn get_portfolio(&self, user_id: &str) -> Option<&CollateralPortfolio> {
self.portfolios.get(user_id)
}
pub fn deposit_collateral(&mut self, deposit: CollateralAssetDeposit) -> Result<()> {
let config = self.configs.get(&deposit.symbol).ok_or_else(|| {
CoreError::NotFound(format!(
"Collateral config for {} not found",
deposit.symbol
))
})?;
if !config.enabled {
return Err(CoreError::Validation(format!(
"{} is not enabled as collateral",
deposit.symbol
)));
}
if deposit.amount < config.min_deposit {
return Err(CoreError::Validation(format!(
"Deposit amount {} below minimum {}",
deposit.amount, config.min_deposit
)));
}
if let Some(max) = config.max_deposit {
if deposit.amount > max {
return Err(CoreError::Validation(format!(
"Deposit amount {} exceeds maximum {}",
deposit.amount, max
)));
}
}
let portfolio = self.get_or_create_portfolio(&deposit.user_id);
portfolio.add_deposit(deposit);
Ok(())
}
pub fn withdraw_collateral(
&mut self,
user_id: &str,
deposit_id: &str,
) -> Result<CollateralAssetDeposit> {
let portfolio = self
.portfolios
.get_mut(user_id)
.ok_or_else(|| CoreError::NotFound(format!("Portfolio for {} not found", user_id)))?;
let deposit = portfolio.remove_deposit(deposit_id)?;
if !portfolio.is_healthy(&self.configs) {
portfolio.add_deposit(deposit.clone());
return Err(CoreError::Validation(
"Withdrawal would make portfolio unhealthy".to_string(),
));
}
Ok(deposit)
}
pub fn update_prices(&mut self, prices: &HashMap<String, Decimal>) {
for portfolio in self.portfolios.values_mut() {
portfolio.update_prices(prices);
}
}
pub fn get_unhealthy_portfolios(&self) -> Vec<String> {
self.portfolios
.iter()
.filter(|(_, portfolio)| !portfolio.is_healthy(&self.configs))
.map(|(user_id, _)| user_id.clone())
.collect()
}
pub fn total_value_locked(&self) -> Decimal {
self.portfolios
.values()
.map(|p| p.total_collateral_value())
.sum()
}
}
impl Default for CollateralManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_asset_category_defaults() {
assert_eq!(
AssetCategory::Stablecoin.default_collateral_factor(),
dec!(0.95)
);
assert_eq!(
AssetCategory::MajorCrypto.default_collateral_factor(),
dec!(0.80)
);
}
#[test]
fn test_collateral_config() {
let config = CollateralConfig::new("BTC".to_string(), AssetCategory::MajorCrypto);
let effective_value = config.effective_value(dec!(1), dec!(50000));
assert_eq!(effective_value, dec!(45000));
let borrowing_power = config.borrowing_power(dec!(1), dec!(50000));
assert_eq!(borrowing_power, dec!(36000)); }
#[test]
fn test_collateral_deposit() {
let deposit = CollateralAssetDeposit::new(
"dep1".to_string(),
"user1".to_string(),
"BTC".to_string(),
dec!(10),
dec!(50000),
);
assert_eq!(deposit.current_value(), dec!(500000));
assert_eq!(deposit.unrealized_pnl(), Decimal::ZERO);
}
#[test]
fn test_collateral_portfolio() {
let mut portfolio = CollateralPortfolio::new("user1".to_string());
let deposit = CollateralAssetDeposit::new(
"dep1".to_string(),
"user1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(50000),
);
portfolio.add_deposit(deposit);
assert_eq!(portfolio.total_collateral_value(), dec!(50000));
}
#[test]
fn test_health_factor() {
let mut portfolio = CollateralPortfolio::new("user1".to_string());
let deposit = CollateralAssetDeposit::new(
"dep1".to_string(),
"user1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(50000),
);
portfolio.add_deposit(deposit);
portfolio.add_borrowing(dec!(30000));
let mut configs = HashMap::new();
configs.insert(
"BTC".to_string(),
CollateralConfig::new("BTC".to_string(), AssetCategory::MajorCrypto),
);
let health = portfolio.health_factor(&configs);
assert!(health > Decimal::ONE);
assert!(portfolio.is_healthy(&configs));
}
#[test]
fn test_collateral_manager() {
let mut manager = CollateralManager::new();
manager.add_config(CollateralConfig::new(
"BTC".to_string(),
AssetCategory::MajorCrypto,
));
let deposit = CollateralAssetDeposit::new(
"dep1".to_string(),
"user1".to_string(),
"BTC".to_string(),
dec!(10), dec!(50000),
);
manager.deposit_collateral(deposit).unwrap();
let portfolio = manager.get_portfolio("user1").unwrap();
assert_eq!(portfolio.deposits.len(), 1);
}
}