Describe the bug
The composite chain-extension entry points AddStakeRecycleV1 / AddStakeBurnV1 (chain-extensions/src/lib.rs:792-853) invoke pallet-level pub fn helpers that are not #[pallet::call] dispatchables - so FRAME's automatic with_storage_layer wrap does not apply. If the second leg (do_recycle_alpha or do_burn_alpha) returns Err after the first leg (do_add_stake) succeeded, the first leg's storage writes persist. The doc-comment on the helper still claims atomicity ("without leaving residual stake if the second leg fails").
This is a silent regression: commit 46989aaba (2026-04-17) originally wrapped both helpers in transactional::with_transaction to deliver the documented atomicity; commit d0702db5a (2026-05-04, "add migration part") removed the wrapper without mention in the commit message.
To Reproduce
Simplest trigger: netuid = NetUid::ROOT. do_add_stake succeeds on the root subnet, then do_recycle_alpha / do_burn_alpha returns Error::CannotBurnOrRecycleOnRootSubnet (pallets/subtensor/src/staking/recycle_alpha.rs:28-31 and :89-92).
- Deploy any ink! contract whose
#[ink(message)] calling the chain extension returns a non-Result type (so ink!'s automatic revert-on-Err dispatch wrapper does not trip). Raw WASM / PolkaVM contracts naturally expose the bug.
- From the contract, invoke
AddStakeRecycleV1(hotkey, netuid = NetUid::ROOT, tao_amount = 100 TAO) (or the burn variant).
- The chain extension returns
Output::CannotBurnOrRecycleOnRootSubnet = 21.
- Inspect the contract's TAO balance,
AlphaV2((hotkey, contract, ROOT)), SubnetTAO[ROOT], SubnetAlphaOut[ROOT], TotalStake.
Observed: contract TAO debited by 100 TAO; root alpha credited 100e9 to (hotkey, contract, ROOT); SubnetTAO[ROOT], SubnetAlphaOut[ROOT], TotalStake all bumped - despite the chain extension returning a failure code that the composite was supposed to roll back.
Other plausible failure modes for the second leg:
- A pre-existing
Lock row on (contract-account, netuid, hotkey) reduces available_stake = total - locked - unlocked below the freshly-minted alpha, so ensure_available_stake (recycle_alpha.rs:54) returns StakeUnavailable.
- An intermediate state-change inside
do_add_stake -> stake_into_subnet can also commit partially: transfer_tao_to_subnet moves real pallet-balances TAO before the internal swap_tao_for_alpha, leaving the TAO stranded if the swap fails.
Expected behavior
AddStakeRecycleV1 / AddStakeBurnV1 must be atomic - either both legs succeed or no storage writes persist. The doc-comment on do_add_stake_recycle already states this contract: "so that contracts can compose the two operations without leaving residual stake if the second leg fails."
Screenshots
No response
Environment
opentensor/subtensor testnet @ e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9
Additional context
Affected code
chain-extensions/src/lib.rs:792-853:
FunctionId::AddStakeRecycleV1 => {
// ...
Pallet::<T>::do_add_stake_recycle(origin, hotkey, netuid, amount) // <-- no with_storage_layer
}
FunctionId::AddStakeBurnV1 => {
// ...
Pallet::<T>::do_add_stake_burn_permissionless(origin, hotkey, netuid, amount) // <-- no with_storage_layer
}
pallets/subtensor/src/staking/recycle_alpha.rs:157-178:
pub fn do_add_stake_recycle(origin, hotkey, netuid, amount) -> Result<AlphaBalance, _> {
let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?;
Self::do_recycle_alpha(origin, hotkey, alpha, netuid)
}
pub fn do_add_stake_burn_permissionless(origin, hotkey, netuid, amount) -> Result<AlphaBalance, _> {
let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?;
Self::do_burn_alpha(origin, hotkey, alpha, netuid)
}
Describe the bug
The composite chain-extension entry points
AddStakeRecycleV1/AddStakeBurnV1(chain-extensions/src/lib.rs:792-853) invoke pallet-levelpub fnhelpers that are not#[pallet::call]dispatchables - so FRAME's automaticwith_storage_layerwrap does not apply. If the second leg (do_recycle_alphaordo_burn_alpha) returnsErrafter the first leg (do_add_stake) succeeded, the first leg's storage writes persist. The doc-comment on the helper still claims atomicity ("without leaving residual stake if the second leg fails").This is a silent regression: commit
46989aaba(2026-04-17) originally wrapped both helpers intransactional::with_transactionto deliver the documented atomicity; commitd0702db5a(2026-05-04, "add migration part") removed the wrapper without mention in the commit message.To Reproduce
Simplest trigger:
netuid = NetUid::ROOT.do_add_stakesucceeds on the root subnet, thendo_recycle_alpha/do_burn_alphareturnsError::CannotBurnOrRecycleOnRootSubnet(pallets/subtensor/src/staking/recycle_alpha.rs:28-31and:89-92).#[ink(message)]calling the chain extension returns a non-Resulttype (so ink!'s automatic revert-on-Errdispatch wrapper does not trip). Raw WASM / PolkaVM contracts naturally expose the bug.AddStakeRecycleV1(hotkey, netuid = NetUid::ROOT, tao_amount = 100 TAO)(or the burn variant).Output::CannotBurnOrRecycleOnRootSubnet = 21.AlphaV2((hotkey, contract, ROOT)),SubnetTAO[ROOT],SubnetAlphaOut[ROOT],TotalStake.Observed: contract TAO debited by 100 TAO; root alpha credited 100e9 to
(hotkey, contract, ROOT);SubnetTAO[ROOT],SubnetAlphaOut[ROOT],TotalStakeall bumped - despite the chain extension returning a failure code that the composite was supposed to roll back.Other plausible failure modes for the second leg:
Lockrow on(contract-account, netuid, hotkey)reducesavailable_stake = total - locked - unlockedbelow the freshly-mintedalpha, soensure_available_stake(recycle_alpha.rs:54) returnsStakeUnavailable.do_add_stake -> stake_into_subnetcan also commit partially:transfer_tao_to_subnetmoves realpallet-balancesTAO before the internalswap_tao_for_alpha, leaving the TAO stranded if the swap fails.Expected behavior
AddStakeRecycleV1/AddStakeBurnV1must be atomic - either both legs succeed or no storage writes persist. The doc-comment ondo_add_stake_recyclealready states this contract: "so that contracts can compose the two operations without leaving residual stake if the second leg fails."Screenshots
No response
Environment
opentensor/subtensor
testnet@e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9Additional context
Affected code
chain-extensions/src/lib.rs:792-853:pallets/subtensor/src/staking/recycle_alpha.rs:157-178: