Skip to content

inject_and_maybe_swap silently strands TAO when swap_tao_for_alpha fails #2661

@Maksandre

Description

@Maksandre

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):

  1. Register a dynamic subnet and seed AMM reserves.
  2. Force SubnetAlphaIn below MinimumReserve (organically reachable via concentrated alpha selling).
  3. Trigger the coinbase entry point — on_initialize does this on every block.
  4. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions