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..99b8b3a5b2 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/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 1e200aaedd..beb8ec9a75 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -900,22 +900,23 @@ fn test_swap_owner_old_hotkey_not_exist() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner_new_hotkey_already_exists --exact --nocapture +// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_already_exists --exact --nocapture #[test] fn test_swap_owner_new_hotkey_already_exists() { new_test_ext(1).execute_with(|| { let old_hotkey = U256::from(1); let new_hotkey = U256::from(2); let coldkey = U256::from(3); + let another_coldkey = U256::from(4); - let netuid = add_dynamic_network(&new_hotkey, &coldkey); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); // old_hotkey is owned by coldkey; new_hotkey was already registered on `netuid` // by add_dynamic_network (the condition under test). Do NOT reassign new_hotkey to // a foreign coldkey — the new_hotkey-ownership check (NonAssociatedColdKey) would // then fire before the already-registered-in-subnet check this test targets. - Owner::::insert(old_hotkey, coldkey); + Owner::::insert(new_hotkey, another_coldkey); // Perform the swap System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); @@ -927,7 +928,7 @@ fn test_swap_owner_new_hotkey_already_exists() { Some(netuid), false ), - Error::::HotKeyAlreadyRegisteredInSubNet + Error::::NonAssociatedColdKey ); // Verify the swap 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(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5cf4d7aadb..94e25a46fb 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 419, + spec_version: 420, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,