1use crate::error::{Result, TokenError};
34use dashmap::DashMap;
35use serde::{Deserialize, Serialize};
36use std::sync::Arc;
37use tenzro_storage::{KvStore, WriteOp, CF_TOKENS};
38use tenzro_types::primitives::{Address, Timestamp};
39use tracing::{debug, info, warn};
40
41const LIQUID_CONFIG_KEY: &[u8] = b"liquid:config";
43const LIQUID_TOTALS_KEY: &[u8] = b"liquid:totals";
46const LIQUID_BALANCE_PREFIX: &[u8] = b"liquid:bal:";
48const LIQUID_DELEGATION_PREFIX: &[u8] = b"liquid:val:";
50const LIQUID_WITHDRAWAL_PREFIX: &[u8] = b"liquid:wr:";
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
57struct LiquidStakingTotals {
58 total_sttnzo_supply: u128,
59 total_underlying_wei: u128,
60 total_protocol_fees: u128,
61 total_rewards_distributed: u128,
62}
63
64pub const STTNZO_DECIMALS: u8 = 18;
66
67pub const ONE_STTNZO: u128 = 1_000_000_000_000_000_000;
69
70pub const DEFAULT_PROTOCOL_FEE_BPS: u32 = 1000;
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LiquidStakingConfig {
76 pub protocol_fee_bps: u32,
78 pub min_deposit: u128,
80 pub max_total_deposits: u128,
82 pub unbonding_period_ms: i64,
84 pub max_validators: usize,
86}
87
88impl Default for LiquidStakingConfig {
89 fn default() -> Self {
90 Self {
91 protocol_fee_bps: DEFAULT_PROTOCOL_FEE_BPS,
92 min_deposit: ONE_STTNZO / 10, max_total_deposits: 0, unbonding_period_ms: 7 * 24 * 60 * 60 * 1000, max_validators: 50,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct WithdrawalRequest {
103 pub requester: Address,
105 pub sttnzo_amount: u128,
107 pub tnzo_amount: u128,
109 pub unbonding_complete_at: Timestamp,
111 pub claimed: bool,
113 pub request_id: String,
115}
116
117pub struct LiquidStakingPool {
121 config: parking_lot::RwLock<LiquidStakingConfig>,
123
124 sttnzo_balances: DashMap<Address, u128>,
126
127 total_sttnzo_supply: parking_lot::RwLock<u128>,
129
130 total_underlying_wei: parking_lot::RwLock<u128>,
132
133 total_protocol_fees: parking_lot::RwLock<u128>,
135
136 withdrawal_requests: DashMap<String, WithdrawalRequest>,
138
139 validator_delegations: DashMap<Address, u128>,
141
142 total_rewards_distributed: parking_lot::RwLock<u128>,
144
145 storage: Option<Arc<dyn KvStore>>,
152}
153
154impl std::fmt::Debug for LiquidStakingPool {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("LiquidStakingPool")
157 .field("config", &*self.config.read())
158 .field("holder_count", &self.sttnzo_balances.len())
159 .field("validator_count", &self.validator_delegations.len())
160 .field("pending_withdrawals", &self.withdrawal_requests.len())
161 .field("total_sttnzo_supply", &*self.total_sttnzo_supply.read())
162 .field("total_underlying_wei", &*self.total_underlying_wei.read())
163 .field("has_storage", &self.storage.is_some())
164 .finish()
165 }
166}
167
168impl LiquidStakingPool {
169 pub fn new(config: LiquidStakingConfig) -> Result<Self> {
171 if config.protocol_fee_bps > 5000 {
173 return Err(TokenError::InvalidAmount(format!(
174 "Protocol fee {} bps exceeds maximum 5000 bps (50%)",
175 config.protocol_fee_bps
176 )));
177 }
178
179 info!("Initializing stTNZO liquid staking pool (fee: {}bps)", config.protocol_fee_bps);
180
181 Ok(Self {
182 config: parking_lot::RwLock::new(config),
183 sttnzo_balances: DashMap::new(),
184 total_sttnzo_supply: parking_lot::RwLock::new(0),
185 total_underlying_wei: parking_lot::RwLock::new(0),
186 total_protocol_fees: parking_lot::RwLock::new(0),
187 withdrawal_requests: DashMap::new(),
188 validator_delegations: DashMap::new(),
189 total_rewards_distributed: parking_lot::RwLock::new(0),
190 storage: None,
191 })
192 }
193
194 pub fn with_storage(
198 config: LiquidStakingConfig,
199 storage: Arc<dyn KvStore>,
200 ) -> Result<Self> {
201 if config.protocol_fee_bps > 5000 {
202 return Err(TokenError::InvalidAmount(format!(
203 "Protocol fee {} bps exceeds maximum 5000 bps (50%)",
204 config.protocol_fee_bps
205 )));
206 }
207
208 let pool = Self {
209 config: parking_lot::RwLock::new(config),
210 sttnzo_balances: DashMap::new(),
211 total_sttnzo_supply: parking_lot::RwLock::new(0),
212 total_underlying_wei: parking_lot::RwLock::new(0),
213 total_protocol_fees: parking_lot::RwLock::new(0),
214 withdrawal_requests: DashMap::new(),
215 validator_delegations: DashMap::new(),
216 total_rewards_distributed: parking_lot::RwLock::new(0),
217 storage: Some(storage),
218 };
219
220 pool.hydrate_from_storage()?;
221 Ok(pool)
222 }
223
224 fn hydrate_from_storage(&self) -> Result<()> {
225 let storage = match &self.storage {
226 Some(s) => s.clone(),
227 None => return Ok(()),
228 };
229
230 match storage
232 .get(CF_TOKENS, LIQUID_CONFIG_KEY)
233 .map_err(|e| TokenError::StorageError(format!("get liquid config: {}", e)))?
234 {
235 Some(bytes) => {
236 let cfg: LiquidStakingConfig = serde_json::from_slice(&bytes).map_err(|e| {
237 TokenError::StorageError(format!("decode liquid config: {}", e))
238 })?;
239 *self.config.write() = cfg;
240 }
241 None => {
242 let snapshot = self.config.read().clone();
243 self.persist_config(&snapshot)?;
244 }
245 }
246
247 if let Some(bytes) = storage
249 .get(CF_TOKENS, LIQUID_TOTALS_KEY)
250 .map_err(|e| TokenError::StorageError(format!("get liquid totals: {}", e)))?
251 {
252 let totals: LiquidStakingTotals = serde_json::from_slice(&bytes).map_err(|e| {
253 TokenError::StorageError(format!("decode liquid totals: {}", e))
254 })?;
255 *self.total_sttnzo_supply.write() = totals.total_sttnzo_supply;
256 *self.total_underlying_wei.write() = totals.total_underlying_wei;
257 *self.total_protocol_fees.write() = totals.total_protocol_fees;
258 *self.total_rewards_distributed.write() = totals.total_rewards_distributed;
259 }
260
261 let bal_keys = storage
263 .get_keys_with_prefix(CF_TOKENS, LIQUID_BALANCE_PREFIX)
264 .map_err(|e| TokenError::StorageError(format!("scan liquid balances: {}", e)))?;
265 for key in &bal_keys {
266 let Some(bytes) = storage
267 .get(CF_TOKENS, key)
268 .map_err(|e| TokenError::StorageError(format!("get liquid balance: {}", e)))?
269 else {
270 continue;
271 };
272 let entry: (Address, u128) = serde_json::from_slice(&bytes).map_err(|e| {
273 TokenError::StorageError(format!("decode liquid balance: {}", e))
274 })?;
275 self.sttnzo_balances.insert(entry.0, entry.1);
276 }
277
278 let val_keys = storage
280 .get_keys_with_prefix(CF_TOKENS, LIQUID_DELEGATION_PREFIX)
281 .map_err(|e| TokenError::StorageError(format!("scan liquid delegations: {}", e)))?;
282 for key in &val_keys {
283 let Some(bytes) = storage
284 .get(CF_TOKENS, key)
285 .map_err(|e| TokenError::StorageError(format!("get liquid delegation: {}", e)))?
286 else {
287 continue;
288 };
289 let entry: (Address, u128) = serde_json::from_slice(&bytes).map_err(|e| {
290 TokenError::StorageError(format!("decode liquid delegation: {}", e))
291 })?;
292 self.validator_delegations.insert(entry.0, entry.1);
293 }
294
295 let wr_keys = storage
297 .get_keys_with_prefix(CF_TOKENS, LIQUID_WITHDRAWAL_PREFIX)
298 .map_err(|e| TokenError::StorageError(format!("scan liquid withdrawals: {}", e)))?;
299 for key in &wr_keys {
300 let Some(bytes) = storage
301 .get(CF_TOKENS, key)
302 .map_err(|e| TokenError::StorageError(format!("get liquid withdrawal: {}", e)))?
303 else {
304 continue;
305 };
306 let req: WithdrawalRequest = serde_json::from_slice(&bytes).map_err(|e| {
307 TokenError::StorageError(format!("decode liquid withdrawal: {}", e))
308 })?;
309 self.withdrawal_requests.insert(req.request_id.clone(), req);
310 }
311
312 info!(
313 holders = self.sttnzo_balances.len(),
314 validators = self.validator_delegations.len(),
315 pending_withdrawals = self.withdrawal_requests.len(),
316 supply = *self.total_sttnzo_supply.read(),
317 underlying = *self.total_underlying_wei.read(),
318 "LiquidStakingPool hydrated from storage"
319 );
320 Ok(())
321 }
322
323 fn persist_config(&self, cfg: &LiquidStakingConfig) -> Result<()> {
324 if let Some(storage) = &self.storage {
325 let value = serde_json::to_vec(cfg)
326 .map_err(|e| TokenError::StorageError(format!("encode liquid config: {}", e)))?;
327 storage
328 .write_batch_sync(vec![WriteOp::Put {
329 cf: CF_TOKENS.to_string(),
330 key: LIQUID_CONFIG_KEY.to_vec(),
331 value,
332 }])
333 .map_err(|e| {
334 TokenError::StorageError(format!("persist liquid config: {}", e))
335 })?;
336 }
337 Ok(())
338 }
339
340 fn persist_totals(&self) -> Result<()> {
341 if let Some(storage) = &self.storage {
342 let totals = LiquidStakingTotals {
343 total_sttnzo_supply: *self.total_sttnzo_supply.read(),
344 total_underlying_wei: *self.total_underlying_wei.read(),
345 total_protocol_fees: *self.total_protocol_fees.read(),
346 total_rewards_distributed: *self.total_rewards_distributed.read(),
347 };
348 let value = serde_json::to_vec(&totals)
349 .map_err(|e| TokenError::StorageError(format!("encode liquid totals: {}", e)))?;
350 storage
351 .write_batch_sync(vec![WriteOp::Put {
352 cf: CF_TOKENS.to_string(),
353 key: LIQUID_TOTALS_KEY.to_vec(),
354 value,
355 }])
356 .map_err(|e| {
357 TokenError::StorageError(format!("persist liquid totals: {}", e))
358 })?;
359 }
360 Ok(())
361 }
362
363 fn balance_storage_key(addr: &Address) -> Vec<u8> {
364 let mut k = LIQUID_BALANCE_PREFIX.to_vec();
365 k.extend_from_slice(addr.as_bytes());
366 k
367 }
368
369 fn delegation_storage_key(addr: &Address) -> Vec<u8> {
370 let mut k = LIQUID_DELEGATION_PREFIX.to_vec();
371 k.extend_from_slice(addr.as_bytes());
372 k
373 }
374
375 fn withdrawal_storage_key(request_id: &str) -> Vec<u8> {
376 let mut k = LIQUID_WITHDRAWAL_PREFIX.to_vec();
377 k.extend_from_slice(request_id.as_bytes());
378 k
379 }
380
381 fn persist_balance(&self, addr: &Address, amount: u128) -> Result<()> {
382 if let Some(storage) = &self.storage {
383 let key = Self::balance_storage_key(addr);
384 if amount == 0 {
385 storage
386 .write_batch_sync(vec![WriteOp::Delete {
387 cf: CF_TOKENS.to_string(),
388 key,
389 }])
390 .map_err(|e| {
391 TokenError::StorageError(format!("delete liquid balance: {}", e))
392 })?;
393 } else {
394 let value = serde_json::to_vec(&(*addr, amount)).map_err(|e| {
395 TokenError::StorageError(format!("encode liquid balance: {}", e))
396 })?;
397 storage
398 .write_batch_sync(vec![WriteOp::Put {
399 cf: CF_TOKENS.to_string(),
400 key,
401 value,
402 }])
403 .map_err(|e| {
404 TokenError::StorageError(format!("persist liquid balance: {}", e))
405 })?;
406 }
407 }
408 Ok(())
409 }
410
411 fn persist_delegation(&self, validator: &Address, amount: u128) -> Result<()> {
412 if let Some(storage) = &self.storage {
413 let key = Self::delegation_storage_key(validator);
414 let value = serde_json::to_vec(&(*validator, amount)).map_err(|e| {
415 TokenError::StorageError(format!("encode liquid delegation: {}", e))
416 })?;
417 storage
418 .write_batch_sync(vec![WriteOp::Put {
419 cf: CF_TOKENS.to_string(),
420 key,
421 value,
422 }])
423 .map_err(|e| {
424 TokenError::StorageError(format!("persist liquid delegation: {}", e))
425 })?;
426 }
427 Ok(())
428 }
429
430 fn persist_withdrawal(&self, req: &WithdrawalRequest) -> Result<()> {
431 if let Some(storage) = &self.storage {
432 let key = Self::withdrawal_storage_key(&req.request_id);
433 let value = serde_json::to_vec(req).map_err(|e| {
434 TokenError::StorageError(format!("encode liquid withdrawal: {}", e))
435 })?;
436 storage
437 .write_batch_sync(vec![WriteOp::Put {
438 cf: CF_TOKENS.to_string(),
439 key,
440 value,
441 }])
442 .map_err(|e| {
443 TokenError::StorageError(format!("persist liquid withdrawal: {}", e))
444 })?;
445 }
446 Ok(())
447 }
448
449 pub fn update_config(&self, new_config: LiquidStakingConfig) -> Result<()> {
451 if new_config.protocol_fee_bps > 5000 {
452 return Err(TokenError::InvalidAmount(format!(
453 "Protocol fee {} bps exceeds maximum 5000 bps (50%)",
454 new_config.protocol_fee_bps
455 )));
456 }
457 *self.config.write() = new_config.clone();
458 self.persist_config(&new_config)?;
459 info!("LiquidStakingConfig updated");
460 Ok(())
461 }
462
463 pub fn exchange_rate(&self) -> u128 {
470 let supply = *self.total_sttnzo_supply.read();
471 let underlying = *self.total_underlying_wei.read();
472
473 if supply == 0 {
474 ONE_STTNZO
476 } else {
477 let quotient = underlying / supply;
482 let remainder = underlying % supply;
483 quotient.saturating_mul(ONE_STTNZO)
484 .saturating_add(
485 remainder.saturating_mul(ONE_STTNZO)
486 / supply
487 )
488 }
489 }
490
491 pub fn deposit(&self, depositor: Address, tnzo_amount: u128) -> Result<u128> {
496 let config = self.config.read();
497
498 if tnzo_amount < config.min_deposit {
500 return Err(TokenError::InvalidAmount(format!(
501 "Deposit below minimum: {} < {}",
502 tnzo_amount, config.min_deposit
503 )));
504 }
505
506 if config.max_total_deposits > 0 {
508 let current_total = *self.total_underlying_wei.read();
509 if current_total + tnzo_amount > config.max_total_deposits {
510 return Err(TokenError::InvalidAmount(
511 "Pool cap exceeded".to_string()
512 ));
513 }
514 }
515 drop(config);
516
517 let rate = self.exchange_rate();
522 let sttnzo_amount = if rate == 0 {
523 tnzo_amount } else {
525 let quotient = tnzo_amount / rate;
526 let remainder = tnzo_amount % rate;
527 quotient
528 .checked_mul(ONE_STTNZO)
529 .and_then(|q| {
530 remainder
531 .checked_mul(ONE_STTNZO)
532 .map(|r| q + r / rate)
533 })
534 .ok_or_else(|| TokenError::ArithmeticOverflow {
535 operation: "stTNZO mint calculation".to_string(),
536 })?
537 };
538
539 if sttnzo_amount == 0 {
540 return Err(TokenError::InvalidAmount(
541 "Deposit too small to mint any stTNZO".to_string()
542 ));
543 }
544
545 let current_balance = self.sttnzo_balances.get(&depositor)
547 .map(|v| *v)
548 .unwrap_or(0);
549 let new_balance = current_balance + sttnzo_amount;
550 self.sttnzo_balances.insert(depositor, new_balance);
551
552 *self.total_sttnzo_supply.write() += sttnzo_amount;
554 *self.total_underlying_wei.write() += tnzo_amount;
555
556 if let Err(e) = self.persist_balance(&depositor, new_balance) {
558 warn!("Failed to persist stTNZO balance: {}", e);
559 }
560 if let Err(e) = self.persist_totals() {
561 warn!("Failed to persist liquid totals: {}", e);
562 }
563
564 info!(
565 "stTNZO: Deposited {} TNZO, minted {} stTNZO to {} (rate: {})",
566 tnzo_amount, sttnzo_amount, depositor, rate
567 );
568
569 Ok(sttnzo_amount)
570 }
571
572 pub fn request_withdrawal(&self, requester: Address, sttnzo_amount: u128) -> Result<WithdrawalRequest> {
577 let balance = self.sttnzo_balances.get(&requester)
579 .map(|v| *v)
580 .unwrap_or(0);
581
582 if balance < sttnzo_amount {
583 return Err(TokenError::InsufficientBalance {
584 required: sttnzo_amount,
585 available: balance,
586 });
587 }
588
589 let rate = self.exchange_rate();
593 let quotient = sttnzo_amount / ONE_STTNZO;
594 let remainder = sttnzo_amount % ONE_STTNZO;
595 let tnzo_amount = quotient
596 .checked_mul(rate)
597 .and_then(|q| {
598 remainder
599 .checked_mul(rate)
600 .map(|r| q + r / ONE_STTNZO)
601 })
602 .ok_or_else(|| TokenError::ArithmeticOverflow {
603 operation: "stTNZO withdrawal calculation".to_string(),
604 })?;
605
606 let new_balance = balance - sttnzo_amount;
608 self.sttnzo_balances.insert(requester, new_balance);
609 *self.total_sttnzo_supply.write() -= sttnzo_amount;
610
611 let config = self.config.read();
615 let unbonding_ms = config.unbonding_period_ms;
616 drop(config);
617
618 let request = WithdrawalRequest {
619 requester,
620 sttnzo_amount,
621 tnzo_amount,
622 unbonding_complete_at: Timestamp::new(
623 Timestamp::now().as_millis() + unbonding_ms,
624 ),
625 claimed: false,
626 request_id: uuid::Uuid::new_v4().to_string(),
627 };
628
629 let request_id = request.request_id.clone();
630 self.withdrawal_requests.insert(request_id.clone(), request.clone());
631
632 if let Err(e) = self.persist_balance(&requester, new_balance) {
635 warn!("Failed to persist stTNZO balance: {}", e);
636 }
637 if let Err(e) = self.persist_totals() {
638 warn!("Failed to persist liquid totals: {}", e);
639 }
640 if let Err(e) = self.persist_withdrawal(&request) {
641 warn!("Failed to persist withdrawal request: {}", e);
642 }
643
644 info!(
645 "stTNZO: Withdrawal requested: {} stTNZO -> {} TNZO, unbonding until {}",
646 sttnzo_amount, tnzo_amount, request.unbonding_complete_at
647 );
648
649 Ok(request)
650 }
651
652 pub fn claim_withdrawal(&self, request_id: &str) -> Result<(Address, u128)> {
656 let snapshot = {
660 let mut request = self
661 .withdrawal_requests
662 .get_mut(request_id)
663 .ok_or_else(|| {
664 TokenError::InvalidAmount(format!(
665 "Withdrawal request not found: {}",
666 request_id
667 ))
668 })?;
669
670 if request.claimed {
671 return Err(TokenError::InvalidAmount(
672 "Withdrawal already claimed".to_string(),
673 ));
674 }
675
676 if Timestamp::now() < request.unbonding_complete_at {
677 return Err(TokenError::StakeLocked {
678 unlock_time: request.unbonding_complete_at.as_millis(),
679 });
680 }
681
682 request.claimed = true;
683 request.value().clone()
684 };
685
686 let requester = snapshot.requester;
687 let tnzo_amount = snapshot.tnzo_amount;
688
689 *self.total_underlying_wei.write() -= tnzo_amount.min(*self.total_underlying_wei.read());
691
692 if let Err(e) = self.persist_withdrawal(&snapshot) {
694 warn!("Failed to persist claimed withdrawal: {}", e);
695 }
696 if let Err(e) = self.persist_totals() {
697 warn!("Failed to persist liquid totals: {}", e);
698 }
699
700 info!(
701 "stTNZO: Withdrawal claimed: {} TNZO to {}",
702 tnzo_amount, requester
703 );
704
705 Ok((requester, tnzo_amount))
706 }
707
708 pub fn distribute_rewards(&self, reward_amount: u128) -> Result<RewardDistribution> {
714 if reward_amount == 0 {
715 return Err(TokenError::InvalidAmount(
716 "Reward amount must be greater than zero".to_string()
717 ));
718 }
719
720 let config = self.config.read();
721 let fee_bps = config.protocol_fee_bps;
722 drop(config);
723
724 let protocol_fee = reward_amount
726 .checked_mul(fee_bps as u128)
727 .unwrap_or(0)
728 / 10000;
729 let staker_reward = reward_amount - protocol_fee;
730
731 *self.total_underlying_wei.write() += staker_reward;
733
734 *self.total_protocol_fees.write() += protocol_fee;
736
737 *self.total_rewards_distributed.write() += reward_amount;
739
740 if let Err(e) = self.persist_totals() {
742 warn!("Failed to persist liquid totals: {}", e);
743 }
744
745 let new_rate = self.exchange_rate();
746 debug!(
747 "stTNZO: Distributed {} TNZO rewards (staker: {}, protocol: {}), new rate: {}",
748 reward_amount, staker_reward, protocol_fee, new_rate
749 );
750
751 Ok(RewardDistribution {
752 total_reward: reward_amount,
753 staker_share: staker_reward,
754 protocol_fee,
755 new_exchange_rate: new_rate,
756 })
757 }
758
759 pub fn balance_of(&self, address: &Address) -> u128 {
761 self.sttnzo_balances.get(address).map(|v| *v).unwrap_or(0)
762 }
763
764 pub fn tnzo_value(&self, sttnzo_amount: u128) -> u128 {
766 let rate = self.exchange_rate();
767 let quotient = sttnzo_amount / ONE_STTNZO;
769 let remainder = sttnzo_amount % ONE_STTNZO;
770 quotient.saturating_mul(rate)
771 .saturating_add(remainder.saturating_mul(rate) / ONE_STTNZO)
772 }
773
774 pub fn stats(&self) -> LiquidStakingStats {
776 LiquidStakingStats {
777 total_sttnzo_supply: *self.total_sttnzo_supply.read(),
778 total_underlying_wei: *self.total_underlying_wei.read(),
779 exchange_rate: self.exchange_rate(),
780 total_protocol_fees: *self.total_protocol_fees.read(),
781 total_rewards_distributed: *self.total_rewards_distributed.read(),
782 holder_count: self.sttnzo_balances.len() as u64,
783 pending_withdrawals: self.withdrawal_requests.iter()
784 .filter(|r| !r.claimed)
785 .count() as u64,
786 }
787 }
788
789 pub fn add_validator(&self, validator: Address, amount: u128) {
791 let current = self.validator_delegations.get(&validator)
792 .map(|v| *v)
793 .unwrap_or(0);
794 let new_amount = current + amount;
795 self.validator_delegations.insert(validator, new_amount);
796 if let Err(e) = self.persist_delegation(&validator, new_amount) {
797 warn!("Failed to persist validator delegation: {}", e);
798 }
799 }
800
801 pub fn validator_delegations(&self) -> Vec<(Address, u128)> {
803 self.validator_delegations.iter()
804 .map(|entry| (*entry.key(), *entry.value()))
805 .collect()
806 }
807
808 pub fn transfer(&self, from: &Address, to: &Address, amount: u128) -> Result<()> {
810 let from_balance = self.balance_of(from);
811 if from_balance < amount {
812 return Err(TokenError::InsufficientBalance {
813 required: amount,
814 available: from_balance,
815 });
816 }
817
818 let new_from = from_balance - amount;
819 self.sttnzo_balances.insert(*from, new_from);
820 let to_balance = self.balance_of(to);
821 let new_to = to_balance + amount;
822 self.sttnzo_balances.insert(*to, new_to);
823
824 if let Err(e) = self.persist_balance(from, new_from) {
825 warn!("Failed to persist stTNZO balance: {}", e);
826 }
827 if let Err(e) = self.persist_balance(to, new_to) {
828 warn!("Failed to persist stTNZO balance: {}", e);
829 }
830
831 debug!("stTNZO: Transferred {} from {} to {}", amount, from, to);
832 Ok(())
833 }
834
835 pub fn pending_withdrawals_for(&self, holder: &Address) -> Vec<WithdrawalRequest> {
837 self.withdrawal_requests
838 .iter()
839 .filter(|entry| !entry.value().claimed && entry.value().requester == *holder)
840 .map(|entry| entry.value().clone())
841 .collect()
842 }
843
844 pub fn get_withdrawal(&self, request_id: &str) -> Option<WithdrawalRequest> {
846 self.withdrawal_requests.get(request_id).map(|r| r.value().clone())
847 }
848
849 pub fn config(&self) -> LiquidStakingConfig {
851 self.config.read().clone()
852 }
853}
854
855impl Default for LiquidStakingPool {
856 fn default() -> Self {
857 Self::new(LiquidStakingConfig::default())
858 .expect("Default config should always be valid")
859 }
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct RewardDistribution {
865 pub total_reward: u128,
867 pub staker_share: u128,
869 pub protocol_fee: u128,
871 pub new_exchange_rate: u128,
873}
874
875#[derive(Debug, Clone, Serialize, Deserialize)]
877pub struct LiquidStakingStats {
878 pub total_sttnzo_supply: u128,
880 pub total_underlying_wei: u128,
882 pub exchange_rate: u128,
884 pub total_protocol_fees: u128,
886 pub total_rewards_distributed: u128,
888 pub holder_count: u64,
890 pub pending_withdrawals: u64,
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897
898 fn one_tnzo() -> u128 {
899 1_000_000_000_000_000_000 }
901
902 #[test]
903 fn test_initial_exchange_rate() {
904 let pool = LiquidStakingPool::default();
905 assert_eq!(pool.exchange_rate(), ONE_STTNZO); }
907
908 #[test]
909 fn test_deposit() {
910 let pool = LiquidStakingPool::default();
911 let user = Address::new([1u8; 32]);
912
913 let deposit_amount = 1000 * one_tnzo();
915 let sttnzo = pool.deposit(user, deposit_amount).unwrap();
916
917 assert_eq!(sttnzo, deposit_amount); assert_eq!(pool.balance_of(&user), deposit_amount);
919 assert_eq!(*pool.total_sttnzo_supply.read(), deposit_amount);
920 assert_eq!(*pool.total_underlying_wei.read(), deposit_amount);
921 }
922
923 #[test]
924 fn test_deposit_below_minimum() {
925 let pool = LiquidStakingPool::default();
926 let user = Address::new([1u8; 32]);
927
928 let result = pool.deposit(user, 1); assert!(result.is_err());
930 }
931
932 #[test]
933 fn test_exchange_rate_increases_with_rewards() {
934 let pool = LiquidStakingPool::default();
935 let user = Address::new([1u8; 32]);
936
937 let deposit = 1000 * one_tnzo();
939 pool.deposit(user, deposit).unwrap();
940
941 let rate_before = pool.exchange_rate();
942
943 pool.distribute_rewards(100 * one_tnzo()).unwrap();
945
946 let rate_after = pool.exchange_rate();
947 assert!(rate_after > rate_before, "Exchange rate should increase after rewards");
948
949 let value = pool.tnzo_value(pool.balance_of(&user));
951 assert!(value > deposit, "stTNZO value should increase after rewards");
952 }
953
954 #[test]
955 fn test_withdrawal_request() {
956 let pool = LiquidStakingPool::default();
957 let user = Address::new([1u8; 32]);
958
959 let deposit = 1000 * one_tnzo();
961 pool.deposit(user, deposit).unwrap();
962
963 let withdraw_sttnzo = 500 * one_tnzo();
965 let request = pool.request_withdrawal(user, withdraw_sttnzo).unwrap();
966
967 assert_eq!(request.sttnzo_amount, withdraw_sttnzo);
968 assert_eq!(request.tnzo_amount, withdraw_sttnzo); assert!(!request.claimed);
970
971 assert_eq!(pool.balance_of(&user), deposit - withdraw_sttnzo);
973 }
974
975 #[test]
976 fn test_insufficient_balance_withdrawal() {
977 let pool = LiquidStakingPool::default();
978 let user = Address::new([1u8; 32]);
979
980 pool.deposit(user, 100 * one_tnzo()).unwrap();
981
982 let result = pool.request_withdrawal(user, 200 * one_tnzo());
983 assert!(result.is_err());
984 }
985
986 #[test]
987 fn test_transfer() {
988 let pool = LiquidStakingPool::default();
989 let alice = Address::new([1u8; 32]);
990 let bob = Address::new([2u8; 32]);
991
992 pool.deposit(alice, 1000 * one_tnzo()).unwrap();
993 pool.transfer(&alice, &bob, 300 * one_tnzo()).unwrap();
994
995 assert_eq!(pool.balance_of(&alice), 700 * one_tnzo());
996 assert_eq!(pool.balance_of(&bob), 300 * one_tnzo());
997 }
998
999 #[test]
1000 fn test_protocol_fee_collection() {
1001 let pool = LiquidStakingPool::default();
1002 let user = Address::new([1u8; 32]);
1003
1004 pool.deposit(user, 1000 * one_tnzo()).unwrap();
1005
1006 let dist = pool.distribute_rewards(100 * one_tnzo()).unwrap();
1008 assert_eq!(dist.protocol_fee, 10 * one_tnzo());
1009 assert_eq!(dist.staker_share, 90 * one_tnzo());
1010 assert_eq!(*pool.total_protocol_fees.read(), 10 * one_tnzo());
1011 }
1012
1013 #[test]
1014 fn test_stats() {
1015 let pool = LiquidStakingPool::default();
1016 let user = Address::new([1u8; 32]);
1017
1018 pool.deposit(user, 1000 * one_tnzo()).unwrap();
1019
1020 let stats = pool.stats();
1021 assert_eq!(stats.total_sttnzo_supply, 1000 * one_tnzo());
1022 assert_eq!(stats.total_underlying_wei, 1000 * one_tnzo());
1023 assert_eq!(stats.holder_count, 1);
1024 assert_eq!(stats.exchange_rate, ONE_STTNZO);
1025 }
1026
1027 #[test]
1028 fn test_multiple_depositors() {
1029 let pool = LiquidStakingPool::default();
1030 let alice = Address::new([1u8; 32]);
1031 let bob = Address::new([2u8; 32]);
1032
1033 pool.deposit(alice, 1000 * one_tnzo()).unwrap();
1035
1036 pool.distribute_rewards(100 * one_tnzo()).unwrap();
1038
1039 let bob_sttnzo = pool.deposit(bob, 1000 * one_tnzo()).unwrap();
1041 assert!(bob_sttnzo < 1000 * one_tnzo(), "Bob should get less stTNZO at higher rate");
1042
1043 let stats = pool.stats();
1044 assert_eq!(stats.holder_count, 2);
1045 }
1046}