Skip to main content

tenzro_token/
liquid_staking.rs

1//! Liquid staking (stTNZO) implementation
2//!
3//! Implements native liquid staking for Tenzro Network, allowing stakers to receive
4//! a liquid staking derivative token (stTNZO) that represents their staked position
5//! plus accrued rewards.
6//!
7//! # Why Liquid Staking is Critical
8//!
9//! Without liquid staking, staked TNZO is "dead capital" — users must choose between
10//! earning staking rewards and participating in DeFi. Every competitive PoS chain in
11//! 2026 offers liquid staking (Ethereum's stETH, Solana's mSOL/jitoSOL, etc.).
12//!
13//! # stTNZO Token
14//!
15//! stTNZO is a rebasing token that represents a claim on staked TNZO plus accrued
16//! rewards. The exchange rate between stTNZO and TNZO increases over time as
17//! staking rewards accumulate.
18//!
19//! - **Mint**: Deposit TNZO → receive stTNZO at current exchange rate
20//! - **Burn**: Return stTNZO → receive TNZO at current exchange rate (after unbonding)
21//! - **Exchange rate**: `stTNZO_value = stTNZO_amount * exchange_rate`
22//!   where `exchange_rate = total_underlying_wei / total_sttnzo_supply`
23//!
24//! # Architecture
25//!
26//! 1. User deposits TNZO into the liquid staking pool
27//! 2. Pool stakes TNZO across multiple validators (diversification)
28//! 3. User receives stTNZO at the current exchange rate
29//! 4. Staking rewards flow into the pool, increasing the exchange rate
30//! 5. A small fee (default 10%) is taken from rewards for the protocol treasury
31//! 6. User can request withdrawal — starts unbonding period, then receives TNZO
32
33use 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
41/// Singleton key for the `LiquidStakingConfig` under `CF_TOKENS`.
42const LIQUID_CONFIG_KEY: &[u8] = b"liquid:config";
43/// Singleton key for aggregate pool totals (supply / underlying / fees /
44/// rewards) serialized as `LiquidStakingTotals`.
45const LIQUID_TOTALS_KEY: &[u8] = b"liquid:totals";
46/// Per-holder stTNZO balance prefix: `liquid:bal:<address-bytes>`.
47const LIQUID_BALANCE_PREFIX: &[u8] = b"liquid:bal:";
48/// Per-validator delegation prefix: `liquid:val:<address-bytes>`.
49const LIQUID_DELEGATION_PREFIX: &[u8] = b"liquid:val:";
50/// Per-withdrawal-request prefix: `liquid:wr:<request_id>`.
51const LIQUID_WITHDRAWAL_PREFIX: &[u8] = b"liquid:wr:";
52
53/// Compact aggregate of pool totals — persisted as a single JSON value
54/// under `LIQUID_TOTALS_KEY` so deposit / withdraw / reward paths only do
55/// one totals round-trip per mutation.
56#[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
64/// stTNZO decimals (same as TNZO: 18)
65pub const STTNZO_DECIMALS: u8 = 18;
66
67/// One stTNZO in smallest unit
68pub const ONE_STTNZO: u128 = 1_000_000_000_000_000_000;
69
70/// Default protocol fee on staking rewards (basis points, 1000 = 10%)
71pub const DEFAULT_PROTOCOL_FEE_BPS: u32 = 1000;
72
73/// Liquid staking pool configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LiquidStakingConfig {
76    /// Protocol fee on rewards (basis points)
77    pub protocol_fee_bps: u32,
78    /// Minimum deposit amount (in TNZO smallest unit)
79    pub min_deposit: u128,
80    /// Maximum total deposits (pool cap, 0 = unlimited)
81    pub max_total_deposits: u128,
82    /// Unbonding period in milliseconds (mirrors native staking)
83    pub unbonding_period_ms: i64,
84    /// Maximum number of validators to delegate to
85    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, // 0.1 TNZO minimum
93            max_total_deposits: 0, // Unlimited
94            unbonding_period_ms: 7 * 24 * 60 * 60 * 1000, // 7 days
95            max_validators: 50,
96        }
97    }
98}
99
100/// A pending withdrawal request
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct WithdrawalRequest {
103    /// Requester address
104    pub requester: Address,
105    /// stTNZO amount being burned
106    pub sttnzo_amount: u128,
107    /// TNZO amount to receive (calculated at request time using exchange rate)
108    pub tnzo_amount: u128,
109    /// Timestamp when unbonding completes
110    pub unbonding_complete_at: Timestamp,
111    /// Whether the withdrawal has been claimed
112    pub claimed: bool,
113    /// Request ID
114    pub request_id: String,
115}
116
117/// Liquid staking pool manager
118///
119/// Manages the stTNZO liquid staking derivative for Tenzro Network.
120pub struct LiquidStakingPool {
121    /// Configuration
122    config: parking_lot::RwLock<LiquidStakingConfig>,
123
124    /// stTNZO balances (holder address → stTNZO balance)
125    sttnzo_balances: DashMap<Address, u128>,
126
127    /// Total stTNZO supply
128    total_sttnzo_supply: parking_lot::RwLock<u128>,
129
130    /// Total underlying TNZO in the pool (staked + rewards - fees)
131    total_underlying_wei: parking_lot::RwLock<u128>,
132
133    /// Total protocol fees collected (TNZO)
134    total_protocol_fees: parking_lot::RwLock<u128>,
135
136    /// Pending withdrawal requests
137    withdrawal_requests: DashMap<String, WithdrawalRequest>,
138
139    /// Validators the pool delegates to (address → delegation amount)
140    validator_delegations: DashMap<Address, u128>,
141
142    /// Total rewards distributed through the pool
143    total_rewards_distributed: parking_lot::RwLock<u128>,
144
145    /// Optional persistent storage backend. When wired, every mutating
146    /// path (deposit / request_withdrawal / claim_withdrawal /
147    /// distribute_rewards / add_validator / transfer / config update)
148    /// write-throughs to `CF_TOKENS`. Hydration on `with_storage` rebuilds
149    /// totals, balances, validator delegations, and pending withdrawals
150    /// from the same prefixes.
151    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    /// Create a new liquid staking pool
170    pub fn new(config: LiquidStakingConfig) -> Result<Self> {
171        // Validate protocol fee (0-50%)
172        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    /// Create a liquid staking pool with persistent backing in `CF_TOKENS`.
195    /// Hydrates config, aggregate totals, per-holder balances, validator
196    /// delegations, and pending withdrawal requests on construction.
197    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        // Config: hydrate if present, otherwise persist current default.
231        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        // Aggregate totals.
248        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        // Per-holder balances.
262        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        // Validator delegations.
279        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        // Pending withdrawal requests.
296        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    /// Update the config (governance-driven). Validates and persists.
450    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    /// Get the current exchange rate: TNZO per stTNZO.
464    ///
465    /// `exchange_rate = total_underlying_wei / total_sttnzo_supply`
466    ///
467    /// Returns the rate as a fixed-point number with 18 decimals.
468    /// A rate of `1_000_000_000_000_000_000` (10^18) means 1:1.
469    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            // Initial rate is 1:1
475            ONE_STTNZO
476        } else {
477            // rate = underlying * ONE_STTNZO / supply
478            // To avoid overflow when underlying is large, split the division:
479            // underlying / supply gives the integer TNZO-per-stTNZO ratio
480            // (underlying % supply) * ONE_STTNZO / supply gives the fractional part
481            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    /// Deposit TNZO and receive stTNZO.
492    ///
493    /// The amount of stTNZO minted is calculated from the current exchange rate:
494    /// `sttnzo_amount = tnzo_amount * 10^18 / exchange_rate`
495    pub fn deposit(&self, depositor: Address, tnzo_amount: u128) -> Result<u128> {
496        let config = self.config.read();
497
498        // Validate minimum deposit
499        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        // Check pool cap
507        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        // Calculate stTNZO to mint
518        // sttnzo = tnzo_amount * ONE_STTNZO / rate
519        // To avoid overflow (both tnzo_amount and ONE_STTNZO can be ~10^18),
520        // use: sttnzo = tnzo_amount / rate * ONE_STTNZO + (tnzo_amount % rate) * ONE_STTNZO / rate
521        let rate = self.exchange_rate();
522        let sttnzo_amount = if rate == 0 {
523            tnzo_amount // First deposit: 1:1
524        } 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        // Mint stTNZO to depositor
546        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        // Update totals
553        *self.total_sttnzo_supply.write() += sttnzo_amount;
554        *self.total_underlying_wei.write() += tnzo_amount;
555
556        // Persist holder balance + aggregate totals.
557        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    /// Request withdrawal: burn stTNZO and start unbonding.
573    ///
574    /// The TNZO amount is calculated from the current exchange rate at request time.
575    /// User must wait the unbonding period before claiming.
576    pub fn request_withdrawal(&self, requester: Address, sttnzo_amount: u128) -> Result<WithdrawalRequest> {
577        // Check balance
578        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        // Calculate TNZO amount at current rate
590        // tnzo = sttnzo_amount * rate / ONE_STTNZO
591        // To avoid overflow, split: quotient * rate + remainder * rate / ONE_STTNZO
592        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        // Burn stTNZO
607        let new_balance = balance - sttnzo_amount;
608        self.sttnzo_balances.insert(requester, new_balance);
609        *self.total_sttnzo_supply.write() -= sttnzo_amount;
610
611        // Don't subtract from underlying yet — wait until claim
612
613        // Create withdrawal request
614        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        // Persist updated holder balance, aggregate totals, and the new
633        // withdrawal request.
634        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    /// Claim a completed withdrawal after the unbonding period.
653    ///
654    /// Returns the TNZO amount to transfer to the user.
655    pub fn claim_withdrawal(&self, request_id: &str) -> Result<(Address, u128)> {
656        // Snapshot + mutate under a short-lived RefMut, then drop the shard
657        // lock before persisting (which itself takes locks inside the
658        // storage backend). Same pattern as `governance::execute_proposal`.
659        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        // Subtract from underlying pool
690        *self.total_underlying_wei.write() -= tnzo_amount.min(*self.total_underlying_wei.read());
691
692        // Persist the claimed-flag flip and updated totals.
693        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    /// Distribute staking rewards to the pool.
709    ///
710    /// This is called by the reward distributor when staking rewards are earned.
711    /// The rewards increase the exchange rate (underlying TNZO goes up,
712    /// stTNZO supply stays the same).
713    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        // Calculate protocol fee
725        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        // Add staker portion to underlying (increases exchange rate)
732        *self.total_underlying_wei.write() += staker_reward;
733
734        // Track protocol fees
735        *self.total_protocol_fees.write() += protocol_fee;
736
737        // Track total rewards
738        *self.total_rewards_distributed.write() += reward_amount;
739
740        // Persist updated aggregates.
741        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    /// Get stTNZO balance for an address
760    pub fn balance_of(&self, address: &Address) -> u128 {
761        self.sttnzo_balances.get(address).map(|v| *v).unwrap_or(0)
762    }
763
764    /// Get the TNZO value of a stTNZO amount at the current exchange rate
765    pub fn tnzo_value(&self, sttnzo_amount: u128) -> u128 {
766        let rate = self.exchange_rate();
767        // Avoid overflow: split into quotient and remainder
768        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    /// Get pool statistics
775    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    /// Add a validator delegation
790    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    /// Get all validator delegations
802    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    /// Transfer stTNZO between addresses
809    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    /// List all pending (unclaimed) withdrawal requests for a holder.
836    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    /// Look up a single withdrawal request by ID.
845    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    /// Read a snapshot of the current liquid staking config.
850    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/// Reward distribution result
863#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct RewardDistribution {
865    /// Total reward amount
866    pub total_reward: u128,
867    /// Amount that went to stakers (increases exchange rate)
868    pub staker_share: u128,
869    /// Amount taken as protocol fee
870    pub protocol_fee: u128,
871    /// New exchange rate after distribution
872    pub new_exchange_rate: u128,
873}
874
875/// Liquid staking pool statistics
876#[derive(Debug, Clone, Serialize, Deserialize)]
877pub struct LiquidStakingStats {
878    /// Total stTNZO in circulation
879    pub total_sttnzo_supply: u128,
880    /// Total TNZO underlying the pool
881    pub total_underlying_wei: u128,
882    /// Current exchange rate (TNZO per stTNZO, 18 decimals)
883    pub exchange_rate: u128,
884    /// Total protocol fees collected
885    pub total_protocol_fees: u128,
886    /// Total rewards distributed through the pool
887    pub total_rewards_distributed: u128,
888    /// Number of stTNZO holders
889    pub holder_count: u64,
890    /// Number of pending withdrawal requests
891    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 // 10^18
900    }
901
902    #[test]
903    fn test_initial_exchange_rate() {
904        let pool = LiquidStakingPool::default();
905        assert_eq!(pool.exchange_rate(), ONE_STTNZO); // 1:1
906    }
907
908    #[test]
909    fn test_deposit() {
910        let pool = LiquidStakingPool::default();
911        let user = Address::new([1u8; 32]);
912
913        // First deposit: 1000 TNZO → should get 1000 stTNZO (1:1)
914        let deposit_amount = 1000 * one_tnzo();
915        let sttnzo = pool.deposit(user, deposit_amount).unwrap();
916
917        assert_eq!(sttnzo, deposit_amount); // 1:1 initial rate
918        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); // 1 wei, way below minimum
929        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        // Deposit 1000 TNZO
938        let deposit = 1000 * one_tnzo();
939        pool.deposit(user, deposit).unwrap();
940
941        let rate_before = pool.exchange_rate();
942
943        // Distribute 100 TNZO in rewards (10% = 10 TNZO protocol fee)
944        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        // User's stTNZO should now be worth more TNZO
950        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        // Deposit
960        let deposit = 1000 * one_tnzo();
961        pool.deposit(user, deposit).unwrap();
962
963        // Request withdrawal of half
964        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); // 1:1 rate
969        assert!(!request.claimed);
970
971        // Balance should be reduced
972        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        // 100 TNZO reward, 10% fee = 10 TNZO
1007        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        // Alice deposits first
1034        pool.deposit(alice, 1000 * one_tnzo()).unwrap();
1035
1036        // Rewards come in
1037        pool.distribute_rewards(100 * one_tnzo()).unwrap();
1038
1039        // Bob deposits now — should get less stTNZO per TNZO
1040        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}