Skip to content

Commit e6b3449

Browse files
authored
Merge branch 'staging' into feat/zyzniewski/set_delegate_take
2 parents f4280b9 + a3a14cf commit e6b3449

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
@@ -2819,6 +2819,9 @@ async def add_stake(
28192819
amount: Optional[Balance] = None,
28202820
wait_for_inclusion: bool = True,
28212821
wait_for_finalization: bool = False,
2822+
safe_staking: bool = False,
2823+
allow_partial_stake: bool = False,
2824+
rate_threshold: float = 0.005,
28222825
) -> bool:
28232826
"""
28242827
Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address.
@@ -2832,12 +2835,20 @@ async def add_stake(
28322835
amount (Balance): The amount of TAO to stake.
28332836
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
28342837
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
2838+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake
2839+
will only execute if the price change doesn't exceed the rate threshold. Default is False.
2840+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when
2841+
the full amount would exceed the price threshold. If false, the entire stake fails if it would
2842+
exceed the threshold. Default is False.
2843+
rate_threshold (float): The maximum allowed price change ratio when staking. For example,
2844+
0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005.
28352845
28362846
Returns:
28372847
bool: ``True`` if the staking is successful, False otherwise.
28382848
2839-
This function enables neurons to increase their stake in the network, enhancing their influence and potential
2840-
rewards in line with Bittensor's consensus and reward mechanisms.
2849+
This function enables neurons to increase their stake in the network, enhancing their influence and potential.
2850+
When safe_staking is enabled, it provides protection against price fluctuations during the time stake is
2851+
executed and the time it is actually processed by the chain.
28412852
"""
28422853
amount = check_and_convert_to_balance(amount)
28432854
return await add_stake_extrinsic(
@@ -2848,6 +2859,9 @@ async def add_stake(
28482859
amount=amount,
28492860
wait_for_inclusion=wait_for_inclusion,
28502861
wait_for_finalization=wait_for_finalization,
2862+
safe_staking=safe_staking,
2863+
allow_partial_stake=allow_partial_stake,
2864+
rate_threshold=rate_threshold,
28512865
)
28522866

28532867
async def add_stake_multiple(
@@ -3545,6 +3559,9 @@ async def swap_stake(
35453559
amount: Balance,
35463560
wait_for_inclusion: bool = True,
35473561
wait_for_finalization: bool = False,
3562+
safe_staking: bool = False,
3563+
allow_partial_stake: bool = False,
3564+
rate_threshold: float = 0.005,
35483565
) -> bool:
35493566
"""
35503567
Moves stake between subnets while keeping the same coldkey-hotkey pair ownership.
@@ -3558,9 +3575,25 @@ async def swap_stake(
35583575
amount (Union[Balance, float]): The amount to swap.
35593576
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
35603577
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3578+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The swap
3579+
will only execute if the price ratio between subnets doesn't exceed the rate threshold.
3580+
Default is False.
3581+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial stake swaps when
3582+
the full amount would exceed the price threshold. If false, the entire swap fails if it would
3583+
exceed the threshold. Default is False.
3584+
rate_threshold (float): The maximum allowed increase in the price ratio between subnets
3585+
(origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used
3586+
when safe_staking is True. Default is 0.005.
35613587
35623588
Returns:
35633589
success (bool): True if the extrinsic was successful.
3590+
3591+
The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price
3592+
When safe_staking is enabled, the swap will only execute if:
3593+
- With allow_partial_stake=False: The entire swap amount can be executed without the price ratio
3594+
increasing more than rate_threshold
3595+
- With allow_partial_stake=True: A partial amount will be swapped up to the point where the
3596+
price ratio would increase by rate_threshold
35643597
"""
35653598
amount = check_and_convert_to_balance(amount)
35663599
return await swap_stake_extrinsic(
@@ -3572,6 +3605,9 @@ async def swap_stake(
35723605
amount=amount,
35733606
wait_for_inclusion=wait_for_inclusion,
35743607
wait_for_finalization=wait_for_finalization,
3608+
safe_staking=safe_staking,
3609+
allow_partial_stake=allow_partial_stake,
3610+
rate_threshold=rate_threshold,
35753611
)
35763612

35773613
async def transfer_stake(
@@ -3660,6 +3696,9 @@ async def unstake(
36603696
amount: Optional[Balance] = None,
36613697
wait_for_inclusion: bool = True,
36623698
wait_for_finalization: bool = False,
3699+
safe_staking: bool = False,
3700+
allow_partial_stake: bool = False,
3701+
rate_threshold: float = 0.005,
36633702
) -> bool:
36643703
"""
36653704
Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting
@@ -3669,10 +3708,17 @@ async def unstake(
36693708
wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being
36703709
removed.
36713710
hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from.
3672-
netuid (Optional[int]): Subnet unique ID.
3711+
netuid (Optional[int]): The unique identifier of the subnet.
36733712
amount (Balance): The amount of TAO to unstake. If not specified, unstakes all.
36743713
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
36753714
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3715+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake
3716+
will only execute if the price change doesn't exceed the rate threshold. Default is False.
3717+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when
3718+
the full amount would exceed the price threshold. If false, the entire unstake fails if it would
3719+
exceed the threshold. Default is False.
3720+
rate_threshold (float): The maximum allowed price change ratio when unstaking. For example,
3721+
0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005.
36763722
36773723
Returns:
36783724
bool: ``True`` if the unstaking process is successful, False otherwise.
@@ -3689,6 +3735,9 @@ async def unstake(
36893735
amount=amount,
36903736
wait_for_inclusion=wait_for_inclusion,
36913737
wait_for_finalization=wait_for_finalization,
3738+
safe_staking=safe_staking,
3739+
allow_partial_stake=allow_partial_stake,
3740+
rate_threshold=rate_threshold,
36923741
)
36933742

36943743
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)