Skip to content

Commit 77bb23b

Browse files
authored
Merge branch 'staging' into fix/zyzniewski/burned_register_supports_root_subnet
2 parents 36f12f6 + a3a14cf commit 77bb23b

File tree

15 files changed

+891
-83
lines changed

15 files changed

+891
-83
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## 9.0.5 /2025-03-12
4+
5+
## What's Changed
6+
* Refactor duplicated unittests code by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2724
7+
* Use uv for circleci by @thewhaleking in https://github.com/opentensor/bittensor/pull/2729
8+
* Fix E2E test_metagraph_info by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2728
9+
* Tests: deduplicate fake_wallet and correctly create Mock by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2730
10+
* E2E Test: wait cooldown period to check set_children effect by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2733
11+
* Tests: wait for Miner/Validator to fully start by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2737
12+
* All metagraph subtensor methods now use block by @thewhaleking in https://github.com/opentensor/bittensor/pull/2738
13+
* Tests: increse test_incentive timeout + fix sudo_set_weights_set_rate_limit by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2739
14+
* Feat/safe staking by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2736
15+
16+
**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.4...v9.0.5
17+
318
## 9.0.4 /2025-03-06
419

520
## What's Changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9.0.4
1+
9.0.5

bittensor/core/async_subtensor.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2810,6 +2810,9 @@ async def add_stake(
28102810
amount: Optional[Balance] = None,
28112811
wait_for_inclusion: bool = True,
28122812
wait_for_finalization: bool = False,
2813+
safe_staking: bool = False,
2814+
allow_partial_stake: bool = False,
2815+
rate_threshold: float = 0.005,
28132816
) -> bool:
28142817
"""
28152818
Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address.
@@ -2823,12 +2826,20 @@ async def add_stake(
28232826
amount (Balance): The amount of TAO to stake.
28242827
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
28252828
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
2829+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake
2830+
will only execute if the price change doesn't exceed the rate threshold. Default is False.
2831+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when
2832+
the full amount would exceed the price threshold. If false, the entire stake fails if it would
2833+
exceed the threshold. Default is False.
2834+
rate_threshold (float): The maximum allowed price change ratio when staking. For example,
2835+
0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005.
28262836
28272837
Returns:
28282838
bool: ``True`` if the staking is successful, False otherwise.
28292839
2830-
This function enables neurons to increase their stake in the network, enhancing their influence and potential
2831-
rewards in line with Bittensor's consensus and reward mechanisms.
2840+
This function enables neurons to increase their stake in the network, enhancing their influence and potential.
2841+
When safe_staking is enabled, it provides protection against price fluctuations during the time stake is
2842+
executed and the time it is actually processed by the chain.
28322843
"""
28332844
amount = check_and_convert_to_balance(amount)
28342845
return await add_stake_extrinsic(
@@ -2839,6 +2850,9 @@ async def add_stake(
28392850
amount=amount,
28402851
wait_for_inclusion=wait_for_inclusion,
28412852
wait_for_finalization=wait_for_finalization,
2853+
safe_staking=safe_staking,
2854+
allow_partial_stake=allow_partial_stake,
2855+
rate_threshold=rate_threshold,
28422856
)
28432857

28442858
async def add_stake_multiple(
@@ -3439,6 +3453,9 @@ async def swap_stake(
34393453
amount: Balance,
34403454
wait_for_inclusion: bool = True,
34413455
wait_for_finalization: bool = False,
3456+
safe_staking: bool = False,
3457+
allow_partial_stake: bool = False,
3458+
rate_threshold: float = 0.005,
34423459
) -> bool:
34433460
"""
34443461
Moves stake between subnets while keeping the same coldkey-hotkey pair ownership.
@@ -3452,9 +3469,25 @@ async def swap_stake(
34523469
amount (Union[Balance, float]): The amount to swap.
34533470
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
34543471
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3472+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The swap
3473+
will only execute if the price ratio between subnets doesn't exceed the rate threshold.
3474+
Default is False.
3475+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial stake swaps when
3476+
the full amount would exceed the price threshold. If false, the entire swap fails if it would
3477+
exceed the threshold. Default is False.
3478+
rate_threshold (float): The maximum allowed increase in the price ratio between subnets
3479+
(origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used
3480+
when safe_staking is True. Default is 0.005.
34553481
34563482
Returns:
34573483
success (bool): True if the extrinsic was successful.
3484+
3485+
The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price
3486+
When safe_staking is enabled, the swap will only execute if:
3487+
- With allow_partial_stake=False: The entire swap amount can be executed without the price ratio
3488+
increasing more than rate_threshold
3489+
- With allow_partial_stake=True: A partial amount will be swapped up to the point where the
3490+
price ratio would increase by rate_threshold
34583491
"""
34593492
amount = check_and_convert_to_balance(amount)
34603493
return await swap_stake_extrinsic(
@@ -3466,6 +3499,9 @@ async def swap_stake(
34663499
amount=amount,
34673500
wait_for_inclusion=wait_for_inclusion,
34683501
wait_for_finalization=wait_for_finalization,
3502+
safe_staking=safe_staking,
3503+
allow_partial_stake=allow_partial_stake,
3504+
rate_threshold=rate_threshold,
34693505
)
34703506

34713507
async def transfer_stake(
@@ -3554,6 +3590,9 @@ async def unstake(
35543590
amount: Optional[Balance] = None,
35553591
wait_for_inclusion: bool = True,
35563592
wait_for_finalization: bool = False,
3593+
safe_staking: bool = False,
3594+
allow_partial_stake: bool = False,
3595+
rate_threshold: float = 0.005,
35573596
) -> bool:
35583597
"""
35593598
Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting
@@ -3563,10 +3602,17 @@ async def unstake(
35633602
wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being
35643603
removed.
35653604
hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from.
3566-
netuid (Optional[int]): Subnet unique ID.
3605+
netuid (Optional[int]): The unique identifier of the subnet.
35673606
amount (Balance): The amount of TAO to unstake. If not specified, unstakes all.
35683607
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
35693608
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3609+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake
3610+
will only execute if the price change doesn't exceed the rate threshold. Default is False.
3611+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when
3612+
the full amount would exceed the price threshold. If false, the entire unstake fails if it would
3613+
exceed the threshold. Default is False.
3614+
rate_threshold (float): The maximum allowed price change ratio when unstaking. For example,
3615+
0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005.
35703616
35713617
Returns:
35723618
bool: ``True`` if the unstaking process is successful, False otherwise.
@@ -3583,6 +3629,9 @@ async def unstake(
35833629
amount=amount,
35843630
wait_for_inclusion=wait_for_inclusion,
35853631
wait_for_finalization=wait_for_finalization,
3632+
safe_staking=safe_staking,
3633+
allow_partial_stake=allow_partial_stake,
3634+
rate_threshold=rate_threshold,
35863635
)
35873636

35883637
async def unstake_multiple(

bittensor/core/chain_data/dynamic_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo":
7777
price = (
7878
Balance.from_tao(1.0)
7979
if netuid == 0
80-
else Balance.from_tao(tao_in.tao / alpha_in.tao)
80+
else Balance.from_tao(tao_in.tao / alpha_in.tao).set_unit(netuid)
8181
if alpha_in.tao > 0
82-
else Balance.from_tao(1)
82+
else Balance.from_tao(1).set_unit(netuid)
8383
) # Root always has 1-1 price
8484

8585
if decoded.get("subnet_identity"):

bittensor/core/extrinsics/asyncex/move_stake.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ async def swap_stake_extrinsic(
160160
amount: Balance,
161161
wait_for_inclusion: bool = True,
162162
wait_for_finalization: bool = False,
163+
safe_staking: bool = False,
164+
allow_partial_stake: bool = False,
165+
rate_threshold: float = 0.005,
163166
) -> bool:
164167
"""
165168
Swaps stake from one subnet to another for a given hotkey in the Bittensor network.
@@ -173,6 +176,9 @@ async def swap_stake_extrinsic(
173176
amount (Balance): The amount of stake to swap as a `Balance` object.
174177
wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True.
175178
wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False.
179+
safe_staking (bool): If true, enables price safety checks to protect against price impact.
180+
allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price threshold.
181+
rate_threshold (float): Maximum allowed increase in price ratio (0.005 = 0.5%).
176182
177183
Returns:
178184
bool: True if the swap was successful, False otherwise.
@@ -205,20 +211,47 @@ async def swap_stake_extrinsic(
205211
return False
206212

207213
try:
208-
logging.info(
209-
f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n"
210-
f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid "
211-
f"[yellow]{destination_netuid}[/yellow]"
212-
)
214+
call_params = {
215+
"hotkey": hotkey_ss58,
216+
"origin_netuid": origin_netuid,
217+
"destination_netuid": destination_netuid,
218+
"alpha_amount": amount.rao,
219+
}
220+
221+
if safe_staking:
222+
origin_pool, destination_pool = await asyncio.gather(
223+
subtensor.subnet(netuid=origin_netuid),
224+
subtensor.subnet(netuid=destination_netuid),
225+
)
226+
swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao
227+
swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_threshold)
228+
229+
logging.info(
230+
f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n"
231+
f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid "
232+
f"[green]{destination_netuid}[/green]\n"
233+
f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], "
234+
f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]"
235+
)
236+
call_params.update(
237+
{
238+
"limit_price": swap_rate_ratio_with_tolerance,
239+
"allow_partial": allow_partial_stake,
240+
}
241+
)
242+
call_function = "swap_stake_limit"
243+
else:
244+
logging.info(
245+
f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n"
246+
f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid "
247+
f"[green]{destination_netuid}[/green]"
248+
)
249+
call_function = "swap_stake"
250+
213251
call = await subtensor.substrate.compose_call(
214252
call_module="SubtensorModule",
215-
call_function="swap_stake",
216-
call_params={
217-
"hotkey": hotkey_ss58,
218-
"origin_netuid": origin_netuid,
219-
"destination_netuid": destination_netuid,
220-
"alpha_amount": amount.rao,
221-
},
253+
call_function=call_function,
254+
call_params=call_params,
222255
)
223256

224257
success, err_msg = await subtensor.sign_and_send_extrinsic(
@@ -253,7 +286,12 @@ async def swap_stake_extrinsic(
253286

254287
return True
255288
else:
256-
logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}")
289+
if safe_staking and "Custom error: 8" in err_msg:
290+
logging.error(
291+
":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking."
292+
)
293+
else:
294+
logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}")
257295
return False
258296

259297
except Exception as e:

bittensor/core/extrinsics/asyncex/staking.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ async def add_stake_extrinsic(
2121
amount: Optional[Balance] = None,
2222
wait_for_inclusion: bool = True,
2323
wait_for_finalization: bool = False,
24+
safe_staking: bool = False,
25+
allow_partial_stake: bool = False,
26+
rate_threshold: float = 0.005,
2427
) -> bool:
2528
"""
2629
Adds the specified amount of stake to passed hotkey `uid`.
@@ -36,6 +39,9 @@ async def add_stake_extrinsic(
3639
`False` if the extrinsic fails to enter the block within the timeout.
3740
wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
3841
or returns `False` if the extrinsic fails to be finalized within the timeout.
42+
safe_staking: If set, uses safe staking logic
43+
allow_partial_stake: If set, allows partial stake
44+
rate_threshold: The rate threshold for safe staking
3945
4046
Returns:
4147
success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
@@ -97,19 +103,48 @@ async def add_stake_extrinsic(
97103
return False
98104

99105
try:
100-
logging.info(
101-
f":satellite: [magenta]Staking to:[/magenta] "
102-
f"[blue]netuid: {netuid}, amount: {staking_balance} "
103-
f"on {subtensor.network}[/blue] [magenta]...[/magenta]"
104-
)
106+
call_params = {
107+
"hotkey": hotkey_ss58,
108+
"netuid": netuid,
109+
"amount_staked": staking_balance.rao,
110+
}
111+
112+
if safe_staking:
113+
pool = await subtensor.subnet(netuid=netuid)
114+
base_price = pool.price.rao
115+
price_with_tolerance = base_price * (1 + rate_threshold)
116+
call_params.update(
117+
{
118+
"limit_price": price_with_tolerance,
119+
"allow_partial": allow_partial_stake,
120+
}
121+
)
122+
call_function = "add_stake_limit"
123+
124+
# For logging
125+
base_rate = pool.price.tao
126+
rate_with_tolerance = base_rate * (1 + rate_threshold)
127+
logging.info(
128+
f":satellite: [magenta]Safe Staking to:[/magenta] "
129+
f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], "
130+
f"tolerance percentage: [green]{rate_threshold*100}%[/green], "
131+
f"price limit: [green]{rate_with_tolerance}[/green], "
132+
f"original price: [green]{base_rate}[/green], "
133+
f"with partial stake: [green]{allow_partial_stake}[/green] "
134+
f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]"
135+
)
136+
else:
137+
logging.info(
138+
f":satellite: [magenta]Staking to:[/magenta] "
139+
f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green] "
140+
f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]"
141+
)
142+
call_function = "add_stake"
143+
105144
call = await subtensor.substrate.compose_call(
106145
call_module="SubtensorModule",
107-
call_function="add_stake",
108-
call_params={
109-
"hotkey": hotkey_ss58,
110-
"amount_staked": staking_balance.rao,
111-
"netuid": netuid,
112-
},
146+
call_function=call_function,
147+
call_params=call_params,
113148
)
114149
staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
115150
call,
@@ -152,7 +187,12 @@ async def add_stake_extrinsic(
152187
)
153188
return True
154189
else:
155-
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
190+
if safe_staking and "Custom error: 8" in err_msg:
191+
logging.error(
192+
":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking."
193+
)
194+
else:
195+
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
156196
return False
157197

158198
except NotRegisteredError:

0 commit comments

Comments
 (0)