Skip to content

Commit 64f183f

Browse files
authored
Merge pull request #2736 from opentensor/feat/safe-staking
Feat/safe staking
2 parents 9ded430 + 818eff9 commit 64f183f

File tree

11 files changed

+873
-80
lines changed

11 files changed

+873
-80
lines changed

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(
@@ -3460,6 +3474,9 @@ async def swap_stake(
34603474
amount: Balance,
34613475
wait_for_inclusion: bool = True,
34623476
wait_for_finalization: bool = False,
3477+
safe_staking: bool = False,
3478+
allow_partial_stake: bool = False,
3479+
rate_threshold: float = 0.005,
34633480
) -> bool:
34643481
"""
34653482
Moves stake between subnets while keeping the same coldkey-hotkey pair ownership.
@@ -3473,9 +3490,25 @@ async def swap_stake(
34733490
amount (Union[Balance, float]): The amount to swap.
34743491
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
34753492
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3493+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The swap
3494+
will only execute if the price ratio between subnets doesn't exceed the rate threshold.
3495+
Default is False.
3496+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial stake swaps when
3497+
the full amount would exceed the price threshold. If false, the entire swap fails if it would
3498+
exceed the threshold. Default is False.
3499+
rate_threshold (float): The maximum allowed increase in the price ratio between subnets
3500+
(origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used
3501+
when safe_staking is True. Default is 0.005.
34763502
34773503
Returns:
34783504
success (bool): True if the extrinsic was successful.
3505+
3506+
The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price
3507+
When safe_staking is enabled, the swap will only execute if:
3508+
- With allow_partial_stake=False: The entire swap amount can be executed without the price ratio
3509+
increasing more than rate_threshold
3510+
- With allow_partial_stake=True: A partial amount will be swapped up to the point where the
3511+
price ratio would increase by rate_threshold
34793512
"""
34803513
amount = check_and_convert_to_balance(amount)
34813514
return await swap_stake_extrinsic(
@@ -3487,6 +3520,9 @@ async def swap_stake(
34873520
amount=amount,
34883521
wait_for_inclusion=wait_for_inclusion,
34893522
wait_for_finalization=wait_for_finalization,
3523+
safe_staking=safe_staking,
3524+
allow_partial_stake=allow_partial_stake,
3525+
rate_threshold=rate_threshold,
34903526
)
34913527

34923528
async def transfer_stake(
@@ -3575,6 +3611,9 @@ async def unstake(
35753611
amount: Optional[Balance] = None,
35763612
wait_for_inclusion: bool = True,
35773613
wait_for_finalization: bool = False,
3614+
safe_staking: bool = False,
3615+
allow_partial_stake: bool = False,
3616+
rate_threshold: float = 0.005,
35783617
) -> bool:
35793618
"""
35803619
Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting
@@ -3584,10 +3623,17 @@ async def unstake(
35843623
wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being
35853624
removed.
35863625
hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from.
3587-
netuid (Optional[int]): Subnet unique ID.
3626+
netuid (Optional[int]): The unique identifier of the subnet.
35883627
amount (Balance): The amount of TAO to unstake. If not specified, unstakes all.
35893628
wait_for_inclusion (bool): Waits for the transaction to be included in a block.
35903629
wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain.
3630+
safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake
3631+
will only execute if the price change doesn't exceed the rate threshold. Default is False.
3632+
allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when
3633+
the full amount would exceed the price threshold. If false, the entire unstake fails if it would
3634+
exceed the threshold. Default is False.
3635+
rate_threshold (float): The maximum allowed price change ratio when unstaking. For example,
3636+
0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005.
35913637
35923638
Returns:
35933639
bool: ``True`` if the unstaking process is successful, False otherwise.
@@ -3604,6 +3650,9 @@ async def unstake(
36043650
amount=amount,
36053651
wait_for_inclusion=wait_for_inclusion,
36063652
wait_for_finalization=wait_for_finalization,
3653+
safe_staking=safe_staking,
3654+
allow_partial_stake=allow_partial_stake,
3655+
rate_threshold=rate_threshold,
36073656
)
36083657

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