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
41 changes: 30 additions & 11 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,36 @@ impl<T: Config> Pallet<T> {
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::<T>::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::<T>::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::<T>::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::<T>::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) => {
Expand Down
29 changes: 29 additions & 0 deletions pallets/subtensor/src/coinbase/tao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,35 @@ impl<T: Config> Pallet<T> {
}
}

/// 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<T>,
) -> Result<CreditOf<T>, DispatchError> {
let balances_ti_before = <T as Config>::Currency::total_issuance();

let credit = <T as Config>::Currency::withdraw(
coldkey,
amount,
Precision::Exact,
Preservation::Expendable,
Fortitude::Polite,
)?;

let balances_ti_after = <T as Config>::Currency::total_issuance();
if balances_ti_after < balances_ti_before {
let burned = balances_ti_before.saturating_sub(balances_ti_after);
TotalIssuance::<T>::mutate(|total| {
*total = total.saturating_sub(burned);
});
}

Ok(credit)
}

/// Finalizes the unused part of the minted TAO.
pub fn recycle_credit(credit: CreditOf<T>) {
let amount = credit.peek();
Expand Down
50 changes: 50 additions & 0 deletions pallets/subtensor/src/tests/coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::set(
netuid,
AlphaBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1),
);
assert!(
SubtensorModule::swap_tao_for_alpha(
netuid,
tao_to_swap,
<Test as Config>::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::<Test>::get(netuid);
let total_issuance_before = TotalIssuance::<Test>::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::<Test>::get(netuid), subnet_tao_before);
assert_eq!(SubnetExcessTao::<Test>::get(netuid), TaoBalance::ZERO);
assert_eq!(TotalIssuance::<Test>::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(|| {
Expand Down
9 changes: 5 additions & 4 deletions pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::insert(old_hotkey, coldkey);
Owner::<Test>::insert(new_hotkey, another_coldkey);

// Perform the swap
System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get());
Expand All @@ -927,7 +928,7 @@ fn test_swap_owner_new_hotkey_already_exists() {
Some(netuid),
false
),
Error::<Test>::HotKeyAlreadyRegisteredInSubNet
Error::<Test>::NonAssociatedColdKey
);

// Verify the swap
Expand Down
27 changes: 27 additions & 0 deletions pallets/subtensor/src/tests/tao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading