code/pallets/subtensor/src/staking/basket_views.rs

basket_views.rs

190 lines · 8,522 bytes · 67dcf7f791RawGitHub
//! 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<T: Config> Pallet<T> {
    /// 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<Option<u64>, DispatchError> {
        if alpha == 0 {
            return Ok(Some(0));
        }
        if netuid.is_root() || SubnetMechanism::<T>::get(netuid) != 1 {
            return Ok(Some(alpha));
        }
        #[cfg(test)]
        crate::tests::mock::inc_basket_quote_ops();

        let maximum = T::SwapInterface::max_swap_input::<GetTaoForAlpha<T>>(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::<T>::with_amount(alpha);
            T::SwapInterface::swap(
                netuid.into(),
                order,
                T::SwapInterface::min_price::<TaoBalance>(),
                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<u64, DispatchError> {
        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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::try_get(NetUid::ROOT, hotkey)
            .ok()
            .map(|uid| Weights::<T>::get(NetUidStorageIndex::ROOT, uid))
            .unwrap_or_default()
            .into_iter()
            .map(|(dest, weight)| (NetUid::from(dest), weight))
            .collect()
    }
}