Describe the bug
In inject_and_maybe_swap (pallets/subtensor/src/coinbase/run_coinbase.rs:70-121), the buy-swap branch first deposits tao_to_swap_with into the subnet account via spend_tao, then attempts swap_tao_for_alpha. The swap's Err result is silently dropped by if let Ok(buy_swap_result_ok) = buy_swap_result { ... } at line 102. Because on_initialize runs without an outer with_storage_layer (pallets/subtensor/src/macros/hooks.rs:17-39), the prior spend_tao deposit persists while no compensating update to SubnetTAO / TotalStake / SubnetExcessTao / record_protocol_inflow fires. TAO accumulates on the subnet account's chain balance every failing block, invisible to check_total_issuance (both issuance counters stay in sync — only internal bookkeeping diverges).
To Reproduce
The bug fires automatically inside on_initialize whenever the buy-swap fails. The simplest precondition for a pallet test is SubnetAlphaIn[netuid] < T::MinimumReserve (triggers swap_inner's Error::ReservesTooLow at pallets/swap/src/pallet/impls.rs:288-291):
- Register a dynamic subnet and seed AMM reserves.
- Force
SubnetAlphaIn below MinimumReserve (organically reachable via concentrated alpha selling).
- Trigger the coinbase entry point —
on_initialize does this on every block.
- Read
Balances::free_balance(get_subnet_account_id(netuid)) vs SubnetTAO[netuid] before/after.
#[test]
fn inject_and_maybe_swap_swap_failure_strands_tao() {
new_test_ext(1).execute_with(|| {
let netuid = add_dynamic_network(&U256::from(1), &U256::from(2));
mock::setup_reserves(netuid, 1_000_000_000_000_u64.into(), 1_000_000_000_000_u64.into());
// Force buy-swap to fail with ReservesTooLow.
SubnetAlphaIn::<Test>::set(netuid, AlphaBalance::from(10_u64));
// Sanity-check the precondition.
assert!(SubtensorModule::swap_tao_for_alpha(
netuid, TaoBalance::from(789_100_u64),
<Test as pallet::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 sub_tao_before = SubnetTAO::<Test>::get(netuid);
SubtensorModule::run_coinbase(SubtensorModule::mint_tao(10_000_000u64.into()));
// Chain balance grew, SubnetTAO did NOT.
assert!(Balances::free_balance(&subnet_account) > chain_before);
assert_eq!(SubnetTAO::<Test>::get(netuid), sub_tao_before);
assert_eq!(SubnetExcessTao::<Test>::get(netuid), TaoBalance::ZERO);
});
}
Other organic triggers:
MAX_SWAP_ITERATIONS = 30 tick crossings exceeded -> Error::TooManySwapSteps
- Post-swap reserve check fails ->
Error::InsufficientLiquidity
Expected behavior
On swap_tao_for_alpha returning Err, the prior spend_tao deposit must be reverted (re-withdraw into a fresh Credit and merge back into remaining_credit), OR SubnetTAO / TotalStake / SubnetExcessTao must be bumped and record_protocol_inflow called to match the on-chain balance. The invariant SubnetTAO[netuid] ≈ subnet account chain balance must hold after the function returns.
Screenshots
No response
Environment
opentensor/subtensor testnet @ e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9
Additional context
- TAO permanently stranded on the
PalletId-derived subnet account — no public extrinsic recovers it (transfer_tao_from_subnet / dissolution refund both gate on SubnetTAO / SubnetLocked, which lag the real balance).
- Invisible to
check_total_issuance: both balances::TotalIssuance and subtensor::TotalIssuance stay in sync because mint_tao + Currency::resolve move them together; only SubnetTAO-vs-chain-balance diverges.
record_protocol_inflow skipped -> SubnetProtocolFlow EMA underestimates this subnet's inflow, biasing future emission shares.
- Zero-privilege trigger; strand grows linearly each block the AMM stays in the failing state.
Recommended fix: revert the spend_tao deposit on swap failure (re-withdraw into a fresh Credit and merge into remaining_credit), OR bump SubnetTAO / TotalStake / SubnetExcessTao and call record_protocol_inflow on the failure path so accounting matches the actual on-chain balance.
Describe the bug
In
inject_and_maybe_swap(pallets/subtensor/src/coinbase/run_coinbase.rs:70-121), the buy-swap branch first depositstao_to_swap_withinto the subnet account viaspend_tao, then attemptsswap_tao_for_alpha. The swap'sErrresult is silently dropped byif let Ok(buy_swap_result_ok) = buy_swap_result { ... }at line 102. Becauseon_initializeruns without an outerwith_storage_layer(pallets/subtensor/src/macros/hooks.rs:17-39), the priorspend_taodeposit persists while no compensating update toSubnetTAO/TotalStake/SubnetExcessTao/record_protocol_inflowfires. TAO accumulates on the subnet account's chain balance every failing block, invisible tocheck_total_issuance(both issuance counters stay in sync — only internal bookkeeping diverges).To Reproduce
The bug fires automatically inside
on_initializewhenever the buy-swap fails. The simplest precondition for a pallet test isSubnetAlphaIn[netuid] < T::MinimumReserve(triggersswap_inner'sError::ReservesTooLowatpallets/swap/src/pallet/impls.rs:288-291):SubnetAlphaInbelowMinimumReserve(organically reachable via concentrated alpha selling).on_initializedoes this on every block.Balances::free_balance(get_subnet_account_id(netuid))vsSubnetTAO[netuid]before/after.Other organic triggers:
MAX_SWAP_ITERATIONS = 30tick crossings exceeded ->Error::TooManySwapStepsError::InsufficientLiquidityExpected behavior
On
swap_tao_for_alphareturningErr, the priorspend_taodeposit must be reverted (re-withdraw into a freshCreditand merge back intoremaining_credit), ORSubnetTAO/TotalStake/SubnetExcessTaomust be bumped andrecord_protocol_inflowcalled to match the on-chain balance. The invariantSubnetTAO[netuid] ≈ subnet account chain balancemust hold after the function returns.Screenshots
No response
Environment
opentensor/subtensor
testnet@e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9Additional context
PalletId-derived subnet account — no public extrinsic recovers it (transfer_tao_from_subnet/ dissolution refund both gate onSubnetTAO/SubnetLocked, which lag the real balance).check_total_issuance: bothbalances::TotalIssuanceandsubtensor::TotalIssuancestay in sync becausemint_tao+Currency::resolvemove them together; onlySubnetTAO-vs-chain-balance diverges.record_protocol_inflowskipped ->SubnetProtocolFlowEMA underestimates this subnet's inflow, biasing future emission shares.Recommended fix: revert the
spend_taodeposit on swap failure (re-withdraw into a freshCreditand merge intoremaining_credit), OR bumpSubnetTAO/TotalStake/SubnetExcessTaoand callrecord_protocol_inflowon the failure path so accounting matches the actual on-chain balance.