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:
- Not refunded to the user (no analog of the
unstake_from_subnet refund).
- Not visible to the AMM —
current_price reads SubnetTAO, not the subnet account balance. Future swaps on this pool price as if the excess does not exist.
- Eventually destroyed: on
do_dissolve_network → remove_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.
Pallet::<T>::stake_into_subnet(inpallets/subtensor/src/staking/stake_utils.rs:845-952) is not symmetric withunstake_from_subnet(stake_utils.rs:742-840) when the underlying swap hits the AMM price limit:unstake_from_subnetcorrectly refunds the unused alpha portion:stake_utils.rs:757-766).stake_into_subnethas no analogous refund step. The fulltao_stakedistransfer_tao_to_subnet-moved to the subnet PalletId account, but onlyswap_result.amount_paid_in(plusfee_to_block_authorpaid out separately) is accounted for inSubnetTAO/ 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 setsdelta_in = balancer.calculate_quote_delta_in(current, limit)(or the alpha-side analog) and recomputesfee = delta_in * fee_rate / (u16::MAX - fee_rate). The returnedSwapResultcarriesamount_paid_in = delta_inandfee_paid = fee_to_block_author = fee_recalc, so:The difference,
tao_staked - delta_in * u16::MAX / (u16::MAX - fee_rate), stays on the subnet PalletId account. Concretely:do_add_stakeCase 2+ tao_staked - fee_to_block_authorSubnetTAO+ amount_paid_in (= delta_in)TotalStake+ tao_staked(linestake_utils.rs:676)so
subnet_account_balanceandSubnetTAOdiverge bytao_staked - delta_in - fee_to_block_authorper call.That excess TAO is:
unstake_from_subnetrefund).current_pricereadsSubnetTAO, not the subnet account balance. Future swaps on this pool price as if the excess does not exist.do_dissolve_network→remove_stake.rs:638-643, the residual subnet-account balance isrecycle_tao'd (burned).The asymmetry with
unstake_from_subnet, combined withTotalStakebeing incremented by the fulltao_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_subnetrefund: afterswap_tao_for_alphareturns, computerefund = 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 theTotalStakeincrement fromtao_stakedtotao_staked - refund). Also re-checkEvent::StakeAddedto emit the actually-debited amount.