From 84f64dfabaab58f29ad69f1859bf81068bedfadd Mon Sep 17 00:00:00 2001 From: michael trestman Date: Wed, 25 Feb 2026 14:07:05 -0800 Subject: [PATCH 1/4] wip --- docs/learn/price-protection.md | 170 +++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md index 823cf6e4b..454fd0304 100644 --- a/docs/learn/price-protection.md +++ b/docs/learn/price-protection.md @@ -101,6 +101,33 @@ btcli stake add --amount 1000 --netuid 1 --safe --tolerance 0.02 --partial btcli stake add --amount 300 --netuid 1 --unsafe ``` +**Swap stake between subnets (same coldkey-hotkey pair):** + +In BTCLI terminology: + +- **`stake move`**: move staked TAO between hotkeys while keeping the same coldkey ownership +- **`stake transfer`**: transfer stake between coldkeys while keeping the same hotkey ownership +- **`stake swap`**: swap stake between different subnets while keeping the same coldkey-hotkey pair ownership + +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 --network test --wallet-name default --hotkey default +btcli stake list --network test --wallet-name default --hotkey default +btcli subnets list --network test + +# 1) (Optional) Add stake on the origin subnet so you have something to swap +btcli stake add --network test --wallet-name default --hotkey default --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 --network test --wallet-name default --hotkey default --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 --network test --wallet-name default --hotkey default +``` + ## Managing Price Protection with SDK The Bittensor SDK provides price protection through method parameters: @@ -179,6 +206,149 @@ response = subtensor.add_stake( ) ``` +### Moving stake between subnets (swap/move stake) + +Stake movement 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$. + +#### SDK example: swap stake between two subnets + + + +```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) +``` + +
+ See how it's calculated! + + 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 (you are effectively buying 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, you’d 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. + + Why define it this way (consistent vs. “intuitive”)? + + - If we flipped the formula depending on direction (staking vs unstaking), the dynamic $\rightarrow$ dynamic case becomes 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. + +
+ ## 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. From 2aa0001c63d110603a426c429ef41d0d5c8313da Mon Sep 17 00:00:00 2001 From: michael trestman Date: Wed, 25 Feb 2026 15:29:11 -0800 Subject: [PATCH 2/4] wip --- docs/learn/price-protection.md | 128 +++++++++++++++++---------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md index 454fd0304..77e0c5a25 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 (you are effectively buying 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, you’d 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 origin price you are willing to accept (e.g. a bit higher than current), for example $0.017$. + + Why define it this way (consistent vs. “intuitive”)? + + - If we flipped the formula depending on direction (staking vs unstaking), the dynamic $\rightarrow$ dynamic case becomes 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,32 +155,26 @@ 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) -**Swap stake between subnets (same coldkey-hotkey pair):** - -In BTCLI terminology: - -- **`stake move`**: move staked TAO between hotkeys while keeping the same coldkey ownership -- **`stake transfer`**: transfer stake between coldkeys while keeping the same hotkey ownership -- **`stake swap`**: swap stake between different subnets while keeping the same coldkey-hotkey pair ownership 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 --network test --wallet-name default --hotkey default -btcli stake list --network test --wallet-name default --hotkey default -btcli subnets list --network test +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 --network test --wallet-name default --hotkey default --netuid 1 --amount 100 --safe --tolerance 0.02 --no-partial --no-prompt +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 --network test --wallet-name default --hotkey default --origin-netuid 1 --dest-netuid 2 --amount 50 --safe --tolerance 0.01 --allow-partial-stake --no-prompt +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 --network test --wallet-name default --hotkey default +btcli stake list ``` ## Managing Price Protection with SDK @@ -155,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 @@ -206,21 +253,8 @@ response = subtensor.add_stake( ) ``` -### Moving stake between subnets (swap/move stake) - -Stake movement 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$. - -#### SDK example: swap stake between two subnets +### Swapping stake between subnets (on the same validator coldkey-hotkey pair) @@ -315,39 +349,7 @@ print("origin_after:", origin_after) print("dest_after:", dest_after) ``` -
- See how it's calculated! - - 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 (you are effectively buying 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, you’d 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. - - Why define it this way (consistent vs. “intuitive”)? - - - If we flipped the formula depending on direction (staking vs unstaking), the dynamic $\rightarrow$ dynamic case becomes 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. -
## Price Protection Simulation From bbd29396073351c01b6f80bf882e4c0307df73f4 Mon Sep 17 00:00:00 2001 From: michael trestman Date: Wed, 25 Feb 2026 15:38:56 -0800 Subject: [PATCH 3/4] wip --- docs/learn/price-protection.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md index 77e0c5a25..b0a291212 100644 --- a/docs/learn/price-protection.md +++ b/docs/learn/price-protection.md @@ -76,7 +76,7 @@ The key non-obvious detail is that, for stake movement, the slippage check is ba \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 (you are effectively buying the destination alpha), which makes the ratio $\text{origin}/\text{destination}$ go **down**. + 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):** @@ -84,7 +84,7 @@ The key non-obvious detail is that, for stake movement, the slippage check is ba - 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, you’d set: + 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 @@ -92,11 +92,11 @@ The key non-obvious detail is that, for stake movement, the slippage check is ba 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 origin price you are willing to accept (e.g. a bit higher than current), for example $0.017$. + 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 we flipped the formula depending on direction (staking vs unstaking), the dynamic $\rightarrow$ dynamic case becomes very hard to reason about and easy to misuse. + - 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. @@ -380,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: From 77db0fb587d1f0e9a606aa6adae03fec6c4f421c Mon Sep 17 00:00:00 2001 From: michael trestman Date: Tue, 3 Mar 2026 07:29:20 -0800 Subject: [PATCH 4/4] wip --- docs/learn/price-protection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md index b0a291212..272d7cbe9 100644 --- a/docs/learn/price-protection.md +++ b/docs/learn/price-protection.md @@ -58,7 +58,7 @@ In Bittensor terminology: - **`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. +Stake transactions between subnets can be executed via slippage-protected `*_limit` variants (for example `swap_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.