Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions pallets/subtensor/src/coinbase/tao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
/// - Reading colkey TAO balances
/// - Access to subnet TAO reserves
///
use frame_support::traits::{
Imbalance, LockableCurrency, WithdrawReasons,
fungible::Mutate,
tokens::{
Fortitude, Precision, Preservation,
fungible::{Balanced, Credit, Inspect},
use frame_support::{
storage::{TransactionOutcome, with_transaction},
traits::{
Imbalance, LockableCurrency, WithdrawReasons,
fungible::Mutate,
tokens::{
Fortitude, Precision, Preservation,
fungible::{Balanced, Credit, Inspect, Unbalanced},
},
},
};
use sp_runtime::traits::AccountIdConversion;
use sp_runtime::{DispatchError, DispatchResult};
use sp_runtime::traits::{AccountIdConversion, CheckedAdd};
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
use subtensor_runtime_common::{NetUid, TaoBalance};

use super::*;
Expand Down Expand Up @@ -237,6 +240,51 @@ impl<T: Config> Pallet<T> {
)
}

/// Atomically move TAO from one or more accounts into one destination without emitting balance
/// events.
///
/// The requested debits and aggregate credit cancel without changing total issuance. Normal
/// dust handling can still reduce issuance if a source account is reaped. The nested storage
/// transaction rolls every debit (including dust handling) back if any debit or the final
/// credit fails.
pub(crate) fn settle_tao_balances(
debits: &[(T::AccountId, BalanceOf<T>)],
destination: &T::AccountId,
) -> Result<BalanceOf<T>, DispatchError> {
let amount_to_credit = debits
.iter()
.try_fold(BalanceOf::<T>::zero(), |total, (_, amount)| {
total.checked_add(amount).ok_or(ArithmeticError::Overflow)
})?;

if amount_to_credit.is_zero() {
return Ok(amount_to_credit);
}

with_transaction(|| {
for (source, amount) in debits {
if let Err(err) = <T as Config>::Currency::decrease_balance(
source,
*amount,
Precision::Exact,
Preservation::Expendable,
Fortitude::Polite,
) {
return TransactionOutcome::Rollback(Err(err));
}
}

match <T as Config>::Currency::increase_balance(
destination,
amount_to_credit,
Precision::Exact,
) {
Ok(_) => TransactionOutcome::Commit(Ok(amount_to_credit)),
Err(err) => TransactionOutcome::Rollback(Err(err)),
}
})
}

/// Create TAO and return the imbalance.
///
/// The mint workflow is following:
Expand Down
72 changes: 51 additions & 21 deletions pallets/subtensor/src/staking/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ impl<T: Config> Pallet<T> {
// must also be credited to the root reserves.
let mut root_slot_tao: u64 = 0;
let mut swapped_tao: u64 = 0;
let mut tao_debits: Vec<(T::AccountId, TaoBalance)> = Vec::new();

for (netuid, slot_alpha) in holdings.iter() {
// This staker's pro-rata slice of the holding: slot_alpha * owed / P.
Expand All @@ -625,10 +626,12 @@ impl<T: Config> Pallet<T> {
}

// Sell the slice to TAO.
let tao = match Self::sell_basket_alpha_for_root_tao(*netuid, take.into()) {
Ok(tao) => tao,
Err(err) => return TransactionOutcome::Rollback(Err(err)),
};
let (source_account, tao) =
match Self::sell_basket_alpha_for_settlement(*netuid, take.into()) {
Ok(settlement) => settlement,
Err(err) => return TransactionOutcome::Rollback(Err(err)),
};
tao_debits.push((source_account, tao));

// Record root sell (reduces protocol cost).
SubnetRootSellTao::<T>::mutate(*netuid, |total| {
Expand All @@ -649,6 +652,21 @@ impl<T: Config> Pallet<T> {
return TransactionOutcome::Rollback(Ok(0));
}

// Settle every source debit and the aggregate root credit atomically, without balance
// events or an untracked issuance imbalance.
if swapped_tao > 0 {
let Some(root_account) = Self::get_subnet_account_id(NetUid::ROOT) else {
return TransactionOutcome::Rollback(Err(
Error::<T>::RootNetworkDoesNotExist.into()
));
};
if let Err(err) = Self::settle_tao_balances(&tao_debits, &root_account)
.inspect_err(|err| log::error!("Error settling basket TAO to root: {err:?}"))
{
return TransactionOutcome::Rollback(Err(err));
}
}

// Stake the redeemed TAO on root for the staker. Only the swapped portion is new TAO
// on root (the root-slot portion was already counted in the root reserves).
Self::increase_stake_for_hotkey_and_coldkey_on_subnet(
Expand Down Expand Up @@ -1032,13 +1050,27 @@ impl<T: Config> Pallet<T> {
holding_alpha,
);

let tao = match Self::sell_basket_alpha_for_root_tao(netuid, holding_alpha) {
Ok(tao) => tao,
Err(err) => {
log::error!("Error converting basket holding to root: {err:?}");
return TransactionOutcome::Rollback(Err(err));
}
let (source_account, tao) =
match Self::sell_basket_alpha_for_settlement(netuid, holding_alpha) {
Ok(settlement) => settlement,
Err(err) => {
log::error!("Error converting basket holding to root: {err:?}");
return TransactionOutcome::Rollback(Err(err));
}
};

let Some(root_account) = Self::get_subnet_account_id(NetUid::ROOT) else {
return TransactionOutcome::Rollback(Err(
Error::<T>::RootNetworkDoesNotExist.into()
));
};
if let Err(err) = Self::settle_tao_balances(&[(source_account, tao)], &root_account)
.inspect_err(|err| {
log::error!("Error settling converted basket TAO to root: {err:?}")
})
{
return TransactionOutcome::Rollback(Err(err));
}

// Hold the realized TAO as the fund's root-slot (cash) position.
Self::increase_stake_for_hotkey_and_coldkey_on_subnet(
Expand All @@ -1060,13 +1092,14 @@ impl<T: Config> Pallet<T> {
.is_ok()
}

/// Sells basket `alpha` on `netuid` for TAO and lands it in the root subnet account, booking
/// the protocol outflow. The alpha must already have been removed from the escrow position.
/// Shared by claim redemption and dissolution conversion; callers stay transactional.
fn sell_basket_alpha_for_root_tao(
/// Sells basket `alpha` on `netuid` for TAO and returns the source-account debit for the
/// caller's eventual aggregate settlement. The alpha must already have been removed from the
/// escrow position. Protocol outflow accounting stays in the caller's surrounding storage
/// transaction with the eventual TAO settlement.
fn sell_basket_alpha_for_settlement(
netuid: NetUid,
alpha: AlphaBalance,
) -> Result<TaoBalance, DispatchError> {
) -> Result<(T::AccountId, TaoBalance), DispatchError> {
let out = Self::swap_alpha_for_tao(
netuid,
alpha,
Expand All @@ -1075,15 +1108,12 @@ impl<T: Config> Pallet<T> {
)
.inspect_err(|err| log::error!("Error swapping basket alpha for TAO: {err:?}"))?;

let root_subnet_account_id =
Self::get_subnet_account_id(NetUid::ROOT).ok_or(Error::<T>::RootNetworkDoesNotExist)?;

Self::transfer_tao_from_subnet(netuid, &root_subnet_account_id, out.amount_paid_out.into())
.inspect_err(|err| log::error!("Error transferring basket TAO from subnet: {err:?}"))?;
let subnet_account =
Self::get_subnet_account_id(netuid).ok_or(Error::<T>::SubnetNotExists)?;

Self::record_protocol_outflow(netuid, out.amount_paid_out);

Ok(out.amount_paid_out)
Ok((subnet_account, out.amount_paid_out))
}

/// Drop a dissolving subnet's entries from the LEGACY per-subnet claimable rates. The
Expand Down
43 changes: 42 additions & 1 deletion pallets/subtensor/src/tests/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
BasketClaimed, BasketRate, BasketShares, BurnIncreaseMult, DefaultMinRootClaimAmount, Error,
Keys, MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, NumStakingColdkeys, RootClaimableThreshold,
StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMovingPrice, SubnetProtocolFlow,
SubnetTAO, SubnetworkN, Tempo, TotalStake, Uids, Weights,
SubnetTAO, SubnetworkN, Tempo, TotalIssuance, TotalStake, Uids, Weights,
};
use approx::assert_abs_diff_eq;
use frame_support::dispatch::{DispatchClass, GetDispatchInfo, RawOrigin};
Expand Down Expand Up @@ -535,11 +535,52 @@ fn test_root_basket_records_symmetric_protocol_flow() {
// Now redeem the basket. The fund-level claim sells the staker's pro-rata slice of BOTH
// holdings back to TAO, booking an outflow on each dest that nets the round-trip back
// toward zero.
let root_account = SubtensorModule::get_subnet_account_id(NetUid::ROOT).unwrap();
let account_b = SubtensorModule::get_subnet_account_id(netuid_b).unwrap();
let account_c = SubtensorModule::get_subnet_account_id(netuid_c).unwrap();
let root_balance_before = SubtensorModule::get_coldkey_balance(&root_account);
let balance_b_before = SubtensorModule::get_coldkey_balance(&account_b);
let balance_c_before = SubtensorModule::get_coldkey_balance(&account_c);
let balances_issuance_before = Balances::total_issuance();
let subtensor_issuance_before = TotalIssuance::<Test>::get();
System::reset_events();
assert_ok!(SubtensorModule::claim_root_with_hotkey(
RuntimeOrigin::signed(coldkey),
hotkey
));

let root_balance_after = SubtensorModule::get_coldkey_balance(&root_account);
let source_debits = balance_b_before
.saturating_sub(SubtensorModule::get_coldkey_balance(&account_b))
.saturating_add(
balance_c_before.saturating_sub(SubtensorModule::get_coldkey_balance(&account_c)),
);
assert_eq!(
root_balance_after.saturating_sub(root_balance_before),
source_debits,
"all source-account debits must land in the root account"
);
assert!(source_debits > TaoBalance::ZERO);
assert_eq!(Balances::total_issuance(), balances_issuance_before);
assert_eq!(TotalIssuance::<Test>::get(), subtensor_issuance_before);
assert!(
System::events()
.iter()
.all(|record| !matches!(record.event, RuntimeEvent::Balances(_))),
"an internal basket claim must not emit per-holding Balances events"
);
assert_eq!(
System::events()
.iter()
.filter(|record| matches!(
record.event,
RuntimeEvent::SubtensorModule(crate::Event::BasketClaimed { .. })
))
.count(),
1,
"the claim must retain one aggregate event"
);

let flow_b_after = SubnetProtocolFlow::<Test>::get(netuid_b);
let flow_c_after = SubnetProtocolFlow::<Test>::get(netuid_c);

Expand Down
52 changes: 52 additions & 0 deletions pallets/subtensor/src/tests/tao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,58 @@ fn total_balance(account: &U256) -> TaoBalance {
Balances::total_balance(account)
}

#[test]
fn test_settle_tao_balances_rolls_back_debits_when_credit_fails() {
new_test_ext(1).execute_with(|| {
let source = U256::from(1);
let destination = U256::from(10_001);
let amount = ExistentialDeposit::get() - TaoBalance::from(1);

let source_before = total_balance(&source);
let destination_before = total_balance(&destination);
let balances_ti_before = balances_total_issuance();
let subtensor_ti_before = subtensor_total_issuance();

// The destination does not exist, so an exact credit below ED must fail after the debit.
assert!(SubtensorModule::settle_tao_balances(&[(source, amount)], &destination).is_err());

assert_eq!(total_balance(&source), source_before);
assert_eq!(total_balance(&destination), destination_before);
assert_eq!(balances_total_issuance(), balances_ti_before);
assert_eq!(subtensor_total_issuance(), subtensor_ti_before);
});
}

#[test]
fn test_settle_tao_balances_rolls_back_prior_debits_when_later_debit_fails() {
new_test_ext(1).execute_with(|| {
let funded_source = U256::from(1);
let empty_source = U256::from(10_002);
let destination = U256::from(2);
let amount = ExistentialDeposit::get();

let funded_source_before = total_balance(&funded_source);
let empty_source_before = total_balance(&empty_source);
let destination_before = total_balance(&destination);
let balances_ti_before = balances_total_issuance();
let subtensor_ti_before = subtensor_total_issuance();

assert!(
SubtensorModule::settle_tao_balances(
&[(funded_source, amount), (empty_source, amount)],
&destination,
)
.is_err()
);

assert_eq!(total_balance(&funded_source), funded_source_before);
assert_eq!(total_balance(&empty_source), empty_source_before);
assert_eq!(total_balance(&destination), destination_before);
assert_eq!(balances_total_issuance(), balances_ti_before);
assert_eq!(subtensor_total_issuance(), subtensor_ti_before);
});
}

// ----------------------------------------------------
// transfer_tao
// ----------------------------------------------------
Expand Down
Loading