Skip to content

inject_and_maybe_swap leaves orphan TAO on subnet account when swap_tao_for_alpha fails after spend_tao succeeded #2740

@gztensor

Description

@gztensor

In inject_and_maybe_swap (pallets/subtensor/src/coinbase/run_coinbase.rs:71-176), when the per-subnet "excess TAO swap" branch runs (lines 99-132):

match Self::spend_tao(&subnet_account_id, remaining_credit, tao_to_swap_with) {
    Ok(remainder) => {
        remaining_credit = remainder;

        let buy_swap_result = Self::swap_tao_for_alpha(
            *netuid_i,
            tao_to_swap_with,
            T::SwapInterface::max_price(),
            true,
        );
        if let Ok(buy_swap_result_ok) = buy_swap_result {
            // ... SubnetProtocolAlpha += bought_alpha, SubnetExcessTao = actual_excess,
            //     record_protocol_inflow(actual_excess)
        }
        // No `else` branch: failure is silently swallowed.
    }
    Err(remainder) => { /* log + continue */ }
}

The spend_tao call atomically resolves tao_to_swap_with units of the block-emission Credit into subnet_account (coinbase/tao.rs:271-287Currency::resolve), permanently crediting subnet_account.free and counting the issuance in both subtensor::TotalIssuance (incremented earlier by mint_tao at tao.rs:252-266) and balances::TotalIssuance (incremented by Currency::issue inside mint_tao).

If swap_tao_for_alpha then returns Err, the AMM-side bookkeeping is reverted (the inner swap runs in transactional::with_transactionpallets/swap/src/pallet/impls.rs:198-222), but the prior spend_tao is outside that transaction. The net post-condition for the failing subnet is:

  • subnet_account.free += tao_to_swap_with (the credit-resolved TAO sits on the pallet-derived account).
  • subtensor::TotalIssuance and balances::TotalIssuance each +tao_to_swap_with (matching balances side).
  • SubnetTAO[netuid] unchanged (would have been incremented inside swap_tao_for_alpha at stake_utils.rs:676-679 only on success).
  • TotalStake unchanged (would have been incremented on success at stake_utils.rs:682).
  • SubnetExcessTao[netuid] = 0 (zeroed unconditionally at line 96 and never re-written in the failure path).
  • SubnetProtocolAlpha[netuid] unchanged.
  • record_protocol_inflow(netuid, ...) never called, so the failed inflow is missing from SubnetProtocolFlow EMA accounting (coinbase/subnet_emissions.rs:94-98).

The tao_to_swap_with TAO is therefore orphaned: physically present on subnet_account (so Currency::total_issuance() keeps counting it), but the per-subnet accounting maps treat the swap as never having happened. The subnet's SubnetTAO reserve is now below its real on-chain balance.

Recommended remediation

Either:

  1. Wrap the spend_tao + swap_tao_for_alpha pair in transactional::with_transaction so the credit resolution rolls back on swap failure, returning the credit (and thus the TAO) to recycle_credit.
  2. On swap failure, explicitly transfer_tao the tao_to_swap_with back from subnet_account into a recycled imbalance (recycle_tao against subnet_account) so both subtensor::TotalIssuance and balances::TotalIssuance are decremented and the orphan never accumulates.
  3. At minimum, in the Err / else-of-Ok path, mutate SubnetTAO[netuid] += tao_to_swap_with and TotalStake += tao_to_swap_with so the on-account TAO is at least tracked as a "free" reserve owned by the subnet rather than orphaned.

Option (1) is the cleanest and matches the pattern already used inside the swap pallet.

Metadata

Metadata

Assignees

Labels

No labels
No labels

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