diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md
index 823cf6e4b..b0a291212 100644
--- a/docs/learn/price-protection.md
+++ b/docs/learn/price-protection.md
@@ -47,10 +47,64 @@ Consider attempting to unstake 1000 alpha when executing the full transaction wo
| Partial Safe | Unstakes ~400 alpha (maximum amount that keeps final price within 2% tolerance) |
| Unsafe | Unstakes full 1000 alpha regardless of 5% price impact |
+
+### Moving stake between subnets
+
+:::note
+In Bittensor terminology:
+
+- **`stake transfer`**: transfer stake in a specific hotkey on a subnet to another coldkey
+- **`stake move`**: move stake delegated to one valdiator to another validator hotkey, while maintaining ownership of the stake
+- **`stake swap`**: swap stake delegated to a particular validator hotkey to that same hotkey on another subnet, without affecting ownership of the stake
+:::
+
+Stake transactions between subnets can be executed via slippage-protected `*_limit` variants (for example `swap_stake_limit`; some client interfaces refer to this flow as `move_stake_limit`) which take a `limit_price` parameter.
+
+The `limit_price` parameter bounds how far the **relative price** between the origin subnet and destination subnet is allowed to move during execution.
+
+The relative price is defined as:
+
+$$
+\text{relative price} = \frac{\text{origin price}}{\text{destination price}}
+$$
+
+Where each subnet price is its current spot price in $ \tau/\alpha $ (TAO per alpha), and the Root subnet’s price is $1.0$.
+
+The key non-obvious detail is that, for stake movement, the slippage check is based on a **single consistent definition** of relative price:
+
+ $$
+ \frac{\text{origin price}}{\text{destination price}}
+ $$
+
+ This is counter-intuitive when moving stake from Root $\rightarrow$ a dynamic subnet, because the destination subnet price typically goes **up** during the move (the operation effectively buys the destination alpha), which makes the ratio $\text{origin}/\text{destination}$ go **down**.
+
+ **Example (Root $\rightarrow$ SN100):**
+
+ - Suppose subnet 100 has price $0.0167\ \tau/\alpha$.
+ - Root price is $1.0$.
+ - Relative price is $1.0 / 0.0167 \approx 59.88$.
+
+ Since execution tends to push the destination price up, the relative price tends to move down. To enforce (say) a 5% slippage bound, set:
+
+ $$
+ \text{limit\_price} \approx 59.88 \cdot (1 - 0.05) \approx 56.89
+ $$
+
+ In the on-chain call, `limit_price` is encoded as a fixed-point `u64` with $10^9$ precision (so $1.0 \mapsto 1{,}000{,}000{,}000$). In that representation, the current relative price $59.88$ is about $59{,}880{,}000{,}000$, and a “just below” limit might be around $59{,}000{,}000{,}000$.
+
+ If the destination is Root (subnet 0), destination price is $1.0$, so the relative price reduces to the origin subnet price. In that case the formula is intuitive: set the limit to the desired origin price (e.g. a bit higher than current), for example $0.017$.
+
+ Why define it this way (consistent vs. “intuitive”)?
+
+ - If the formula were flipped depending on direction (staking vs unstaking), the dynamic $\rightarrow$ dynamic case would become very hard to reason about and easy to misuse.
+ - A uniform definition makes client integrations safer and simpler: “apply 5% slippage” can be implemented as “compute the relative price once, then multiply by $1 - 0.05$” for all cases.
+
+
## Managing Price Protection with BTCLI
The `btcli stake` interface provides parameters to control price protection modes.
+### Parameters
**Enable/disable price protection (strict or partial):**
True by default. Enables price protection, which is strict by default.
@@ -79,7 +133,8 @@ If in **partial safe** staking mode, determines the threshold of price variation
- **Range**: 0.0 to 1.0 (0% to 100%)
- **Purpose**: Maximum allowed final price deviation from submission price
-### BTCLI Examples
+
+### Adding stake
**Strict Safe Mode (reject if price moves beyond tolerance):**
@@ -100,6 +155,27 @@ btcli stake add --amount 1000 --netuid 1 --safe --tolerance 0.02 --partial
```bash
btcli stake add --amount 300 --netuid 1 --unsafe
```
+### Swapping stake between subnets (on the same validator coldkey-hotkey pair)
+
+
+The following is a minimal testnet walkthrough to swap stake from one subnet to another.
+
+```bash
+# 0) Inspect your current balances/stake and pick origin/destination netuids
+btcli wallet balance
+btcli stake list
+btcli subnets list
+
+# 1) (Optional) Add stake on the origin subnet so you have something to swap
+btcli stake add --netuid 1 --amount 100 --safe --tolerance 0.02 --no-partial --no-prompt
+
+# 2) Swap stake from subnet 1 -> subnet 2 with price protection enabled
+# note that --safe is unnecessary as it is enabled by default
+btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 50 --safe --tolerance 0.01 --allow-partial-stake --no-prompt
+
+# 3) Verify the stake moved
+btcli stake list
+```
## Managing Price Protection with SDK
@@ -128,12 +204,10 @@ You must explicitly configure price protection when using the SDK's staking/unst
- **Range**: 0.0 to 1.0
- **Purpose**: Maximum allowed final price deviation from submission price
-### SDK Examples
-See [Price Protection Simulation](#price-protection-simulation) for an extended example.
-
+### Adding Stake
#### Safe Mode (reject if price moves beyond tolerance)
```python
@@ -179,6 +253,104 @@ response = subtensor.add_stake(
)
```
+
+### Swapping stake between subnets (on the same validator coldkey-hotkey pair)
+
+
+
+```python
+import bittensor as bt
+
+subtensor = bt.Subtensor(network="test")
+wallet = bt.Wallet("my_wallet")
+
+# Swap stake from subnet 1 -> subnet 2, keeping the same coldkey-hotkey pair.
+response = subtensor.swap_stake(
+ wallet=wallet,
+ hotkey_ss58=wallet.hotkey.ss58_address,
+ origin_netuid=1,
+ destination_netuid=2,
+ amount=bt.Balance.from_tao(50),
+ safe_swapping=True,
+ rate_tolerance=0.01, # 1% tolerance on the relative price
+ allow_partial_stake=True, # execute what fits within tolerance
+)
+print(response)
+```
+
+#### SDK walkthrough: inspect → simulate → swap → verify
+
+
+
+```python
+import bittensor as bt
+
+subtensor = bt.Subtensor(network="test")
+wallet = bt.Wallet("my_wallet")
+wallet.unlock_coldkey()
+
+coldkey = wallet.coldkeypub.ss58_address
+hotkey = wallet.hotkey.ss58_address
+
+# 1) Choose origin/destination netuids.
+# A practical approach is: pick an origin netuid where you already have stake.
+stakes = subtensor.get_stake_info_for_coldkey(coldkey)
+positions = [
+ s for s in stakes
+ if s.hotkey_ss58 == hotkey and float(s.stake) > 0
+]
+print("Positions with stake (this coldkey+hotkey):")
+for s in positions:
+ print(" netuid=", s.netuid, " stake=", s.stake)
+
+origin_netuid = positions[0].netuid # choose intentionally
+destination_netuid = 2 # choose intentionally (must exist)
+
+# 2) Inspect the current relative price = origin_price / destination_price.
+origin_pool = subtensor.subnet(netuid=origin_netuid)
+dest_pool = subtensor.subnet(netuid=destination_netuid)
+relative_price = origin_pool.price.tao / (dest_pool.price.tao or 1.0)
+print("relative price (origin/dest):", relative_price)
+
+# 3) Choose an amount to swap.
+# IMPORTANT: swap_stake amount is denominated in the origin subnet's alpha units.
+amount = bt.Balance.from_tao(50).set_unit(origin_netuid)
+
+# 4) (Optional) simulate fees and output amounts (does not include the extrinsic fee).
+sim = subtensor.sim_swap(
+ origin_netuid=origin_netuid,
+ destination_netuid=destination_netuid,
+ amount=amount,
+)
+print("sim.alpha_fee:", sim.alpha_fee)
+print("sim.tao_fee:", sim.tao_fee)
+print("sim.alpha_amount:", sim.alpha_amount)
+print("sim.tao_amount:", sim.tao_amount)
+
+# 5) Execute the swap with price protection.
+resp = subtensor.swap_stake(
+ wallet=wallet,
+ hotkey_ss58=hotkey,
+ origin_netuid=origin_netuid,
+ destination_netuid=destination_netuid,
+ amount=amount,
+ safe_swapping=True,
+ rate_tolerance=0.01, # 1% tolerance on the relative price
+ allow_partial_stake=True,
+ wait_for_inclusion=True,
+ wait_for_finalization=True,
+)
+print(resp)
+
+# 6) Verify updated stakes.
+origin_after = subtensor.get_stake(coldkey_ss58=coldkey, hotkey_ss58=hotkey, netuid=origin_netuid)
+dest_after = subtensor.get_stake(coldkey_ss58=coldkey, hotkey_ss58=hotkey, netuid=destination_netuid)
+print("origin_after:", origin_after)
+print("dest_after:", dest_after)
+```
+
+
+
## Price Protection Simulation
The following script runs through several different stake and unstake operations with different price protection modes, to demonstrate the different behaviors contingent on price.
@@ -208,7 +380,7 @@ def display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, label)
print(f"Coldkey balance: {balance}")
- # Find stake for our target hotkey and netuid
+ # Find stake for the target hotkey and netuid
target_stake = None
for stake_info in stakes:
if stake_info.hotkey_ss58 == target_hotkey and stake_info.netuid == netuid: