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: