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

deferred_dividends.rs

686 lines · 30,435 bytes · d967d13588RawGitHub
//! Entitlements to dividend alpha whose conversion into basket shares is deferred.
//!
//! For each origin/generation, `owed_alpha = root_stake * rate - debt`.
//! A root stake change of `delta` adds `delta * rate` to the account's signed debt,
//! so neither arrivals nor departures change the alpha already earned. Rates are
//! activated lazily before the first stake/fund change that cannot settle a batch;
//! subsequent epoch credits record their earning stake immediately. Unsnapshotted
//! sub-threshold dust retains the existing queue policy.
//!
//! Once conversion is safe, `F = minted_shares / remaining_alpha` advances
//! BasketRate by `rate * F`. Signed debts become BasketClaimed corrections by
//! `debt * F`, lazily on the account's next operation. A generation's conversion
//! remains until every debt using it is consumed. This avoids walking stakers.
//! A claimant may instead sell their own alpha directly. Dissolution uses the
//! same accounting with a separate TAO accumulator, outside the basket NAV.

use super::*;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use substrate_fixed::types::{I64F64, I96F32};
use subtensor_swap_interface::SwapHandler;

/// Unpriced alpha earned per unit of real root stake. Stake changes adjust a
/// separate signed debt, preserving the entitlement earned before the change.
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
pub struct DeferredBasketState {
    pub generation: u64,
    pub rate: I64F64,
    pub debt_count: u32,
}

#[derive(Clone, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
pub struct DeferredBasketDebt {
    pub generation: u64,
    pub amount: I64F64,
}

/// Conversion is retained until every signed debt has been converted to a
/// BasketClaimed watermark. Accounts with unchanged stake need no debt row.
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
pub struct DeferredBasketConversion {
    pub shares_per_alpha: I64F64,
    pub cash_per_alpha: I64F64,
    pub debt_count: u32,
}

impl<T: Config> Pallet<T> {
    /// Make a pending credit independent of future root stake and escrow changes.
    /// The fund's own dividend is credited in alpha immediately; it belongs to
    /// the shareholders who held the fund when the dividend was earned.
    pub(crate) fn snapshot_deferred_dividends(hotkey: &T::AccountId) -> DispatchResult {
        let pending: Vec<_> = PendingBasketDeposits::<T>::iter_prefix(hotkey)
            .take((crate::MAX_ROOT_CLAIM_WORK as usize).saturating_add(1))
            .collect();
        ensure!(
            pending.len() <= crate::MAX_ROOT_CLAIM_WORK as usize,
            Error::<T>::BasketDepositPending
        );
        for (netuid, alpha) in pending {
            if !DeferredBasketRates::<T>::contains_key(hotkey, netuid) {
                Self::record_deferred_dividend(hotkey, netuid, alpha)?;
            }
        }
        Ok(())
    }

    fn record_deferred_dividend(
        hotkey: &T::AccountId,
        netuid: NetUid,
        alpha: AlphaBalance,
    ) -> DispatchResult {
        if alpha.is_zero() {
            return Ok(());
        }
        ensure!(
            DeferredBasketRates::<T>::contains_key(hotkey, netuid)
                || DeferredBasketRates::<T>::iter_prefix(hotkey)
                    .take(crate::MAX_ROOT_CLAIM_WORK as usize)
                    .count()
                    < crate::MAX_ROOT_CLAIM_WORK as usize,
            Error::<T>::BasketDepositPending
        );
        let escrow = Self::get_beta_escrow_account_id();
        let escrow_stake =
            Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, &escrow, NetUid::ROOT)
                .to_u64();
        let total = Self::get_stake_for_hotkey_on_subnet(hotkey, NetUid::ROOT).to_u64();
        let root = total.saturating_sub(escrow_stake);
        let real_alpha = Self::mul_div_u64(alpha.to_u64(), root, total);
        let fund_alpha = alpha.to_u64().saturating_sub(real_alpha);
        if fund_alpha > 0 && escrow_stake > 0 && BasketShares::<T>::get(hotkey) > 0 {
            Self::increase_stake_for_hotkey_and_coldkey_on_subnet(
                hotkey,
                &escrow,
                netuid,
                fund_alpha.into(),
            );
        } else if fund_alpha > 0 {
            Self::recycle_subnet_alpha(netuid, fund_alpha.into());
        }
        if real_alpha == 0 {
            PendingBasketDeposits::<T>::remove(hotkey, netuid);
            return Ok(());
        }
        let increment = I64F64::checked_from_num(real_alpha)
            .ok_or(sp_runtime::ArithmeticError::Overflow)?
            .checked_div(
                I64F64::checked_from_num(root).ok_or(sp_runtime::ArithmeticError::Overflow)?,
            )
            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        let state = match DeferredBasketRates::<T>::get(hotkey, netuid) {
            Some(mut state) => {
                state.rate = state
                    .rate
                    .checked_add(increment)
                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                state
            }
            None => {
                let generation = DeferredBasketGeneration::<T>::get(hotkey)
                    .checked_add(1)
                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                DeferredBasketGeneration::<T>::insert(hotkey, generation);
                DeferredBasketState {
                    generation,
                    rate: increment,
                    debt_count: 0,
                }
            }
        };
        DeferredBasketRates::<T>::insert(hotkey, netuid, state);
        // Dissolution scans this fund index even if all shareholders/root
        // stakers have exited and only deferred dividend rights remain.
        BasketShares::<T>::mutate(hotkey, |_| {});
        PendingBasketDeposits::<T>::insert(hotkey, netuid, AlphaBalance::from(real_alpha));
        Ok(())
    }

    /// Coinbase uses this entry point; a retry must only requeue alpha, never
    /// record the same earned dividend a second time.
    pub(crate) fn enqueue_root_dividend(
        hotkey: &T::AccountId,
        netuid: NetUid,
        alpha: AlphaBalance,
    ) {
        if DeferredBasketRates::<T>::iter_prefix(hotkey)
            .next()
            .is_none()
        {
            Self::enqueue_basket_deposit(hotkey, netuid, alpha);
            return;
        }
        let old = PendingBasketDeposits::<T>::get(hotkey, netuid);
        // All writes, including the escrow yield, roll back on accounting failure.
        let outcome = frame_support::storage::with_transaction(|| {
            match Self::record_deferred_dividend(hotkey, netuid, alpha) {
                Ok(()) => {
                    PendingBasketDeposits::<T>::mutate(hotkey, netuid, |pending| {
                        *pending = pending.saturating_add(old)
                    });
                    frame_support::storage::TransactionOutcome::Commit(Ok(()))
                }
                Err(error) => frame_support::storage::TransactionOutcome::Rollback(Err(error)),
            }
        });
        // Overflow is not a reason to silently assign the credit to a later
        // claimant base. Leave the original accounting intact and recycle it.
        if outcome.is_err() {
            Self::recycle_subnet_alpha(netuid, alpha);
        }
    }

    fn converted_debt(debt: &DeferredBasketDebt, factor: I64F64) -> Result<i128, DispatchError> {
        // Round toward +infinity: negative corrections cannot grant more shares
        // than earned, positive corrections cannot let a new staker capture dust.
        let shares = debt
            .amount
            .checked_mul(factor)
            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        Ok(shares
            .checked_ceil()
            .ok_or(sp_runtime::ArithmeticError::Overflow)?
            .saturating_to_num::<i128>())
    }

    pub(super) fn deferred_claimed_correction(
        hotkey: &T::AccountId,
        coldkey: &T::AccountId,
    ) -> i128 {
        DeferredBasketDebts::<T>::get(hotkey, coldkey)
            .iter()
            .fold(
                0i128,
                |sum, (netuid, debt)| match DeferredBasketConversions::<T>::get((
                    hotkey,
                    netuid,
                    debt.generation,
                )) {
                    Some(conversion) => sum.saturating_add(
                        Self::converted_debt(debt, conversion.shares_per_alpha)
                            .unwrap_or(i128::MAX),
                    ),
                    None => sum,
                },
            )
    }

    pub(super) fn settle_deferred_debts(
        hotkey: &T::AccountId,
        coldkey: &T::AccountId,
    ) -> DispatchResult {
        frame_support::storage::with_storage_layer(|| {
            DeferredBasketDebts::<T>::try_mutate_exists(
                hotkey,
                coldkey,
                |entry| -> DispatchResult {
                    let Some(debts) = entry else {
                        return Ok(());
                    };
                    let mut completed = Vec::new();
                    for (netuid, debt) in debts.iter() {
                        if let Some(mut conversion) =
                            DeferredBasketConversions::<T>::get((hotkey, netuid, debt.generation))
                        {
                            let correction =
                                Self::converted_debt(debt, conversion.shares_per_alpha)?;
                            BasketClaimed::<T>::try_mutate(
                                hotkey,
                                coldkey,
                                |claimed| -> DispatchResult {
                                    *claimed = claimed
                                        .checked_add(correction)
                                        .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                                    Ok(())
                                },
                            )?;
                            let cash = Self::converted_debt(debt, conversion.cash_per_alpha)?;
                            if cash != 0 {
                                DeferredBasketCashClaimed::<T>::try_mutate(
                                    hotkey,
                                    coldkey,
                                    |claimed| -> DispatchResult {
                                        *claimed = claimed
                                            .checked_add(cash)
                                            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                                        Ok(())
                                    },
                                )?;
                            }
                            conversion.debt_count = conversion.debt_count.saturating_sub(1);
                            if conversion.debt_count == 0 {
                                DeferredBasketConversions::<T>::remove((
                                    hotkey,
                                    netuid,
                                    debt.generation,
                                ));
                            } else {
                                DeferredBasketConversions::<T>::insert(
                                    (hotkey, netuid, debt.generation),
                                    conversion,
                                );
                            }
                            completed.push(*netuid);
                        }
                    }
                    for netuid in completed {
                        debts.remove(&netuid);
                    }
                    if debts.is_empty() {
                        *entry = None;
                    }
                    Ok(())
                },
            )
        })
    }

    pub(crate) fn rebase_deferred_dividends(
        hotkey: &T::AccountId,
        coldkey: &T::AccountId,
        delta: i128,
    ) -> DispatchResult {
        Self::settle_deferred_debts(hotkey, coldkey)?;
        let cash = DeferredBasketCashRate::<T>::get(hotkey)
            .checked_mul(
                I64F64::checked_from_num(delta).ok_or(sp_runtime::ArithmeticError::Overflow)?,
            )
            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        if cash != I64F64::from_num(0) {
            DeferredBasketCashClaimed::<T>::try_mutate(
                hotkey,
                coldkey,
                |claimed| -> DispatchResult {
                    *claimed = claimed
                        .checked_add(
                            cash.checked_ceil()
                                .ok_or(sp_runtime::ArithmeticError::Overflow)?
                                .saturating_to_num::<i128>(),
                        )
                        .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                    Ok(())
                },
            )?;
        }
        if DeferredBasketRates::<T>::iter_prefix(hotkey)
            .next()
            .is_none()
        {
            return Ok(());
        }
        DeferredBasketDebts::<T>::try_mutate(hotkey, coldkey, |debts| -> DispatchResult {
            for (netuid, mut state) in DeferredBasketRates::<T>::iter_prefix(hotkey) {
                let correction = state
                    .rate
                    .checked_mul(
                        I64F64::checked_from_num(delta)
                            .ok_or(sp_runtime::ArithmeticError::Overflow)?,
                    )
                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                if let Some(debt) = debts.get_mut(&netuid) {
                    ensure!(
                        debt.generation == state.generation,
                        DispatchError::Other("unsettled dividend generation")
                    );
                    debt.amount = debt
                        .amount
                        .checked_add(correction)
                        .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                } else {
                    debts
                        .try_insert(
                            netuid,
                            DeferredBasketDebt {
                                generation: state.generation,
                                amount: correction,
                            },
                        )
                        .map_err(|_| Error::<T>::BasketDepositPending)?;
                    state.debt_count = state
                        .debt_count
                        .checked_add(1)
                        .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                    DeferredBasketRates::<T>::insert(hotkey, netuid, state);
                }
            }
            Ok(())
        })
    }

    /// Bind a settled credit's share mint to the root stake that earned its alpha,
    /// without iterating stakers. Closed generations are applied lazily per coldkey.
    pub(super) fn convert_deferred_dividend(
        hotkey: &T::AccountId,
        netuid: NetUid,
        alpha: u64,
        shares: u64,
    ) -> DispatchResult {
        let state = DeferredBasketRates::<T>::take(hotkey, netuid)
            .ok_or(DispatchError::Other("missing dividend entitlement"))?;
        let factor = I64F64::checked_from_num(shares)
            .ok_or(sp_runtime::ArithmeticError::Overflow)?
            .checked_div(
                I64F64::checked_from_num(alpha).ok_or(sp_runtime::ArithmeticError::Overflow)?,
            )
            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        let increment = state
            .rate
            .checked_mul(factor)
            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        BasketRate::<T>::try_mutate(hotkey, |rate| -> DispatchResult {
            *rate = rate
                .checked_add(I96F32::from_num(increment))
                .ok_or(sp_runtime::ArithmeticError::Overflow)?;
            Ok(())
        })?;
        if state.debt_count > 0 {
            DeferredBasketConversions::<T>::insert(
                (hotkey, netuid, state.generation),
                DeferredBasketConversion {
                    shares_per_alpha: factor,
                    cash_per_alpha: I64F64::from_num(0),
                    debt_count: state.debt_count,
                },
            );
        }
        Ok(())
    }

    /// A claimant can redeem their own deferred alpha without repricing the
    /// basket. Only that claimant bears the sale price/slippage, so this needs
    /// no common EMA window and cannot transfer value from basket shareholders.
    pub(super) fn claim_deferred_dividends(
        hotkey: &T::AccountId,
        coldkey: &T::AccountId,
        ignore_minimum: bool,
    ) -> Result<(u64, u32), DispatchError> {
        let states: Vec<_> = DeferredBasketRates::<T>::iter_prefix(hotkey).collect();
        Self::settle_deferred_debts(hotkey, coldkey)?;
        let mut total = 0u64;
        let mut rows = 0u32;
        for (netuid, state) in states {
            let stake =
                Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, NetUid::ROOT)
                    .to_u64();
            let debt = DeferredBasketDebts::<T>::get(hotkey, coldkey)
                .get(&netuid)
                .map_or(I64F64::from_num(0), |debt| debt.amount);
            let owed = state
                .rate
                .checked_mul(
                    I64F64::checked_from_num(stake).ok_or(sp_runtime::ArithmeticError::Overflow)?,
                )
                .and_then(|earned| earned.checked_sub(debt))
                .ok_or(sp_runtime::ArithmeticError::Overflow)?;
            if owed <= I64F64::from_num(0) {
                continue;
            }
            let alpha = owed
                .saturating_to_num::<u64>()
                .min(PendingBasketDeposits::<T>::get(hotkey, netuid).to_u64());
            if alpha == 0 {
                continue;
            }
            let value = Self::try_realizable_tao_for_alpha(netuid, alpha)?;
            let Some(value) = value else {
                continue;
            };
            if value == 0
                || (!ignore_minimum
                    && I96F32::from_num(value) < RootClaimableThreshold::<T>::get(NetUid::ROOT))
            {
                continue;
            }
            Self::ensure_staking_hotkeys_can_grow(coldkey, hotkey)?;
            let tao =
                frame_support::storage::with_storage_layer(|| -> Result<u64, DispatchError> {
                    // Reserve the paid entitlement before the sale and before its TAO
                    // becomes new root stake. The stake rebase below is separate.
                    DeferredBasketDebts::<T>::try_mutate(
                        hotkey,
                        coldkey,
                        |debts| -> DispatchResult {
                            if let Some(debt) = debts.get_mut(&netuid) {
                                debt.amount = debt
                                    .amount
                                    .checked_add(
                                        I64F64::checked_from_num(alpha)
                                            .ok_or(sp_runtime::ArithmeticError::Overflow)?,
                                    )
                                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                            } else {
                                debts
                                    .try_insert(
                                        netuid,
                                        DeferredBasketDebt {
                                            generation: state.generation,
                                            amount: I64F64::checked_from_num(alpha)
                                                .ok_or(sp_runtime::ArithmeticError::Overflow)?,
                                        },
                                    )
                                    .map_err(|_| Error::<T>::BasketDepositPending)?;
                                DeferredBasketRates::<T>::mutate(hotkey, netuid, |state| {
                                    if let Some(state) = state {
                                        state.debt_count = state.debt_count.saturating_add(1);
                                    }
                                });
                            }
                            Ok(())
                        },
                    )?;
                    let tao = Self::sell_basket_alpha_for_root_tao(netuid, alpha.into())?.to_u64();
                    ensure!(tao > 0, Error::<T>::AmountTooLow);
                    PendingBasketDeposits::<T>::mutate(hotkey, netuid, |pending| {
                        *pending = pending.saturating_sub(alpha.into())
                    });
                    SubnetRootSellTao::<T>::mutate(netuid, |total| {
                        *total = total.saturating_add(tao.into())
                    });
                    Self::increase_stake_for_hotkey_and_coldkey_on_subnet(
                        hotkey,
                        coldkey,
                        NetUid::ROOT,
                        tao.into(),
                    );
                    Self::credit_root_reserves(tao.into());
                    Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey(
                        hotkey, coldkey, tao,
                    )?;
                    Self::touch_root_stake_age(coldkey, hotkey);
                    if PendingBasketDeposits::<T>::get(hotkey, netuid).is_zero() {
                        PendingBasketDeposits::<T>::remove(hotkey, netuid);
                        Self::convert_deferred_dividend(hotkey, netuid, 1, 0)?;
                    }
                    Ok(tao)
                })?;
            total = total
                .checked_add(tao)
                .ok_or(sp_runtime::ArithmeticError::Overflow)?;
            rows = rows.saturating_add(1);
        }
        let stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, NetUid::ROOT)
            .to_u64();
        let owed_cash = DeferredBasketCashRate::<T>::get(hotkey)
            .checked_mul(
                I64F64::checked_from_num(stake).ok_or(sp_runtime::ArithmeticError::Overflow)?,
            )
            .ok_or(sp_runtime::ArithmeticError::Overflow)?
            .saturating_to_num::<i128>()
            .saturating_sub(DeferredBasketCashClaimed::<T>::get(hotkey, coldkey));
        let cash = u64::try_from(owed_cash)
            .unwrap_or(0)
            .min(DeferredBasketCashReserve::<T>::get(hotkey));
        if cash > 0
            && (ignore_minimum
                || I96F32::from_num(cash) >= RootClaimableThreshold::<T>::get(NetUid::ROOT))
        {
            Self::ensure_staking_hotkeys_can_grow(coldkey, hotkey)?;
            frame_support::storage::with_storage_layer(|| -> DispatchResult {
                DeferredBasketCashClaimed::<T>::try_mutate(
                    hotkey,
                    coldkey,
                    |claimed| -> DispatchResult {
                        *claimed = claimed
                            .checked_add(i128::from(cash))
                            .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                        Ok(())
                    },
                )?;
                DeferredBasketCashReserve::<T>::mutate(hotkey, |reserve| {
                    *reserve = reserve.saturating_sub(cash)
                });
                Self::increase_stake_for_hotkey_and_coldkey_on_subnet(
                    hotkey,
                    coldkey,
                    NetUid::ROOT,
                    cash.into(),
                );
                Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey(hotkey, coldkey, cash)?;
                Self::touch_root_stake_age(coldkey, hotkey);
                Ok(())
            })?;
            total = total
                .checked_add(cash)
                .ok_or(sp_runtime::ArithmeticError::Overflow)?;
        }
        if total > 0 {
            BasketRedeemedTao::<T>::mutate(hotkey, |redeemed| {
                *redeemed = redeemed.saturating_add(total.into())
            });
            Self::deposit_event(Event::BasketClaimed {
                hotkey: hotkey.clone(),
                coldkey: coldkey.clone(),
                tao: total.into(),
            });
        }
        Ok((total, rows))
    }

    /// Close a dissolving origin into a separate cash entitlement, never into
    /// basket shares priced against unrelated (possibly manipulated) pools.
    pub(crate) fn dissolve_deferred_dividend(
        hotkey: &T::AccountId,
        netuid: NetUid,
        alpha: AlphaBalance,
    ) -> DispatchResult {
        frame_support::storage::with_storage_layer(|| -> DispatchResult {
            let state = DeferredBasketRates::<T>::take(hotkey, netuid)
                .ok_or(DispatchError::Other("missing dividend entitlement"))?;
            let total_stake_before_sale =
                (!NetworksAdded::<T>::get(netuid)).then(TotalStake::<T>::get);
            let tao = if alpha.is_zero() {
                0
            } else {
                match Self::sell_basket_alpha_for_root_tao(netuid, alpha) {
                    Ok(tao) => tao.to_u64(),
                    Err(error)
                        if T::SwapInterface::classify_failure(&error)
                            == subtensor_swap_interface::SwapFailureKind::TerminalLiquidity =>
                    {
                        Self::burn_subnet_alpha(netuid, alpha);
                        Self::deposit_event(Event::BasketAlphaWrittenOff {
                            hotkey: hotkey.clone(),
                            netuid,
                            alpha,
                        });
                        0
                    }
                    Err(error) => return Err(error),
                }
            };
            // Dissolve start already removed this subnet's reserves from TotalStake.
            if let Some(total) = total_stake_before_sale {
                TotalStake::<T>::put(total);
            }
            SubnetRootSellTao::<T>::mutate(netuid, |total| {
                *total = total.saturating_add(tao.into())
            });
            let factor = I64F64::checked_from_num(tao)
                .ok_or(sp_runtime::ArithmeticError::Overflow)?
                .checked_div(
                    I64F64::checked_from_num(alpha.to_u64().max(1))
                        .ok_or(sp_runtime::ArithmeticError::Overflow)?,
                )
                .ok_or(sp_runtime::ArithmeticError::Overflow)?;
            DeferredBasketCashRate::<T>::try_mutate(hotkey, |rate| -> DispatchResult {
                *rate = rate
                    .checked_add(
                        state
                            .rate
                            .checked_mul(factor)
                            .ok_or(sp_runtime::ArithmeticError::Overflow)?,
                    )
                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                Ok(())
            })?;
            DeferredBasketCashReserve::<T>::try_mutate(hotkey, |reserve| -> DispatchResult {
                *reserve = reserve
                    .checked_add(tao)
                    .ok_or(sp_runtime::ArithmeticError::Overflow)?;
                Ok(())
            })?;
            if state.debt_count > 0 {
                DeferredBasketConversions::<T>::insert(
                    (hotkey, netuid, state.generation),
                    DeferredBasketConversion {
                        shares_per_alpha: I64F64::from_num(0),
                        cash_per_alpha: factor,
                        debt_count: state.debt_count,
                    },
                );
            }
            Self::credit_root_reserves(tao.into());
            PendingBasketDeposits::<T>::remove(hotkey, netuid);
            Ok(())
        })
    }

    pub(super) fn move_deferred_dividends(old: &T::AccountId, new: &T::AccountId) -> u32 {
        let generation = DeferredBasketGeneration::<T>::take(old);
        DeferredBasketGeneration::<T>::insert(new, generation);
        let rates: Vec<_> = DeferredBasketRates::<T>::drain_prefix(old).collect();
        let debts: Vec<_> = DeferredBasketDebts::<T>::drain_prefix(old).collect();
        let conversions: Vec<_> = DeferredBasketConversions::<T>::drain_prefix((old,)).collect();
        let cash_claimed: Vec<_> = DeferredBasketCashClaimed::<T>::drain_prefix(old).collect();
        let rows = rates
            .len()
            .saturating_add(debts.len())
            .saturating_add(conversions.len())
            .saturating_add(cash_claimed.len()) as u32;
        let cash_rate = DeferredBasketCashRate::<T>::take(old);
        if cash_rate != I64F64::from_num(0) {
            DeferredBasketCashRate::<T>::insert(new, cash_rate);
        }
        let cash_reserve = DeferredBasketCashReserve::<T>::take(old);
        if cash_reserve != 0 {
            DeferredBasketCashReserve::<T>::insert(new, cash_reserve);
        }
        if !rates.is_empty() {
            BasketShares::<T>::mutate(new, |_| {});
        }
        for (coldkey, claimed) in cash_claimed {
            DeferredBasketCashClaimed::<T>::insert(new, &coldkey, claimed);
            if claimed < 0 && !StakingHotkeys::<T>::get(&coldkey).contains(new) {
                HotkeySwapBasketCredits::<T>::insert(&coldkey, new, ());
            }
        }
        for (netuid, state) in rates {
            DeferredBasketRates::<T>::insert(new, netuid, state);
        }
        for (coldkey, debt) in debts {
            let retained = debt
                .values()
                .any(|entry| entry.amount < I64F64::from_num(0));
            DeferredBasketDebts::<T>::insert(new, &coldkey, debt);
            if (HotkeySwapBasketCredits::<T>::take(&coldkey, old).is_some() || retained)
                && !StakingHotkeys::<T>::get(&coldkey).contains(new)
            {
                HotkeySwapBasketCredits::<T>::insert(&coldkey, new, ());
            }
        }
        for ((netuid, generation), conversion) in conversions {
            DeferredBasketConversions::<T>::insert((new, netuid, generation), conversion);
        }
        rows
    }
}