use crate::error::{Result, TokenError};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tenzro_storage::{KvStore, WriteOp, CF_TOKENS};
use tenzro_types::primitives::{Address, Timestamp};
use tracing::{debug, info, warn};
const LIQUID_CONFIG_KEY: &[u8] = b"liquid:config";
const LIQUID_TOTALS_KEY: &[u8] = b"liquid:totals";
const LIQUID_BALANCE_PREFIX: &[u8] = b"liquid:bal:";
const LIQUID_DELEGATION_PREFIX: &[u8] = b"liquid:val:";
const LIQUID_WITHDRAWAL_PREFIX: &[u8] = b"liquid:wr:";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
struct LiquidStakingTotals {
total_sttnzo_supply: u128,
total_underlying_wei: u128,
total_protocol_fees: u128,
total_rewards_distributed: u128,
}
pub const STTNZO_DECIMALS: u8 = 18;
pub const ONE_STTNZO: u128 = 1_000_000_000_000_000_000;
pub const DEFAULT_PROTOCOL_FEE_BPS: u32 = 1000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidStakingConfig {
pub protocol_fee_bps: u32,
pub min_deposit: u128,
pub max_total_deposits: u128,
pub unbonding_period_ms: i64,
pub max_validators: usize,
}
impl Default for LiquidStakingConfig {
fn default() -> Self {
Self {
protocol_fee_bps: DEFAULT_PROTOCOL_FEE_BPS,
min_deposit: ONE_STTNZO / 10, max_total_deposits: 0, unbonding_period_ms: 7 * 24 * 60 * 60 * 1000, max_validators: 50,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawalRequest {
pub requester: Address,
pub sttnzo_amount: u128,
pub tnzo_amount: u128,
pub unbonding_complete_at: Timestamp,
pub claimed: bool,
pub request_id: String,
}
pub struct LiquidStakingPool {
config: parking_lot::RwLock<LiquidStakingConfig>,
sttnzo_balances: DashMap<Address, u128>,
total_sttnzo_supply: parking_lot::RwLock<u128>,
total_underlying_wei: parking_lot::RwLock<u128>,
total_protocol_fees: parking_lot::RwLock<u128>,
withdrawal_requests: DashMap<String, WithdrawalRequest>,
validator_delegations: DashMap<Address, u128>,
total_rewards_distributed: parking_lot::RwLock<u128>,
storage: Option<Arc<dyn KvStore>>,
}
impl std::fmt::Debug for LiquidStakingPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiquidStakingPool")
.field("config", &*self.config.read())
.field("holder_count", &self.sttnzo_balances.len())
.field("validator_count", &self.validator_delegations.len())
.field("pending_withdrawals", &self.withdrawal_requests.len())
.field("total_sttnzo_supply", &*self.total_sttnzo_supply.read())
.field("total_underlying_wei", &*self.total_underlying_wei.read())
.field("has_storage", &self.storage.is_some())
.finish()
}
}
impl LiquidStakingPool {
pub fn new(config: LiquidStakingConfig) -> Result<Self> {
if config.protocol_fee_bps > 5000 {
return Err(TokenError::InvalidAmount(format!(
"Protocol fee {} bps exceeds maximum 5000 bps (50%)",
config.protocol_fee_bps
)));
}
info!("Initializing stTNZO liquid staking pool (fee: {}bps)", config.protocol_fee_bps);
Ok(Self {
config: parking_lot::RwLock::new(config),
sttnzo_balances: DashMap::new(),
total_sttnzo_supply: parking_lot::RwLock::new(0),
total_underlying_wei: parking_lot::RwLock::new(0),
total_protocol_fees: parking_lot::RwLock::new(0),
withdrawal_requests: DashMap::new(),
validator_delegations: DashMap::new(),
total_rewards_distributed: parking_lot::RwLock::new(0),
storage: None,
})
}
pub fn with_storage(
config: LiquidStakingConfig,
storage: Arc<dyn KvStore>,
) -> Result<Self> {
if config.protocol_fee_bps > 5000 {
return Err(TokenError::InvalidAmount(format!(
"Protocol fee {} bps exceeds maximum 5000 bps (50%)",
config.protocol_fee_bps
)));
}
let pool = Self {
config: parking_lot::RwLock::new(config),
sttnzo_balances: DashMap::new(),
total_sttnzo_supply: parking_lot::RwLock::new(0),
total_underlying_wei: parking_lot::RwLock::new(0),
total_protocol_fees: parking_lot::RwLock::new(0),
withdrawal_requests: DashMap::new(),
validator_delegations: DashMap::new(),
total_rewards_distributed: parking_lot::RwLock::new(0),
storage: Some(storage),
};
pool.hydrate_from_storage()?;
Ok(pool)
}
fn hydrate_from_storage(&self) -> Result<()> {
let storage = match &self.storage {
Some(s) => s.clone(),
None => return Ok(()),
};
match storage
.get(CF_TOKENS, LIQUID_CONFIG_KEY)
.map_err(|e| TokenError::StorageError(format!("get liquid config: {}", e)))?
{
Some(bytes) => {
let cfg: LiquidStakingConfig = serde_json::from_slice(&bytes).map_err(|e| {
TokenError::StorageError(format!("decode liquid config: {}", e))
})?;
*self.config.write() = cfg;
}
None => {
let snapshot = self.config.read().clone();
self.persist_config(&snapshot)?;
}
}
if let Some(bytes) = storage
.get(CF_TOKENS, LIQUID_TOTALS_KEY)
.map_err(|e| TokenError::StorageError(format!("get liquid totals: {}", e)))?
{
let totals: LiquidStakingTotals = serde_json::from_slice(&bytes).map_err(|e| {
TokenError::StorageError(format!("decode liquid totals: {}", e))
})?;
*self.total_sttnzo_supply.write() = totals.total_sttnzo_supply;
*self.total_underlying_wei.write() = totals.total_underlying_wei;
*self.total_protocol_fees.write() = totals.total_protocol_fees;
*self.total_rewards_distributed.write() = totals.total_rewards_distributed;
}
let bal_keys = storage
.get_keys_with_prefix(CF_TOKENS, LIQUID_BALANCE_PREFIX)
.map_err(|e| TokenError::StorageError(format!("scan liquid balances: {}", e)))?;
for key in &bal_keys {
let Some(bytes) = storage
.get(CF_TOKENS, key)
.map_err(|e| TokenError::StorageError(format!("get liquid balance: {}", e)))?
else {
continue;
};
let entry: (Address, u128) = serde_json::from_slice(&bytes).map_err(|e| {
TokenError::StorageError(format!("decode liquid balance: {}", e))
})?;
self.sttnzo_balances.insert(entry.0, entry.1);
}
let val_keys = storage
.get_keys_with_prefix(CF_TOKENS, LIQUID_DELEGATION_PREFIX)
.map_err(|e| TokenError::StorageError(format!("scan liquid delegations: {}", e)))?;
for key in &val_keys {
let Some(bytes) = storage
.get(CF_TOKENS, key)
.map_err(|e| TokenError::StorageError(format!("get liquid delegation: {}", e)))?
else {
continue;
};
let entry: (Address, u128) = serde_json::from_slice(&bytes).map_err(|e| {
TokenError::StorageError(format!("decode liquid delegation: {}", e))
})?;
self.validator_delegations.insert(entry.0, entry.1);
}
let wr_keys = storage
.get_keys_with_prefix(CF_TOKENS, LIQUID_WITHDRAWAL_PREFIX)
.map_err(|e| TokenError::StorageError(format!("scan liquid withdrawals: {}", e)))?;
for key in &wr_keys {
let Some(bytes) = storage
.get(CF_TOKENS, key)
.map_err(|e| TokenError::StorageError(format!("get liquid withdrawal: {}", e)))?
else {
continue;
};
let req: WithdrawalRequest = serde_json::from_slice(&bytes).map_err(|e| {
TokenError::StorageError(format!("decode liquid withdrawal: {}", e))
})?;
self.withdrawal_requests.insert(req.request_id.clone(), req);
}
info!(
holders = self.sttnzo_balances.len(),
validators = self.validator_delegations.len(),
pending_withdrawals = self.withdrawal_requests.len(),
supply = *self.total_sttnzo_supply.read(),
underlying = *self.total_underlying_wei.read(),
"LiquidStakingPool hydrated from storage"
);
Ok(())
}
fn persist_config(&self, cfg: &LiquidStakingConfig) -> Result<()> {
if let Some(storage) = &self.storage {
let value = serde_json::to_vec(cfg)
.map_err(|e| TokenError::StorageError(format!("encode liquid config: {}", e)))?;
storage
.write_batch_sync(vec![WriteOp::Put {
cf: CF_TOKENS.to_string(),
key: LIQUID_CONFIG_KEY.to_vec(),
value,
}])
.map_err(|e| {
TokenError::StorageError(format!("persist liquid config: {}", e))
})?;
}
Ok(())
}
fn persist_totals(&self) -> Result<()> {
if let Some(storage) = &self.storage {
let totals = LiquidStakingTotals {
total_sttnzo_supply: *self.total_sttnzo_supply.read(),
total_underlying_wei: *self.total_underlying_wei.read(),
total_protocol_fees: *self.total_protocol_fees.read(),
total_rewards_distributed: *self.total_rewards_distributed.read(),
};
let value = serde_json::to_vec(&totals)
.map_err(|e| TokenError::StorageError(format!("encode liquid totals: {}", e)))?;
storage
.write_batch_sync(vec![WriteOp::Put {
cf: CF_TOKENS.to_string(),
key: LIQUID_TOTALS_KEY.to_vec(),
value,
}])
.map_err(|e| {
TokenError::StorageError(format!("persist liquid totals: {}", e))
})?;
}
Ok(())
}
fn balance_storage_key(addr: &Address) -> Vec<u8> {
let mut k = LIQUID_BALANCE_PREFIX.to_vec();
k.extend_from_slice(addr.as_bytes());
k
}
fn delegation_storage_key(addr: &Address) -> Vec<u8> {
let mut k = LIQUID_DELEGATION_PREFIX.to_vec();
k.extend_from_slice(addr.as_bytes());
k
}
fn withdrawal_storage_key(request_id: &str) -> Vec<u8> {
let mut k = LIQUID_WITHDRAWAL_PREFIX.to_vec();
k.extend_from_slice(request_id.as_bytes());
k
}
fn persist_balance(&self, addr: &Address, amount: u128) -> Result<()> {
if let Some(storage) = &self.storage {
let key = Self::balance_storage_key(addr);
if amount == 0 {
storage
.write_batch_sync(vec![WriteOp::Delete {
cf: CF_TOKENS.to_string(),
key,
}])
.map_err(|e| {
TokenError::StorageError(format!("delete liquid balance: {}", e))
})?;
} else {
let value = serde_json::to_vec(&(*addr, amount)).map_err(|e| {
TokenError::StorageError(format!("encode liquid balance: {}", e))
})?;
storage
.write_batch_sync(vec![WriteOp::Put {
cf: CF_TOKENS.to_string(),
key,
value,
}])
.map_err(|e| {
TokenError::StorageError(format!("persist liquid balance: {}", e))
})?;
}
}
Ok(())
}
fn persist_delegation(&self, validator: &Address, amount: u128) -> Result<()> {
if let Some(storage) = &self.storage {
let key = Self::delegation_storage_key(validator);
let value = serde_json::to_vec(&(*validator, amount)).map_err(|e| {
TokenError::StorageError(format!("encode liquid delegation: {}", e))
})?;
storage
.write_batch_sync(vec![WriteOp::Put {
cf: CF_TOKENS.to_string(),
key,
value,
}])
.map_err(|e| {
TokenError::StorageError(format!("persist liquid delegation: {}", e))
})?;
}
Ok(())
}
fn persist_withdrawal(&self, req: &WithdrawalRequest) -> Result<()> {
if let Some(storage) = &self.storage {
let key = Self::withdrawal_storage_key(&req.request_id);
let value = serde_json::to_vec(req).map_err(|e| {
TokenError::StorageError(format!("encode liquid withdrawal: {}", e))
})?;
storage
.write_batch_sync(vec![WriteOp::Put {
cf: CF_TOKENS.to_string(),
key,
value,
}])
.map_err(|e| {
TokenError::StorageError(format!("persist liquid withdrawal: {}", e))
})?;
}
Ok(())
}
pub fn update_config(&self, new_config: LiquidStakingConfig) -> Result<()> {
if new_config.protocol_fee_bps > 5000 {
return Err(TokenError::InvalidAmount(format!(
"Protocol fee {} bps exceeds maximum 5000 bps (50%)",
new_config.protocol_fee_bps
)));
}
*self.config.write() = new_config.clone();
self.persist_config(&new_config)?;
info!("LiquidStakingConfig updated");
Ok(())
}
pub fn exchange_rate(&self) -> u128 {
let supply = *self.total_sttnzo_supply.read();
let underlying = *self.total_underlying_wei.read();
if supply == 0 {
ONE_STTNZO
} else {
let quotient = underlying / supply;
let remainder = underlying % supply;
quotient.saturating_mul(ONE_STTNZO)
.saturating_add(
remainder.saturating_mul(ONE_STTNZO)
/ supply
)
}
}
pub fn deposit(&self, depositor: Address, tnzo_amount: u128) -> Result<u128> {
let config = self.config.read();
if tnzo_amount < config.min_deposit {
return Err(TokenError::InvalidAmount(format!(
"Deposit below minimum: {} < {}",
tnzo_amount, config.min_deposit
)));
}
if config.max_total_deposits > 0 {
let current_total = *self.total_underlying_wei.read();
if current_total + tnzo_amount > config.max_total_deposits {
return Err(TokenError::InvalidAmount(
"Pool cap exceeded".to_string()
));
}
}
drop(config);
let rate = self.exchange_rate();
let sttnzo_amount = if rate == 0 {
tnzo_amount } else {
let quotient = tnzo_amount / rate;
let remainder = tnzo_amount % rate;
quotient
.checked_mul(ONE_STTNZO)
.and_then(|q| {
remainder
.checked_mul(ONE_STTNZO)
.map(|r| q + r / rate)
})
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "stTNZO mint calculation".to_string(),
})?
};
if sttnzo_amount == 0 {
return Err(TokenError::InvalidAmount(
"Deposit too small to mint any stTNZO".to_string()
));
}
let current_balance = self.sttnzo_balances.get(&depositor)
.map(|v| *v)
.unwrap_or(0);
let new_balance = current_balance + sttnzo_amount;
self.sttnzo_balances.insert(depositor, new_balance);
*self.total_sttnzo_supply.write() += sttnzo_amount;
*self.total_underlying_wei.write() += tnzo_amount;
if let Err(e) = self.persist_balance(&depositor, new_balance) {
warn!("Failed to persist stTNZO balance: {}", e);
}
if let Err(e) = self.persist_totals() {
warn!("Failed to persist liquid totals: {}", e);
}
info!(
"stTNZO: Deposited {} TNZO, minted {} stTNZO to {} (rate: {})",
tnzo_amount, sttnzo_amount, depositor, rate
);
Ok(sttnzo_amount)
}
pub fn request_withdrawal(&self, requester: Address, sttnzo_amount: u128) -> Result<WithdrawalRequest> {
let balance = self.sttnzo_balances.get(&requester)
.map(|v| *v)
.unwrap_or(0);
if balance < sttnzo_amount {
return Err(TokenError::InsufficientBalance {
required: sttnzo_amount,
available: balance,
});
}
let rate = self.exchange_rate();
let quotient = sttnzo_amount / ONE_STTNZO;
let remainder = sttnzo_amount % ONE_STTNZO;
let tnzo_amount = quotient
.checked_mul(rate)
.and_then(|q| {
remainder
.checked_mul(rate)
.map(|r| q + r / ONE_STTNZO)
})
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "stTNZO withdrawal calculation".to_string(),
})?;
let new_balance = balance - sttnzo_amount;
self.sttnzo_balances.insert(requester, new_balance);
*self.total_sttnzo_supply.write() -= sttnzo_amount;
let config = self.config.read();
let unbonding_ms = config.unbonding_period_ms;
drop(config);
let request = WithdrawalRequest {
requester,
sttnzo_amount,
tnzo_amount,
unbonding_complete_at: Timestamp::new(
Timestamp::now().as_millis() + unbonding_ms,
),
claimed: false,
request_id: uuid::Uuid::new_v4().to_string(),
};
let request_id = request.request_id.clone();
self.withdrawal_requests.insert(request_id.clone(), request.clone());
if let Err(e) = self.persist_balance(&requester, new_balance) {
warn!("Failed to persist stTNZO balance: {}", e);
}
if let Err(e) = self.persist_totals() {
warn!("Failed to persist liquid totals: {}", e);
}
if let Err(e) = self.persist_withdrawal(&request) {
warn!("Failed to persist withdrawal request: {}", e);
}
info!(
"stTNZO: Withdrawal requested: {} stTNZO -> {} TNZO, unbonding until {}",
sttnzo_amount, tnzo_amount, request.unbonding_complete_at
);
Ok(request)
}
pub fn claim_withdrawal(&self, request_id: &str) -> Result<(Address, u128)> {
let snapshot = {
let mut request = self
.withdrawal_requests
.get_mut(request_id)
.ok_or_else(|| {
TokenError::InvalidAmount(format!(
"Withdrawal request not found: {}",
request_id
))
})?;
if request.claimed {
return Err(TokenError::InvalidAmount(
"Withdrawal already claimed".to_string(),
));
}
if Timestamp::now() < request.unbonding_complete_at {
return Err(TokenError::StakeLocked {
unlock_time: request.unbonding_complete_at.as_millis(),
});
}
request.claimed = true;
request.value().clone()
};
let requester = snapshot.requester;
let tnzo_amount = snapshot.tnzo_amount;
*self.total_underlying_wei.write() -= tnzo_amount.min(*self.total_underlying_wei.read());
if let Err(e) = self.persist_withdrawal(&snapshot) {
warn!("Failed to persist claimed withdrawal: {}", e);
}
if let Err(e) = self.persist_totals() {
warn!("Failed to persist liquid totals: {}", e);
}
info!(
"stTNZO: Withdrawal claimed: {} TNZO to {}",
tnzo_amount, requester
);
Ok((requester, tnzo_amount))
}
pub fn distribute_rewards(&self, reward_amount: u128) -> Result<RewardDistribution> {
if reward_amount == 0 {
return Err(TokenError::InvalidAmount(
"Reward amount must be greater than zero".to_string()
));
}
let config = self.config.read();
let fee_bps = config.protocol_fee_bps;
drop(config);
let protocol_fee = reward_amount
.checked_mul(fee_bps as u128)
.unwrap_or(0)
/ 10000;
let staker_reward = reward_amount - protocol_fee;
*self.total_underlying_wei.write() += staker_reward;
*self.total_protocol_fees.write() += protocol_fee;
*self.total_rewards_distributed.write() += reward_amount;
if let Err(e) = self.persist_totals() {
warn!("Failed to persist liquid totals: {}", e);
}
let new_rate = self.exchange_rate();
debug!(
"stTNZO: Distributed {} TNZO rewards (staker: {}, protocol: {}), new rate: {}",
reward_amount, staker_reward, protocol_fee, new_rate
);
Ok(RewardDistribution {
total_reward: reward_amount,
staker_share: staker_reward,
protocol_fee,
new_exchange_rate: new_rate,
})
}
pub fn balance_of(&self, address: &Address) -> u128 {
self.sttnzo_balances.get(address).map(|v| *v).unwrap_or(0)
}
pub fn tnzo_value(&self, sttnzo_amount: u128) -> u128 {
let rate = self.exchange_rate();
let quotient = sttnzo_amount / ONE_STTNZO;
let remainder = sttnzo_amount % ONE_STTNZO;
quotient.saturating_mul(rate)
.saturating_add(remainder.saturating_mul(rate) / ONE_STTNZO)
}
pub fn stats(&self) -> LiquidStakingStats {
LiquidStakingStats {
total_sttnzo_supply: *self.total_sttnzo_supply.read(),
total_underlying_wei: *self.total_underlying_wei.read(),
exchange_rate: self.exchange_rate(),
total_protocol_fees: *self.total_protocol_fees.read(),
total_rewards_distributed: *self.total_rewards_distributed.read(),
holder_count: self.sttnzo_balances.len() as u64,
pending_withdrawals: self.withdrawal_requests.iter()
.filter(|r| !r.claimed)
.count() as u64,
}
}
pub fn add_validator(&self, validator: Address, amount: u128) {
let current = self.validator_delegations.get(&validator)
.map(|v| *v)
.unwrap_or(0);
let new_amount = current + amount;
self.validator_delegations.insert(validator, new_amount);
if let Err(e) = self.persist_delegation(&validator, new_amount) {
warn!("Failed to persist validator delegation: {}", e);
}
}
pub fn validator_delegations(&self) -> Vec<(Address, u128)> {
self.validator_delegations.iter()
.map(|entry| (*entry.key(), *entry.value()))
.collect()
}
pub fn transfer(&self, from: &Address, to: &Address, amount: u128) -> Result<()> {
let from_balance = self.balance_of(from);
if from_balance < amount {
return Err(TokenError::InsufficientBalance {
required: amount,
available: from_balance,
});
}
let new_from = from_balance - amount;
self.sttnzo_balances.insert(*from, new_from);
let to_balance = self.balance_of(to);
let new_to = to_balance + amount;
self.sttnzo_balances.insert(*to, new_to);
if let Err(e) = self.persist_balance(from, new_from) {
warn!("Failed to persist stTNZO balance: {}", e);
}
if let Err(e) = self.persist_balance(to, new_to) {
warn!("Failed to persist stTNZO balance: {}", e);
}
debug!("stTNZO: Transferred {} from {} to {}", amount, from, to);
Ok(())
}
pub fn pending_withdrawals_for(&self, holder: &Address) -> Vec<WithdrawalRequest> {
self.withdrawal_requests
.iter()
.filter(|entry| !entry.value().claimed && entry.value().requester == *holder)
.map(|entry| entry.value().clone())
.collect()
}
pub fn get_withdrawal(&self, request_id: &str) -> Option<WithdrawalRequest> {
self.withdrawal_requests.get(request_id).map(|r| r.value().clone())
}
pub fn config(&self) -> LiquidStakingConfig {
self.config.read().clone()
}
}
impl Default for LiquidStakingPool {
fn default() -> Self {
Self::new(LiquidStakingConfig::default())
.expect("Default config should always be valid")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardDistribution {
pub total_reward: u128,
pub staker_share: u128,
pub protocol_fee: u128,
pub new_exchange_rate: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidStakingStats {
pub total_sttnzo_supply: u128,
pub total_underlying_wei: u128,
pub exchange_rate: u128,
pub total_protocol_fees: u128,
pub total_rewards_distributed: u128,
pub holder_count: u64,
pub pending_withdrawals: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn one_tnzo() -> u128 {
1_000_000_000_000_000_000 }
#[test]
fn test_initial_exchange_rate() {
let pool = LiquidStakingPool::default();
assert_eq!(pool.exchange_rate(), ONE_STTNZO); }
#[test]
fn test_deposit() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
let deposit_amount = 1000 * one_tnzo();
let sttnzo = pool.deposit(user, deposit_amount).unwrap();
assert_eq!(sttnzo, deposit_amount); assert_eq!(pool.balance_of(&user), deposit_amount);
assert_eq!(*pool.total_sttnzo_supply.read(), deposit_amount);
assert_eq!(*pool.total_underlying_wei.read(), deposit_amount);
}
#[test]
fn test_deposit_below_minimum() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
let result = pool.deposit(user, 1); assert!(result.is_err());
}
#[test]
fn test_exchange_rate_increases_with_rewards() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
let deposit = 1000 * one_tnzo();
pool.deposit(user, deposit).unwrap();
let rate_before = pool.exchange_rate();
pool.distribute_rewards(100 * one_tnzo()).unwrap();
let rate_after = pool.exchange_rate();
assert!(rate_after > rate_before, "Exchange rate should increase after rewards");
let value = pool.tnzo_value(pool.balance_of(&user));
assert!(value > deposit, "stTNZO value should increase after rewards");
}
#[test]
fn test_withdrawal_request() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
let deposit = 1000 * one_tnzo();
pool.deposit(user, deposit).unwrap();
let withdraw_sttnzo = 500 * one_tnzo();
let request = pool.request_withdrawal(user, withdraw_sttnzo).unwrap();
assert_eq!(request.sttnzo_amount, withdraw_sttnzo);
assert_eq!(request.tnzo_amount, withdraw_sttnzo); assert!(!request.claimed);
assert_eq!(pool.balance_of(&user), deposit - withdraw_sttnzo);
}
#[test]
fn test_insufficient_balance_withdrawal() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
pool.deposit(user, 100 * one_tnzo()).unwrap();
let result = pool.request_withdrawal(user, 200 * one_tnzo());
assert!(result.is_err());
}
#[test]
fn test_transfer() {
let pool = LiquidStakingPool::default();
let alice = Address::new([1u8; 32]);
let bob = Address::new([2u8; 32]);
pool.deposit(alice, 1000 * one_tnzo()).unwrap();
pool.transfer(&alice, &bob, 300 * one_tnzo()).unwrap();
assert_eq!(pool.balance_of(&alice), 700 * one_tnzo());
assert_eq!(pool.balance_of(&bob), 300 * one_tnzo());
}
#[test]
fn test_protocol_fee_collection() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
pool.deposit(user, 1000 * one_tnzo()).unwrap();
let dist = pool.distribute_rewards(100 * one_tnzo()).unwrap();
assert_eq!(dist.protocol_fee, 10 * one_tnzo());
assert_eq!(dist.staker_share, 90 * one_tnzo());
assert_eq!(*pool.total_protocol_fees.read(), 10 * one_tnzo());
}
#[test]
fn test_stats() {
let pool = LiquidStakingPool::default();
let user = Address::new([1u8; 32]);
pool.deposit(user, 1000 * one_tnzo()).unwrap();
let stats = pool.stats();
assert_eq!(stats.total_sttnzo_supply, 1000 * one_tnzo());
assert_eq!(stats.total_underlying_wei, 1000 * one_tnzo());
assert_eq!(stats.holder_count, 1);
assert_eq!(stats.exchange_rate, ONE_STTNZO);
}
#[test]
fn test_multiple_depositors() {
let pool = LiquidStakingPool::default();
let alice = Address::new([1u8; 32]);
let bob = Address::new([2u8; 32]);
pool.deposit(alice, 1000 * one_tnzo()).unwrap();
pool.distribute_rewards(100 * one_tnzo()).unwrap();
let bob_sttnzo = pool.deposit(bob, 1000 * one_tnzo()).unwrap();
assert!(bob_sttnzo < 1000 * one_tnzo(), "Bob should get less stTNZO at higher rate");
let stats = pool.stats();
assert_eq!(stats.holder_count, 2);
}
}