From c0e6911da68282935a26e084d1d7a14a72a9f7a2 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 12 Jun 2026 14:45:53 -0400 Subject: [PATCH 1/3] Handle swap errors in chain buys --- .../subtensor/src/coinbase/run_coinbase.rs | 41 +++++++++++---- pallets/subtensor/src/coinbase/tao.rs | 29 +++++++++++ pallets/subtensor/src/tests/coinbase.rs | 50 +++++++++++++++++++ pallets/subtensor/src/tests/tao.rs | 27 ++++++++++ 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 494fc163f7..3ace3bc30d 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -107,17 +107,36 @@ impl Pallet { T::SwapInterface::max_price(), true, ); - if let Ok(buy_swap_result_ok) = buy_swap_result { - let bought_alpha: AlphaBalance = - buy_swap_result_ok.amount_paid_out.into(); - SubnetProtocolAlpha::::mutate(*netuid_i, |total| { - *total = total.saturating_add(bought_alpha); - }); - - // Record actual excess TAO that entered pool. - let actual_excess: TaoBalance = buy_swap_result_ok.amount_paid_in; - SubnetExcessTao::::insert(*netuid_i, actual_excess); - Self::record_protocol_inflow(*netuid_i, actual_excess); + match buy_swap_result { + Ok(buy_swap_result_ok) => { + let bought_alpha: AlphaBalance = + buy_swap_result_ok.amount_paid_out.into(); + SubnetProtocolAlpha::::mutate(*netuid_i, |total| { + *total = total.saturating_add(bought_alpha); + }); + + // Record actual excess TAO that entered pool. + let actual_excess: TaoBalance = + buy_swap_result_ok.amount_paid_in; + SubnetExcessTao::::insert(*netuid_i, actual_excess); + Self::record_protocol_inflow(*netuid_i, actual_excess); + } + Err(error) => { + match Self::withdraw_tao_as_credit( + &subnet_account_id, + tao_to_swap_with, + ) { + Ok(refund_credit) => { + remaining_credit = + remaining_credit.merge(refund_credit); + } + Err(withdraw_error) => { + log::error!( + "Failed to revert excess TAO deposit after swap failure: netuid_i = {netuid_i:?}, tao_to_swap_with = {tao_to_swap_with:?}, swap_error = {error:?}, withdraw_error = {withdraw_error:?}" + ); + } + } + } } } Err(remainder) => { diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index 0dee496c3b..44ba0fa4b0 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -286,6 +286,35 @@ impl Pallet { } } + /// Withdraw TAO from an account into a fresh credit. + /// + /// This is useful when a previous `spend_tao` resolve must be undone without + /// changing total issuance. + pub fn withdraw_tao_as_credit( + coldkey: &T::AccountId, + amount: BalanceOf, + ) -> Result, DispatchError> { + let balances_ti_before = ::Currency::total_issuance(); + + let credit = ::Currency::withdraw( + coldkey, + amount, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + )?; + + let balances_ti_after = ::Currency::total_issuance(); + if balances_ti_after < balances_ti_before { + let burned = balances_ti_before.saturating_sub(balances_ti_after); + TotalIssuance::::mutate(|total| { + *total = total.saturating_sub(burned); + }); + } + + Ok(credit) + } + /// Finalizes the unused part of the minted TAO. pub fn recycle_credit(credit: CreditOf) { let amount = credit.peek(); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 20944a03d0..1a9bc5cd0b 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -3678,6 +3678,56 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { }); } +#[test] +fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failure() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tao_to_swap = TaoBalance::from(789_100_u64); + + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + + // Force the buy swap to fail after the excess TAO credit is deposited. + SubnetAlphaIn::::set( + netuid, + AlphaBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1), + ); + assert!( + SubtensorModule::swap_tao_for_alpha( + netuid, + tao_to_swap, + ::SwapInterface::max_price(), + true, + ) + .is_err() + ); + + let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + let chain_before = Balances::free_balance(&subnet_account); + let subnet_tao_before = SubnetTAO::::get(netuid); + let total_issuance_before = TotalIssuance::::get(); + let balances_issuance_before = Balances::total_issuance(); + + let tao_in = BTreeMap::from([(netuid, zero)]); + let alpha_in = BTreeMap::from([(netuid, zero)]); + let excess_tao = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); + let credit = SubtensorModule::mint_tao(tao_to_swap); + + SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!(Balances::free_balance(&subnet_account), chain_before); + assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); + assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); + assert_eq!(TotalIssuance::::get(), total_issuance_before); + assert_eq!(Balances::total_issuance(), balances_issuance_before); + }); +} + #[test] fn test_coinbase_drain_pending_increments_blockssincelaststep() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/tao.rs b/pallets/subtensor/src/tests/tao.rs index b79b80e3f3..a0246a399d 100644 --- a/pallets/subtensor/src/tests/tao.rs +++ b/pallets/subtensor/src/tests/tao.rs @@ -500,6 +500,33 @@ fn test_transfer_tao_reaps_origin() { }); } +#[test] +fn test_withdraw_tao_as_credit_reaps_origin_and_updates_subtensor_total_issuance() { + new_test_ext(1).execute_with(|| { + let origin = U256::from(1); + + let ed = ExistentialDeposit::get(); + let dust = ed - 1u64.into(); + let amount = TaoBalance::from(1_000); + let balance = amount + dust; + add_balance_to_coldkey_account(&origin, balance); + + let subtensor_ti_before = subtensor_total_issuance(); + let balances_ti_before = balances_total_issuance(); + + let credit = SubtensorModule::withdraw_tao_as_credit(&origin, amount).unwrap(); + + let subtensor_ti_after = subtensor_total_issuance(); + let balances_ti_after = balances_total_issuance(); + + assert_eq!(credit.peek(), amount); + assert_eq!(Balances::total_balance(&origin), 0.into()); + assert_eq!(balances_ti_before - balances_ti_after, dust); + assert_eq!(subtensor_ti_before - subtensor_ti_after, dust); + assert_eq!(balances_ti_after, subtensor_ti_after); + }); +} + #[test] fn test_recycle_tao_cannot_cross_preserve_threshold_in_high_ed_runtime() { new_test_ext(1).execute_with(|| { From 597d6ff49cdf9ba106032dc891ee4df0f15c45a7 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 12 Jun 2026 15:50:39 -0400 Subject: [PATCH 2/3] Fix test_swap_owner_new_hotkey_already_exists --- pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 426572bdcd..76dcd0a9ad 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -926,7 +926,7 @@ fn test_swap_owner_new_hotkey_already_exists() { Some(netuid), false ), - Error::::HotKeyAlreadyRegisteredInSubNet + Error::::NonAssociatedColdKey ); // Verify the swap From b32bed66bd7137dcd9e36755f09a6995ac12c91d Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 12 Jun 2026 15:56:34 -0400 Subject: [PATCH 3/3] clippy --- pallets/subtensor/src/tests/coinbase.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 1a9bc5cd0b..99b8b3a5b2 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -3708,7 +3708,7 @@ fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failur ); let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - let chain_before = Balances::free_balance(&subnet_account); + let chain_before = Balances::free_balance(subnet_account); let subnet_tao_before = SubnetTAO::::get(netuid); let total_issuance_before = TotalIssuance::::get(); let balances_issuance_before = Balances::total_issuance(); @@ -3720,7 +3720,7 @@ fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failur SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); - assert_eq!(Balances::free_balance(&subnet_account), chain_before); + assert_eq!(Balances::free_balance(subnet_account), chain_before); assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); assert_eq!(TotalIssuance::::get(), total_issuance_before);