Skip to content

stake_into_subnet strands the unswapped TAO portion when the AMM hits its price limit #2735

@gztensor

Description

@gztensor

Pallet::<T>::stake_into_subnet (in pallets/subtensor/src/staking/stake_utils.rs:845-952) is not symmetric with unstake_from_subnet (stake_utils.rs:742-840) when the underlying swap hits the AMM price limit:

  • unstake_from_subnet correctly refunds the unused alpha portion:
    let refund = alpha.saturating_sub(
        swap_result.amount_paid_in.saturating_add(swap_result.fee_paid).into()
    );
    if !refund.is_zero() {
        Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund);
    }
    
    (stake_utils.rs:757-766).
  • stake_into_subnet has no analogous refund step. The full tao_staked is transfer_tao_to_subnet-moved to the subnet PalletId account, but only swap_result.amount_paid_in (plus fee_to_block_author paid out separately) is accounted for in SubnetTAO / sent to the block author.

When BasicSwapStep::determine_action (pallets/swap/src/pallet/swap_step.rs:77-113) takes the Case 2 branch (target price would exceed the limit), it sets delta_in = balancer.calculate_quote_delta_in(current, limit) (or the alpha-side analog) and recomputes fee = delta_in * fee_rate / (u16::MAX - fee_rate). The returned SwapResult carries amount_paid_in = delta_in and fee_paid = fee_to_block_author = fee_recalc, so:

amount_paid_in + fee_to_block_author = delta_in * u16::MAX / (u16::MAX - fee_rate)
                                     < tao_staked   (whenever Case 2 fires)

The difference, tao_staked - delta_in * u16::MAX / (u16::MAX - fee_rate), stays on the subnet PalletId account. Concretely:

Bucket After do_add_stake Case 2
subnet PalletId free balance + tao_staked - fee_to_block_author
SubnetTAO + amount_paid_in (= delta_in)
TotalStake + tao_staked (line stake_utils.rs:676)

so subnet_account_balance and SubnetTAO diverge by tao_staked - delta_in - fee_to_block_author per call.

That excess TAO is:

  1. Not refunded to the user (no analog of the unstake_from_subnet refund).
  2. Not visible to the AMMcurrent_price reads SubnetTAO, not the subnet account balance. Future swaps on this pool price as if the excess does not exist.
  3. Eventually destroyed: on do_dissolve_networkremove_stake.rs:638-643, the residual subnet-account balance is recycle_tao'd (burned).

The asymmetry with unstake_from_subnet, combined with TotalStake being incremented by the full tao_staked (so the global "total stake" counter becomes inconsistent with the actual reserve), points to a missing refund step rather than an intentional design.

Fix sketch

Mirror the unstake_from_subnet refund: after swap_tao_for_alpha returns, compute refund = tao_staked.saturating_sub(amount_paid_in + fee_to_block_author) and, if non-zero, transfer_tao_from_subnet(netuid, coldkey, refund) (and back out the TotalStake increment from tao_staked to tao_staked - refund). Also re-check Event::StakeAdded to emit the actually-debited amount.

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