use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidStakingPool {
pub id: Uuid,
pub asset_token_id: Uuid,
pub st_token_id: Uuid,
pub total_staked: Decimal,
pub st_token_supply: Decimal,
pub accumulated_rewards: Decimal,
pub exchange_rate: Decimal,
pub apy: Decimal,
pub last_reward_time: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub slashing_events: Vec<SlashingEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlashingEvent {
pub id: Uuid,
pub amount: Decimal,
pub reason: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidStakingPosition {
pub id: Uuid,
pub pool_id: Uuid,
pub user_id: Uuid,
pub st_token_amount: Decimal,
pub original_stake: Decimal,
pub staked_at: DateTime<Utc>,
pub last_claimed_at: Option<DateTime<Utc>>,
pub total_rewards_earned: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedemptionRequest {
pub id: Uuid,
pub pool_id: Uuid,
pub user_id: Uuid,
pub st_token_amount: Decimal,
pub expected_amount: Decimal,
pub requested_at: DateTime<Utc>,
pub unlock_at: DateTime<Utc>,
pub status: RedemptionStatus,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RedemptionStatus {
Pending,
ReadyToClaim,
Claimed,
Cancelled,
}
impl LiquidStakingPool {
pub fn new(asset_token_id: Uuid, st_token_id: Uuid, initial_apy: Decimal) -> Self {
Self {
id: Uuid::new_v4(),
asset_token_id,
st_token_id,
total_staked: Decimal::ZERO,
st_token_supply: Decimal::ZERO,
accumulated_rewards: Decimal::ZERO,
exchange_rate: dec!(1.0), apy: initial_apy,
last_reward_time: Utc::now(),
created_at: Utc::now(),
slashing_events: Vec::new(),
}
}
pub fn stake(&mut self, user_id: Uuid, amount: Decimal) -> Result<LiquidStakingPosition> {
if amount <= Decimal::ZERO {
return Err(CoreError::Validation(
"Stake amount must be positive".to_string(),
));
}
let st_tokens_to_mint = amount / self.exchange_rate;
self.total_staked += amount;
self.st_token_supply += st_tokens_to_mint;
Ok(LiquidStakingPosition {
id: Uuid::new_v4(),
pool_id: self.id,
user_id,
st_token_amount: st_tokens_to_mint,
original_stake: amount,
staked_at: Utc::now(),
last_claimed_at: None,
total_rewards_earned: Decimal::ZERO,
})
}
pub fn request_redemption(
&self,
user_id: Uuid,
st_token_amount: Decimal,
unbonding_period_days: i64,
) -> Result<RedemptionRequest> {
if st_token_amount <= Decimal::ZERO {
return Err(CoreError::Validation(
"Redemption amount must be positive".to_string(),
));
}
if st_token_amount > self.st_token_supply {
return Err(CoreError::InsufficientBalance {
required: st_token_amount,
available: self.st_token_supply,
});
}
let expected_amount = st_token_amount * self.exchange_rate;
let now = Utc::now();
let unlock_at = now + chrono::Duration::days(unbonding_period_days);
Ok(RedemptionRequest {
id: Uuid::new_v4(),
pool_id: self.id,
user_id,
st_token_amount,
expected_amount,
requested_at: now,
unlock_at,
status: RedemptionStatus::Pending,
})
}
pub fn complete_redemption(&mut self, request: &mut RedemptionRequest) -> Result<Decimal> {
if request.status != RedemptionStatus::Pending {
return Err(CoreError::Validation(
"Redemption request is not pending".to_string(),
));
}
if Utc::now() < request.unlock_at {
return Err(CoreError::Validation(
"Unbonding period not complete".to_string(),
));
}
let actual_amount = request.st_token_amount * self.exchange_rate;
self.total_staked -= actual_amount;
self.st_token_supply -= request.st_token_amount;
request.status = RedemptionStatus::Claimed;
Ok(actual_amount)
}
pub fn distribute_rewards(&mut self, reward_amount: Decimal) -> Result<()> {
if reward_amount < Decimal::ZERO {
return Err(CoreError::Validation(
"Reward amount cannot be negative".to_string(),
));
}
self.accumulated_rewards += reward_amount;
self.total_staked += reward_amount;
if self.st_token_supply > Decimal::ZERO {
self.exchange_rate = self.total_staked / self.st_token_supply;
}
self.last_reward_time = Utc::now();
Ok(())
}
pub fn slash(&mut self, amount: Decimal, reason: String) -> Result<()> {
if amount <= Decimal::ZERO {
return Err(CoreError::Validation(
"Slash amount must be positive".to_string(),
));
}
if amount > self.total_staked {
return Err(CoreError::Validation(
"Slash amount exceeds total staked".to_string(),
));
}
self.slashing_events.push(SlashingEvent {
id: Uuid::new_v4(),
amount,
reason,
timestamp: Utc::now(),
});
self.total_staked -= amount;
if self.st_token_supply > Decimal::ZERO {
self.exchange_rate = self.total_staked / self.st_token_supply;
}
Ok(())
}
pub fn get_underlying_value(&self, st_token_amount: Decimal) -> Decimal {
st_token_amount * self.exchange_rate
}
pub fn calculate_compound_apy(&self, compounds_per_year: u32) -> Decimal {
if compounds_per_year == 0 {
return self.apy;
}
let rate_per_period = self.apy / Decimal::from(compounds_per_year * 100);
let base = dec!(1.0) + rate_per_period;
let mut result = dec!(1.0);
for _ in 0..compounds_per_year {
result *= base;
}
(result - dec!(1.0)) * dec!(100)
}
pub fn get_stats(&self) -> PoolStats {
let total_slashed: Decimal = self.slashing_events.iter().map(|e| e.amount).sum();
PoolStats {
total_staked: self.total_staked,
st_token_supply: self.st_token_supply,
exchange_rate: self.exchange_rate,
apy: self.apy,
accumulated_rewards: self.accumulated_rewards,
total_slashed,
slashing_events_count: self.slashing_events.len(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolStats {
pub total_staked: Decimal,
pub st_token_supply: Decimal,
pub exchange_rate: Decimal,
pub apy: Decimal,
pub accumulated_rewards: Decimal,
pub total_slashed: Decimal,
pub slashing_events_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct LiquidStakingManager {
pools: HashMap<Uuid, LiquidStakingPool>,
positions: HashMap<Uuid, Vec<LiquidStakingPosition>>,
redemptions: HashMap<Uuid, Vec<RedemptionRequest>>,
}
impl LiquidStakingManager {
pub fn new() -> Self {
Self::default()
}
pub fn create_pool(
&mut self,
asset_token_id: Uuid,
st_token_id: Uuid,
initial_apy: Decimal,
) -> Uuid {
let pool = LiquidStakingPool::new(asset_token_id, st_token_id, initial_apy);
let pool_id = pool.id;
self.pools.insert(pool_id, pool);
pool_id
}
pub fn stake(
&mut self,
pool_id: Uuid,
user_id: Uuid,
amount: Decimal,
) -> Result<LiquidStakingPosition> {
let pool = self
.pools
.get_mut(&pool_id)
.ok_or(CoreError::NotFound("Pool not found".to_string()))?;
let position = pool.stake(user_id, amount)?;
self.positions
.entry(user_id)
.or_default()
.push(position.clone());
Ok(position)
}
pub fn request_redemption(
&mut self,
pool_id: Uuid,
user_id: Uuid,
st_token_amount: Decimal,
unbonding_period_days: i64,
) -> Result<RedemptionRequest> {
let pool = self
.pools
.get(&pool_id)
.ok_or(CoreError::NotFound("Pool not found".to_string()))?;
let request = pool.request_redemption(user_id, st_token_amount, unbonding_period_days)?;
self.redemptions
.entry(user_id)
.or_default()
.push(request.clone());
Ok(request)
}
pub fn complete_redemption(
&mut self,
pool_id: Uuid,
user_id: Uuid,
request_id: Uuid,
) -> Result<Decimal> {
let pool = self
.pools
.get_mut(&pool_id)
.ok_or(CoreError::NotFound("Pool not found".to_string()))?;
let user_redemptions = self
.redemptions
.get_mut(&user_id)
.ok_or(CoreError::NotFound("No redemptions found".to_string()))?;
let request = user_redemptions
.iter_mut()
.find(|r| r.id == request_id)
.ok_or(CoreError::NotFound(
"Redemption request not found".to_string(),
))?;
pool.complete_redemption(request)
}
pub fn get_pool(&self, pool_id: Uuid) -> Option<&LiquidStakingPool> {
self.pools.get(&pool_id)
}
pub fn get_user_positions(&self, user_id: Uuid) -> Vec<&LiquidStakingPosition> {
self.positions
.get(&user_id)
.map(|positions| positions.iter().collect())
.unwrap_or_default()
}
pub fn get_user_redemptions(&self, user_id: Uuid) -> Vec<&RedemptionRequest> {
self.redemptions
.get(&user_id)
.map(|redemptions| redemptions.iter().collect())
.unwrap_or_default()
}
pub fn distribute_rewards(&mut self, pool_id: Uuid, reward_amount: Decimal) -> Result<()> {
let pool = self
.pools
.get_mut(&pool_id)
.ok_or(CoreError::NotFound("Pool not found".to_string()))?;
pool.distribute_rewards(reward_amount)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_liquid_staking_pool_creation() {
let pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
assert_eq!(pool.total_staked, Decimal::ZERO);
assert_eq!(pool.exchange_rate, dec!(1.0));
assert_eq!(pool.apy, dec!(10.0));
}
#[test]
fn test_stake_and_mint() {
let mut pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
let user_id = Uuid::new_v4();
let position = pool.stake(user_id, dec!(1000)).unwrap();
assert_eq!(position.st_token_amount, dec!(1000)); assert_eq!(pool.total_staked, dec!(1000));
assert_eq!(pool.st_token_supply, dec!(1000));
}
#[test]
fn test_reward_distribution_updates_exchange_rate() {
let mut pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
pool.stake(Uuid::new_v4(), dec!(1000)).unwrap();
pool.distribute_rewards(dec!(100)).unwrap();
assert_eq!(pool.exchange_rate, dec!(1.1)); assert_eq!(pool.total_staked, dec!(1100));
}
#[test]
fn test_redemption_flow() {
let mut pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
let user_id = Uuid::new_v4();
pool.stake(user_id, dec!(1000)).unwrap();
let mut request = pool.request_redemption(user_id, dec!(500), 7).unwrap();
assert_eq!(request.status, RedemptionStatus::Pending);
let result = pool.complete_redemption(&mut request);
assert!(result.is_err());
}
#[test]
fn test_slashing_reduces_exchange_rate() {
let mut pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
pool.stake(Uuid::new_v4(), dec!(1000)).unwrap();
pool.slash(dec!(100), "Validator misbehavior".to_string())
.unwrap();
assert_eq!(pool.exchange_rate, dec!(0.9)); assert_eq!(pool.total_staked, dec!(900));
assert_eq!(pool.slashing_events.len(), 1);
}
#[test]
fn test_compound_apy_calculation() {
let pool = LiquidStakingPool::new(Uuid::new_v4(), Uuid::new_v4(), dec!(10.0));
let compound_apy = pool.calculate_compound_apy(365);
assert!(compound_apy > dec!(10.0)); }
#[test]
fn test_manager_operations() {
let mut manager = LiquidStakingManager::new();
let asset_token = Uuid::new_v4();
let st_token = Uuid::new_v4();
let pool_id = manager.create_pool(asset_token, st_token, dec!(10.0));
let user_id = Uuid::new_v4();
let position = manager.stake(pool_id, user_id, dec!(1000)).unwrap();
assert_eq!(position.st_token_amount, dec!(1000));
let positions = manager.get_user_positions(user_id);
assert_eq!(positions.len(), 1);
}
}