//! Beta basket valuation and read-only views (for RPC / dashboards). //! //! Everything here is side-effect free. The valuation primitives //! (`realizable_tao_for_alpha`, `get_validator_basket_nav_tao`, `basket_payout_from`) are //! also the single source of truth used by the money-moving paths in `claim_root.rs`, so //! deposit share pricing, redemption sizing, and what dashboards report can never diverge. use super::*; use frame_support::storage::{TransactionOutcome, with_transaction}; use sp_runtime::DispatchError; use subtensor_runtime_common::NetUidStorageIndex; use subtensor_swap_interface::{Order, SwapHandler}; impl Pallet { /// Realizable TAO value of `alpha` on `netuid`: the slippage-aware quote a full basket /// redemption would fetch right now, using the same fee-free protocol swap as the /// money-moving claim path, not the marked spot value `price * amount`. On a thin pool a /// tiny buy can push spot arbitrarily high, letting a marked NAV grow without bound (and /// saturate to `u64::MAX`); the realizable quote is bounded by the pool's TAO reserve, so /// NAV computed from it matches what the fund could actually pay out. pub fn realizable_tao_for_alpha(netuid: NetUid, alpha: u64) -> u64 { Self::try_realizable_tao_for_alpha(netuid, alpha) .ok() .flatten() .unwrap_or(0) } /// Fallible money-moving valuation. `Ok(None)` means the pool is terminally too shallow to /// realize even one atomic unit and the holding may be explicitly written off. Unknown or /// accounting failures remain `Err`; callers must not silently mark them to zero. pub(crate) fn try_realizable_tao_for_alpha( netuid: NetUid, alpha: u64, ) -> Result, DispatchError> { if alpha == 0 { return Ok(Some(0)); } if netuid.is_root() || SubnetMechanism::::get(netuid) != 1 { return Ok(Some(alpha)); } #[cfg(test)] crate::tests::mock::inc_basket_quote_ops(); let maximum = T::SwapInterface::max_swap_input::>(netuid).to_u64(); let quote = if alpha <= maximum { // Preserve the cheap single-swap simulation for ordinary holdings. Only an // oversized position needs the sequential reserve updates of the rollback overlay. let order = GetTaoForAlpha::::with_amount(alpha); T::SwapInterface::swap( netuid.into(), order, T::SwapInterface::min_price::(), true, true, ) .map(|result| result.amount_paid_out) } else { with_transaction(|| { TransactionOutcome::Rollback(Self::swap_basket_alpha_for_tao_chunks( netuid, alpha.into(), )) }) }; match quote { Ok(tao) => Ok(Some(tao.to_u64())), Err(err) if T::SwapInterface::classify_failure(&err) == subtensor_swap_interface::SwapFailureKind::TerminalLiquidity => { Ok(None) } Err(err) => Err(err), } } /// Fallible NAV for share pricing and claims. Terminal garbage contributes zero; every /// unknown valuation error aborts the money-moving operation. pub(crate) fn try_get_validator_basket_nav_tao( hotkey: &T::AccountId, ) -> Result { let mut nav = 0u64; for (netuid, alpha) in Self::get_basket_holdings(hotkey) { if let Some(value) = Self::try_realizable_tao_for_alpha(netuid, alpha.to_u64())? { nav = nav.saturating_add(value); } } Ok(nav) } /// Single source of truth for redemption sizing: a staker's owed shares are worth /// `owed * N / P` TAO (fund NAV over outstanding shares), capped at the NAV so a claim can /// never be marked above what the fund holds. pub fn basket_payout_from(owed_shares: u64, nav: u64, shares_total: u64) -> u64 { Self::mul_div_u64(owed_shares, nav, shares_total).min(nav) } /// A validator's fund NAV in TAO at realizable (slippage-aware) quotes. This is the single /// valuation used for both deposit share pricing and redemption sizing, so the two can /// never diverge under a manipulated spot price. pub fn get_validator_basket_nav_tao(hotkey: &T::AccountId) -> TaoBalance { let mut nav: u64 = 0; for (netuid, alpha) in Self::get_basket_holdings(hotkey) { nav = nav.saturating_add(Self::realizable_tao_for_alpha(netuid, alpha.to_u64())); } nav.into() } /// Current TAO payout a staker would realize (mark-to-market) by redeeming their owed /// shares on a validator. pub fn get_basket_payout_tao(hotkey: &T::AccountId, coldkey: &T::AccountId) -> u64 { let owed_shares = Self::get_basket_owed_shares(hotkey, coldkey); let shares_total = BasketShares::::get(hotkey); let nav: u64 = Self::get_validator_basket_nav_tao(hotkey).to_u64(); Self::basket_payout_from(owed_shares.min(shares_total), nav, shares_total) } /// Current TAO entitlement of a staker's unclaimed share of one subnet holding in a /// validator's basket. Claims sell the corresponding pro-rata alpha amount, but pay this /// full-liquidation-NAV fraction and retain any larger concavity surplus in the fund. pub fn get_basket_subnet_payout_tao( hotkey: &T::AccountId, coldkey: &T::AccountId, netuid: NetUid, ) -> u64 { let shares_total = BasketShares::::get(hotkey); if shares_total == 0 { return 0; } let owed_shares = Self::get_basket_owed_shares(hotkey, coldkey).min(shares_total); if owed_shares == 0 { return 0; } let escrow = Self::get_beta_escrow_account_id(); let holding = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, &escrow, netuid).to_u64(); let slot_nav = Self::realizable_tao_for_alpha(netuid, holding); Self::basket_payout_from(owed_shares, slot_nav, shares_total) } /// Total TAO a coldkey would realize by redeeming every beta basket it holds across all of /// its validators (mark-to-market). This is the "pending TAO owed" figure for a staker. pub fn get_root_basket_owed_tao(coldkey: &T::AccountId) -> TaoBalance { let mut total: u64 = 0; for hotkey in StakingHotkeys::::get(coldkey) { total = total.saturating_add(Self::get_basket_payout_tao(&hotkey, coldkey)); } total.into() } /// A validator's full basket breakdown: per subnet, the alpha held and its realizable /// TAO value. pub fn get_validator_basket(hotkey: &T::AccountId) -> Vec<(NetUid, AlphaBalance, TaoBalance)> { Self::get_basket_holdings(hotkey) .into_iter() .map(|(netuid, alpha)| { let tao = Self::realizable_tao_for_alpha(netuid, alpha.to_u64()); (netuid, alpha, tao.into()) }) .collect() } /// Network-wide total beta basket NAV across all validators, in TAO (mark-to-market). /// Sampling this over time yields the TAO/day flowing to root stakers. pub fn get_root_basket_total_nav_tao() -> TaoBalance { // Accumulate in u128 so per-validator values near u64::MAX cannot silently pin the // network-wide aggregate at the saturation ceiling. let mut nav: u128 = 0; for hotkey in BasketShares::::iter_keys() { nav = nav.saturating_add(u128::from( Self::get_validator_basket_nav_tao(&hotkey).to_u64(), )); } u64::try_from(nav).unwrap_or(u64::MAX).into() } /// A validator's beta basket weight vector `w`: the `(subnet, weight)` pairs it deploys its /// root dividends into (its curation strategy), exactly as stored. pub fn get_validator_root_weights(hotkey: &T::AccountId) -> Vec<(NetUid, u16)> { Uids::::try_get(NetUid::ROOT, hotkey) .ok() .map(|uid| Weights::::get(NetUidStorageIndex::ROOT, uid)) .unwrap_or_default() .into_iter() .map(|(dest, weight)| (NetUid::from(dest), weight)) .collect() } }