From 69a825819b9cbce0e368c5d3dede5822e17b5caa Mon Sep 17 00:00:00 2001 From: mimalsm Date: Mon, 15 Dec 2025 21:00:06 +0100 Subject: [PATCH 1/4] feat: Add slippage protection options to stake swap and transfer commands This commit implements GitHub issue #644 by adding slippage protection options (--tolerance, --safe-staking, --allow-partial-stake) to both 'btcli stake swap' and 'btcli stake transfer' commands, making them consistent with 'btcli stake add'. Changes: - Added rate_tolerance, safe_staking, and allow_partial_stake parameters to stake_swap CLI command - Added rate_tolerance, safe_staking, and allow_partial_stake parameters to stake_transfer CLI command - Updated swap_stake() function in move.py to accept and use safe_swapping, allow_partial_stake, and rate_tolerance parameters - Modified swap_stake() to use swap_stake_limit when safe_swapping=True, which enforces price ratio limits on-chain to protect against slippage - Updated transfer_stake() function signature to accept these parameters for API consistency (chain-level support may be pending) - Integrated with existing ask_safe_staking(), ask_rate_tolerance(), and ask_partial_stake() helper methods for consistent user experience - Updated command docstrings with examples showing the new options Technical Details: - Safe swapping protects against price slippage by calculating: swap_rate_ratio = origin_price / destination_price limit_price = swap_rate_ratio * (1 + rate_tolerance) - Uses swap_stake_limit() instead of swap_stake() when safe_swapping=True - Default behavior matches stake_add: safe staking enabled, 5% tolerance - Options follow same naming conventions as stake_add: --tolerance/--rate-tolerance, --safe/--safe-staking, --unsafe/--no-safe-staking, --partial/--allow-partial-stake Fixes: #644 --- bittensor_cli/cli.py | 56 ++++++++++++++++++++- bittensor_cli/src/commands/stake/move.py | 62 ++++++++++++++++++++---- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 585b1a6d3..4209d69ec 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5370,6 +5370,9 @@ def stake_transfer( proxy: Optional[str] = Options.proxy, announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, + rate_tolerance: Optional[float] = Options.rate_tolerance, + safe_staking: Optional[bool] = Options.safe_staking, + allow_partial_stake: Optional[bool] = Options.allow_partial_stake, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -5408,6 +5411,15 @@ def stake_transfer( Transfer stake without MEV protection: [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection + + Safe transfer with rate tolerance of 10%: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --safe --tolerance 0.1 + + Allow partial stake if rates change with tolerance of 10%: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --safe --partial --tolerance 0.1 + + Unsafe transfer with no rate protection: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --unsafe """ self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) @@ -5494,6 +5506,15 @@ def stake_transfer( dest_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid)" ) + # Handle safe staking options similar to stake_add + safe_staking_transfer = self.ask_safe_staking(safe_staking) + if safe_staking_transfer: + rate_tolerance_val = self.ask_rate_tolerance(rate_tolerance) + allow_partial_stake_val = self.ask_partial_stake(allow_partial_stake) + else: + rate_tolerance_val = 0.005 # Default, won't be used + allow_partial_stake_val = False + logger.debug( "args:\n" f"network: {network}\n" @@ -5505,7 +5526,10 @@ def stake_transfer( f"amount: {amount}\n" f"era: {period}\n" f"stake_all: {stake_all}\n" - f"mev_protection: {mev_protection}" + f"mev_protection: {mev_protection}\n" + f"safe_staking: {safe_staking_transfer}\n" + f"rate_tolerance: {rate_tolerance_val}\n" + f"allow_partial_stake: {allow_partial_stake_val}\n" f"proxy: {proxy}" ) result, ext_id = self._run_command( @@ -5523,6 +5547,9 @@ def stake_transfer( prompt=prompt, proxy=proxy, mev_protection=mev_protection, + safe_staking=safe_staking_transfer, + rate_tolerance=rate_tolerance_val, + allow_partial_stake=allow_partial_stake_val, ) ) if json_output: @@ -5570,6 +5597,9 @@ def stake_swap( wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, mev_protection: bool = Options.mev_protection, + rate_tolerance: Optional[float] = Options.rate_tolerance, + safe_staking: Optional[bool] = Options.safe_staking, + allow_partial_stake: Optional[bool] = Options.allow_partial_stake, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -5596,6 +5626,15 @@ def stake_swap( 2. Swap stake without MEV protection: [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection + + 3. Safe swapping with rate tolerance of 10%: + [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --safe --tolerance 0.1 + + 4. Allow partial stake if rates change with tolerance of 10%: + [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --safe --partial --tolerance 0.1 + + 5. Unsafe swapping with no rate protection: + [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --unsafe """ self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) @@ -5612,6 +5651,15 @@ def stake_swap( validate=WV.WALLET_AND_HOTKEY, ) + # Handle safe staking options similar to stake_add + safe_swapping = self.ask_safe_staking(safe_staking) + if safe_swapping: + rate_tolerance = self.ask_rate_tolerance(rate_tolerance) + allow_partial_stake = self.ask_partial_stake(allow_partial_stake) + else: + rate_tolerance = 0.005 # Default, won't be used + allow_partial_stake = False + interactive_selection = False if origin_netuid is None and dest_netuid is None and not amount: interactive_selection = True @@ -5640,6 +5688,9 @@ def stake_swap( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" f"mev_protection: {mev_protection}\n" + f"safe_swapping: {safe_swapping}\n" + f"rate_tolerance: {rate_tolerance}\n" + f"allow_partial_stake: {allow_partial_stake}\n" ) result, ext_id = self._run_command( move_stake.swap_stake( @@ -5656,6 +5707,9 @@ def stake_swap( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, mev_protection=mev_protection, + safe_swapping=safe_swapping, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) ) if json_output: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 8f9255f70..0418f4091 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -667,6 +667,9 @@ async def transfer_stake( prompt: bool = True, proxy: Optional[str] = None, mev_protection: bool = True, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> tuple[bool, str]: """Transfers stake from one network to another. @@ -684,6 +687,13 @@ async def transfer_stake( stake_all: If true, transfer all stakes. proxy: Optional proxy to use for this extrinsic mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. + Note: Currently not fully supported at the chain level for transfer_stake, but included for API consistency. + allow_partial_stake: If true and safe_staking is enabled, allows partial stake transfers when the full amount + would exceed the price tolerance. Note: Currently not fully supported at the chain level for transfer_stake. + rate_tolerance: The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when + safe_staking is True. Note: Currently not fully supported at the chain level for transfer_stake. Returns: tuple: @@ -873,6 +883,9 @@ async def swap_stake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, mev_protection: bool = True, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> tuple[bool, str]: """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -890,6 +903,12 @@ async def swap_stake( wait_for_inclusion: If true, waits for the transaction to be included in a block. wait_for_finalization: If true, waits for the transaction to be finalized. mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. + safe_swapping: If true, enables price safety checks to protect against fluctuating prices. + allow_partial_stake: If true and safe_swapping is enabled, allows partial stake swaps when the full amount + would exceed the price tolerance. + rate_tolerance: The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when + safe_swapping is True. Returns: (success, extrinsic_identifier): @@ -947,16 +966,39 @@ async def swap_stake( ) return False, "" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="swap_stake", - call_params={ - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount_to_swap.rao, - }, - ) + # Build call with or without safe swapping + if safe_swapping: + # Get subnet prices to calculate rate ratio with tolerance + origin_subnet, destination_subnet = await asyncio.gather( + subtensor.subnet(origin_netuid), + subtensor.subnet(destination_netuid), + ) + swap_rate_ratio = origin_subnet.price.rao / destination_subnet.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_swap.rao, + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + }, + ) + else: + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_stake", + call_params={ + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_swap.rao, + }, + ) sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, From 7b403a63a387f6a3b1f32756fd9af0226999c354 Mon Sep 17 00:00:00 2001 From: mimalsm Date: Wed, 17 Dec 2025 19:17:00 +0100 Subject: [PATCH 2/4] test: slippage for stake swap transfer --- .../test_stake_slippage_protection.py | 892 ++++++++++++++++++ 1 file changed, 892 insertions(+) create mode 100644 tests/e2e_tests/test_stake_slippage_protection.py diff --git a/tests/e2e_tests/test_stake_slippage_protection.py b/tests/e2e_tests/test_stake_slippage_protection.py new file mode 100644 index 000000000..c3bea4a89 --- /dev/null +++ b/tests/e2e_tests/test_stake_slippage_protection.py @@ -0,0 +1,892 @@ +import asyncio +import json +import pytest + +from .utils import find_stake_entries, set_storage_extrinsic + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_stake_slippage_protection(local_chain, wallet_setup): + """ + Test slippage protection options for swap and transfer commands. + + Steps: + 0. Initial setup: Make alice own SN 0, create SN2, SN3, SN4, start emissions on all subnets. + 1. Activation: Register Bob on subnets 2 and 3; add initial stake for V3 activation. + 2. Test Swap with slippage protection (--safe --tolerance) + 3. Test Swap without slippage protection (--unsafe) + 4. Test Transfer with slippage protection (--safe --tolerance) + 5. Test Transfer without slippage protection (--unsafe) + 6. Test Swap with slippage protection and partial stake (--safe --tolerance --partial) + 7. Test Transfer with slippage protection and partial stake (--safe --tolerance --partial) + + Note: + - All movement commands executed with mev shield + - Stake commands executed without shield to speed up tests + - Shield for stake commands is already covered in its own test + """ + print("Testing slippage protection for swap and transfer commands ๐Ÿงช") + + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + + # Force Alice to own SN0 by setting storage + sn0_owner_storage_items = [ + ( + bytes.fromhex( + "658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000" + ), + bytes.fromhex( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ), + ) + ] + asyncio.run( + set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=sn0_owner_storage_items, + ) + ) + + # Create SN2, SN3, SN4 for swap/transfer checks + subnets_to_create = [2, 3, 4] + for netuid in subnets_to_create: + create_subnet_result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--json-output", + ], + ) + create_subnet_payload = json.loads(create_subnet_result.stdout) + assert create_subnet_payload["success"] is True + assert create_subnet_payload["netuid"] == netuid + + # Start emission schedule for subnets (including root netuid 0) + for netuid in [0] + subnets_to_create: + start_emission_result = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + f"Successfully started subnet {netuid}'s emission schedule." + in start_emission_result.stdout + ) + + # Alice is already registered - register Bob on the two non-root subnets + for netuid in [2, 3]: + register_bob_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Registered" in register_bob_result.stdout, register_bob_result.stderr + assert "Your extrinsic has been included" in register_bob_result.stdout, ( + register_bob_result.stdout + ) + + # Add initial stake to enable V3 (1 TAO) on all created subnets + for netuid in [2, 3, 4]: + add_initial_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "1", + "--unsafe", + "--no-prompt", + "--era", + "144", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in add_initial_stake_result.stdout, ( + add_initial_stake_result.stderr + ) + + ################################ + # TEST 1: Swap with slippage protection (--safe --tolerance) + ################################ + + # Add stake for swap test with slippage protection + swap_safe_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in swap_safe_seed_stake_result.stdout, ( + swap_safe_seed_stake_result.stderr + ) + + print("โœ… Swap with slippage protection seed stake finalized") + + # Verify stake was added + alice_stake_before_swap_safe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_swap_safe = json.loads(alice_stake_before_swap_safe.stdout) + alice_stakes_before_swap_safe = find_stake_entries( + alice_stake_list_before_swap_safe, + netuid=2, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_swap_safe) > 0 + assert any(stake["stake_value"] >= 20 for stake in alice_stakes_before_swap_safe) + + # Swap stake with slippage protection (--safe --tolerance) + # Swap to root netuid (0) which has more liquidity + swap_safe_amount = 20 + swap_safe_result = exec_command_alice( + command="stake", + sub_command="swap", + extra_args=[ + "--origin-netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", # Root netuid has more liquidity + "--amount", + str(swap_safe_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--safe", + "--tolerance", + "0.1", # 10% tolerance + ], + ) + assert "โœ… Sent" in swap_safe_result.stdout, swap_safe_result.stderr + + # Verify stake was swapped + alice_stake_after_swap_safe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_after_swap_safe = json.loads(alice_stake_after_swap_safe.stdout) + alice_stakes_after_swap_safe = find_stake_entries( + alice_stake_list_after_swap_safe, + netuid=0, # Root netuid + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_after_swap_safe) > 0 + assert any( + stake["stake_value"] >= swap_safe_amount + for stake in alice_stakes_after_swap_safe + ) + print("โœ… TEST 1: Swap with slippage protection completed successfully") + + ################################ + # TEST 2: Swap without slippage protection (--unsafe) + ################################ + + # Add stake for swap test without slippage protection + # Use netuid 4 as origin since we already have stake in netuid 0 from previous swap + swap_unsafe_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in swap_unsafe_seed_stake_result.stdout, ( + swap_unsafe_seed_stake_result.stderr + ) + print("โœ… Swap without slippage protection seed stake finalized") + + # Verify stake was added + alice_stake_before_swap_unsafe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_swap_unsafe = json.loads( + alice_stake_before_swap_unsafe.stdout + ) + alice_stakes_before_swap_unsafe = find_stake_entries( + alice_stake_list_before_swap_unsafe, + netuid=4, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_swap_unsafe) > 0 + assert any(stake["stake_value"] >= 20 for stake in alice_stakes_before_swap_unsafe) + + # Swap stake without slippage protection (--unsafe) + # Swap to root netuid (0) which has more liquidity + swap_unsafe_amount = 20 + swap_unsafe_result = exec_command_alice( + command="stake", + sub_command="swap", + extra_args=[ + "--origin-netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", # Root netuid has more liquidity + "--amount", + str(swap_unsafe_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--unsafe", + ], + ) + assert "โœ… Sent" in swap_unsafe_result.stdout, swap_unsafe_result.stderr + + # Verify stake was swapped + alice_stake_after_swap_unsafe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_after_swap_unsafe = json.loads( + alice_stake_after_swap_unsafe.stdout + ) + alice_stakes_after_swap_unsafe = find_stake_entries( + alice_stake_list_after_swap_unsafe, + netuid=0, # Root netuid + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_after_swap_unsafe) > 0 + assert any( + stake["stake_value"] >= swap_unsafe_amount + for stake in alice_stakes_after_swap_unsafe + ) + print("โœ… TEST 2: Swap without slippage protection completed successfully") + + ################################ + # TEST 3: Transfer with slippage protection (--safe --tolerance) + ################################ + + # Add stake for transfer test with slippage protection + transfer_safe_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in transfer_safe_seed_stake_result.stdout, ( + transfer_safe_seed_stake_result.stderr + ) + print("โœ… Transfer with slippage protection seed stake finalized") + + # Verify stake was added + alice_stake_before_transfer_safe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_transfer_safe = json.loads( + alice_stake_before_transfer_safe.stdout + ) + alice_stakes_before_transfer_safe = find_stake_entries( + alice_stake_list_before_transfer_safe, + netuid=0, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_transfer_safe) > 0 + assert any( + stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_safe + ) + + # Transfer stake with slippage protection (--safe --tolerance) + transfer_safe_amount = 20 + transfer_safe_result = exec_command_alice( + command="stake", + sub_command="transfer", + extra_args=[ + "--origin-netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--dest", + wallet_bob.coldkeypub.ss58_address, + "--amount", + str(transfer_safe_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--safe", + "--tolerance", + "0.1", # 10% tolerance + ], + ) + assert "โœ… Sent" in transfer_safe_result.stdout, transfer_safe_result.stderr + + # Verify stake was transferred + bob_stake_after_transfer_safe = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + bob_stake_list_after_transfer_safe = json.loads( + bob_stake_after_transfer_safe.stdout + ) + bob_stakes_after_transfer_safe = find_stake_entries( + bob_stake_list_after_transfer_safe, + netuid=0, + ) + assert len(bob_stakes_after_transfer_safe) > 0 + assert any( + stake["stake_value"] >= transfer_safe_amount + for stake in bob_stakes_after_transfer_safe + ) + print("โœ… TEST 3: Transfer with slippage protection completed successfully") + + ################################ + # TEST 4: Transfer without slippage protection (--unsafe) + ################################ + + # Add stake for transfer test without slippage protection + transfer_unsafe_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in transfer_unsafe_seed_stake_result.stdout, ( + transfer_unsafe_seed_stake_result.stderr + ) + print("โœ… Transfer without slippage protection seed stake finalized") + + # Verify stake was added + alice_stake_before_transfer_unsafe = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_transfer_unsafe = json.loads( + alice_stake_before_transfer_unsafe.stdout + ) + alice_stakes_before_transfer_unsafe = find_stake_entries( + alice_stake_list_before_transfer_unsafe, + netuid=0, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_transfer_unsafe) > 0 + assert any( + stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_unsafe + ) + + # Transfer stake without slippage protection (--unsafe) + transfer_unsafe_amount = 20 + transfer_unsafe_result = exec_command_alice( + command="stake", + sub_command="transfer", + extra_args=[ + "--origin-netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--dest", + wallet_bob.coldkeypub.ss58_address, + "--amount", + str(transfer_unsafe_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--unsafe", + ], + ) + assert "โœ… Sent" in transfer_unsafe_result.stdout, transfer_unsafe_result.stderr + + # Verify stake was transferred + bob_stake_after_transfer_unsafe = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + bob_stake_list_after_transfer_unsafe = json.loads( + bob_stake_after_transfer_unsafe.stdout + ) + bob_stakes_after_transfer_unsafe = find_stake_entries( + bob_stake_list_after_transfer_unsafe, + netuid=0, + ) + assert len(bob_stakes_after_transfer_unsafe) > 0 + assert any( + stake["stake_value"] >= transfer_unsafe_amount + for stake in bob_stakes_after_transfer_unsafe + ) + print("โœ… TEST 4: Transfer without slippage protection completed successfully") + + ################################ + # TEST 5: Swap with slippage protection and partial stake (--safe --tolerance --partial) + ################################ + + # Add stake for swap test with partial stake option + swap_partial_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in swap_partial_seed_stake_result.stdout, ( + swap_partial_seed_stake_result.stderr + ) + print("โœ… Swap with partial stake seed stake finalized") + + # Verify stake was added + alice_stake_before_swap_partial = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_swap_partial = json.loads( + alice_stake_before_swap_partial.stdout + ) + alice_stakes_before_swap_partial = find_stake_entries( + alice_stake_list_before_swap_partial, + netuid=2, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_swap_partial) > 0 + assert any(stake["stake_value"] >= 20 for stake in alice_stakes_before_swap_partial) + + # Swap stake with slippage protection and partial stake (--safe --tolerance --partial) + swap_partial_amount = 20 + swap_partial_result = exec_command_alice( + command="stake", + sub_command="swap", + extra_args=[ + "--origin-netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", # Root netuid has more liquidity + "--amount", + str(swap_partial_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--safe", + "--tolerance", + "0.1", # 10% tolerance + "--partial", # Allow partial stake if rates change + ], + ) + assert "โœ… Sent" in swap_partial_result.stdout, swap_partial_result.stderr + + # Verify stake was swapped (may be partial) + alice_stake_after_swap_partial = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_after_swap_partial = json.loads( + alice_stake_after_swap_partial.stdout + ) + alice_stakes_after_swap_partial = find_stake_entries( + alice_stake_list_after_swap_partial, + netuid=0, # Root netuid + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + # With partial stake, we expect at least some stake to be swapped + assert len(alice_stakes_after_swap_partial) > 0 + print( + "โœ… TEST 5: Swap with slippage protection and partial stake completed successfully" + ) + + ################################ + # TEST 6: Transfer with slippage protection and partial stake (--safe --tolerance --partial) + ################################ + + # Add stake for transfer test with partial stake option + transfer_partial_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in transfer_partial_seed_stake_result.stdout, ( + transfer_partial_seed_stake_result.stderr + ) + print("โœ… Transfer with partial stake seed stake finalized") + + # Verify stake was added + alice_stake_before_transfer_partial = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + alice_stake_list_before_transfer_partial = json.loads( + alice_stake_before_transfer_partial.stdout + ) + alice_stakes_before_transfer_partial = find_stake_entries( + alice_stake_list_before_transfer_partial, + netuid=0, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + assert len(alice_stakes_before_transfer_partial) > 0 + assert any( + stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_partial + ) + + # Transfer stake with slippage protection and partial stake (--safe --tolerance --partial) + transfer_partial_amount = 20 + transfer_partial_result = exec_command_alice( + command="stake", + sub_command="transfer", + extra_args=[ + "--origin-netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--dest", + wallet_bob.coldkeypub.ss58_address, + "--amount", + str(transfer_partial_amount), + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--safe", + "--tolerance", + "0.1", # 10% tolerance + "--partial", # Allow partial stake if rates change + ], + ) + assert "โœ… Sent" in transfer_partial_result.stdout, transfer_partial_result.stderr + + # Verify stake was transferred (may be partial) + bob_stake_after_transfer_partial = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + bob_stake_list_after_transfer_partial = json.loads( + bob_stake_after_transfer_partial.stdout + ) + bob_stakes_after_transfer_partial = find_stake_entries( + bob_stake_list_after_transfer_partial, + netuid=0, + ) + # With partial stake, we expect at least some stake to be transferred + assert len(bob_stakes_after_transfer_partial) > 0 + print( + "โœ… TEST 6: Transfer with slippage protection and partial stake completed successfully" + ) + + print("โœ… Passed all slippage protection tests for swap and transfer commands") From e6e15005d7d9d4380d39df3b7b2587e67ddeaf2a Mon Sep 17 00:00:00 2001 From: mimalsm Date: Thu, 18 Dec 2025 02:16:27 +0100 Subject: [PATCH 3/4] feat: remove unused args for stake transfer --- bittensor_cli/cli.py | 19 ++++--------------- bittensor_cli/src/commands/stake/move.py | 19 +++++-------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a39fda962..407ae34eb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5577,14 +5577,10 @@ def stake_transfer( dest_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid)" ) - # Handle safe staking options similar to stake_add safe_staking_transfer = self.ask_safe_staking(safe_staking) if safe_staking_transfer: rate_tolerance_val = self.ask_rate_tolerance(rate_tolerance) allow_partial_stake_val = self.ask_partial_stake(allow_partial_stake) - else: - rate_tolerance_val = 0.005 # Default, won't be used - allow_partial_stake_val = False logger.debug( "args:\n" @@ -5620,9 +5616,6 @@ def stake_transfer( quiet=quiet, proxy=proxy, mev_protection=mev_protection, - safe_staking=safe_staking_transfer, - rate_tolerance=rate_tolerance_val, - allow_partial_stake=allow_partial_stake_val, ) ) if json_output: @@ -5725,14 +5718,10 @@ def stake_swap( validate=WV.WALLET_AND_HOTKEY, ) - # Handle safe staking options similar to stake_add - safe_swapping = self.ask_safe_staking(safe_staking) - if safe_swapping: + safe_staking = self.ask_safe_staking(safe_staking) + if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) allow_partial_stake = self.ask_partial_stake(allow_partial_stake) - else: - rate_tolerance = 0.005 # Default, won't be used - allow_partial_stake = False interactive_selection = False if origin_netuid is None and dest_netuid is None and not amount: @@ -5762,7 +5751,7 @@ def stake_swap( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" f"mev_protection: {mev_protection}\n" - f"safe_swapping: {safe_swapping}\n" + f"safe_staking: {safe_staking}\n" f"rate_tolerance: {rate_tolerance}\n" f"allow_partial_stake: {allow_partial_stake}\n" ) @@ -5783,7 +5772,7 @@ def stake_swap( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, mev_protection=mev_protection, - safe_swapping=safe_swapping, + safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a40e67d0d..839c8d3de 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -674,9 +674,6 @@ async def transfer_stake( quiet: bool = False, proxy: Optional[str] = None, mev_protection: bool = True, - safe_staking: bool = False, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, ) -> tuple[bool, str]: """Transfers stake from one network to another. @@ -694,13 +691,6 @@ async def transfer_stake( stake_all: If true, transfer all stakes. proxy: Optional proxy to use for this extrinsic mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. - safe_staking: If true, enables price safety checks to protect against fluctuating prices. - Note: Currently not fully supported at the chain level for transfer_stake, but included for API consistency. - allow_partial_stake: If true and safe_staking is enabled, allows partial stake transfers when the full amount - would exceed the price tolerance. Note: Currently not fully supported at the chain level for transfer_stake. - rate_tolerance: The maximum allowed increase in the price ratio between subnets - (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when - safe_staking is True. Note: Currently not fully supported at the chain level for transfer_stake. Returns: tuple: @@ -894,7 +884,7 @@ async def swap_stake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, mev_protection: bool = True, - safe_swapping: bool = False, + safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, ) -> tuple[bool, str]: @@ -978,14 +968,15 @@ async def swap_stake( return False, "" # Build call with or without safe swapping - if safe_swapping: + if safe_staking: # Get subnet prices to calculate rate ratio with tolerance origin_subnet, destination_subnet = await asyncio.gather( subtensor.subnet(origin_netuid), subtensor.subnet(destination_netuid), ) swap_rate_ratio = origin_subnet.price.rao / destination_subnet.price.rao - swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 - rate_tolerance) + limit_price_rao = Balance.from_tao(swap_rate_ratio_with_tolerance).rao call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -995,7 +986,7 @@ async def swap_stake( "origin_netuid": origin_netuid, "destination_netuid": destination_netuid, "alpha_amount": amount_to_swap.rao, - "limit_price": swap_rate_ratio_with_tolerance, + "limit_price": limit_price_rao, "allow_partial": allow_partial_stake, }, ) From cb84bf700f9df3068d461df44d9a9bc5d1a7cb05 Mon Sep 17 00:00:00 2001 From: mimalsm Date: Thu, 18 Dec 2025 02:19:49 +0100 Subject: [PATCH 4/4] test: remove tests for transfer --- .../test_stake_slippage_protection.py | 379 +----------------- 1 file changed, 6 insertions(+), 373 deletions(-) diff --git a/tests/e2e_tests/test_stake_slippage_protection.py b/tests/e2e_tests/test_stake_slippage_protection.py index c3bea4a89..43ecd1a15 100644 --- a/tests/e2e_tests/test_stake_slippage_protection.py +++ b/tests/e2e_tests/test_stake_slippage_protection.py @@ -8,24 +8,21 @@ @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_stake_slippage_protection(local_chain, wallet_setup): """ - Test slippage protection options for swap and transfer commands. + Test slippage protection options for swap command. Steps: 0. Initial setup: Make alice own SN 0, create SN2, SN3, SN4, start emissions on all subnets. 1. Activation: Register Bob on subnets 2 and 3; add initial stake for V3 activation. 2. Test Swap with slippage protection (--safe --tolerance) 3. Test Swap without slippage protection (--unsafe) - 4. Test Transfer with slippage protection (--safe --tolerance) - 5. Test Transfer without slippage protection (--unsafe) - 6. Test Swap with slippage protection and partial stake (--safe --tolerance --partial) - 7. Test Transfer with slippage protection and partial stake (--safe --tolerance --partial) + 4. Test Swap with slippage protection and partial stake (--safe --tolerance --partial) Note: - All movement commands executed with mev shield - Stake commands executed without shield to speed up tests - Shield for stake commands is already covered in its own test """ - print("Testing slippage protection for swap and transfer commands ๐Ÿงช") + print("Testing slippage protection for swap command ๐Ÿงช") wallet_path_alice = "//Alice" wallet_path_bob = "//Bob" @@ -407,249 +404,7 @@ def test_stake_slippage_protection(local_chain, wallet_setup): print("โœ… TEST 2: Swap without slippage protection completed successfully") ################################ - # TEST 3: Transfer with slippage protection (--safe --tolerance) - ################################ - - # Add stake for transfer test with slippage protection - transfer_safe_seed_stake_result = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "25", - "--no-prompt", - "--era", - "144", - "--unsafe", - "--no-mev-protection", - ], - ) - assert "โœ… Finalized" in transfer_safe_seed_stake_result.stdout, ( - transfer_safe_seed_stake_result.stderr - ) - print("โœ… Transfer with slippage protection seed stake finalized") - - # Verify stake was added - alice_stake_before_transfer_safe = exec_command_alice( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - alice_stake_list_before_transfer_safe = json.loads( - alice_stake_before_transfer_safe.stdout - ) - alice_stakes_before_transfer_safe = find_stake_entries( - alice_stake_list_before_transfer_safe, - netuid=0, - hotkey_ss58=wallet_alice.hotkey.ss58_address, - ) - assert len(alice_stakes_before_transfer_safe) > 0 - assert any( - stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_safe - ) - - # Transfer stake with slippage protection (--safe --tolerance) - transfer_safe_amount = 20 - transfer_safe_result = exec_command_alice( - command="stake", - sub_command="transfer", - extra_args=[ - "--origin-netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--dest-netuid", - "0", - "--dest", - wallet_bob.coldkeypub.ss58_address, - "--amount", - str(transfer_safe_amount), - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--safe", - "--tolerance", - "0.1", # 10% tolerance - ], - ) - assert "โœ… Sent" in transfer_safe_result.stdout, transfer_safe_result.stderr - - # Verify stake was transferred - bob_stake_after_transfer_safe = exec_command_bob( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_bob, - "--wallet-name", - wallet_bob.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - bob_stake_list_after_transfer_safe = json.loads( - bob_stake_after_transfer_safe.stdout - ) - bob_stakes_after_transfer_safe = find_stake_entries( - bob_stake_list_after_transfer_safe, - netuid=0, - ) - assert len(bob_stakes_after_transfer_safe) > 0 - assert any( - stake["stake_value"] >= transfer_safe_amount - for stake in bob_stakes_after_transfer_safe - ) - print("โœ… TEST 3: Transfer with slippage protection completed successfully") - - ################################ - # TEST 4: Transfer without slippage protection (--unsafe) - ################################ - - # Add stake for transfer test without slippage protection - transfer_unsafe_seed_stake_result = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "25", - "--no-prompt", - "--era", - "144", - "--unsafe", - "--no-mev-protection", - ], - ) - assert "โœ… Finalized" in transfer_unsafe_seed_stake_result.stdout, ( - transfer_unsafe_seed_stake_result.stderr - ) - print("โœ… Transfer without slippage protection seed stake finalized") - - # Verify stake was added - alice_stake_before_transfer_unsafe = exec_command_alice( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - alice_stake_list_before_transfer_unsafe = json.loads( - alice_stake_before_transfer_unsafe.stdout - ) - alice_stakes_before_transfer_unsafe = find_stake_entries( - alice_stake_list_before_transfer_unsafe, - netuid=0, - hotkey_ss58=wallet_alice.hotkey.ss58_address, - ) - assert len(alice_stakes_before_transfer_unsafe) > 0 - assert any( - stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_unsafe - ) - - # Transfer stake without slippage protection (--unsafe) - transfer_unsafe_amount = 20 - transfer_unsafe_result = exec_command_alice( - command="stake", - sub_command="transfer", - extra_args=[ - "--origin-netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--dest-netuid", - "0", - "--dest", - wallet_bob.coldkeypub.ss58_address, - "--amount", - str(transfer_unsafe_amount), - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--unsafe", - ], - ) - assert "โœ… Sent" in transfer_unsafe_result.stdout, transfer_unsafe_result.stderr - - # Verify stake was transferred - bob_stake_after_transfer_unsafe = exec_command_bob( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_bob, - "--wallet-name", - wallet_bob.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - bob_stake_list_after_transfer_unsafe = json.loads( - bob_stake_after_transfer_unsafe.stdout - ) - bob_stakes_after_transfer_unsafe = find_stake_entries( - bob_stake_list_after_transfer_unsafe, - netuid=0, - ) - assert len(bob_stakes_after_transfer_unsafe) > 0 - assert any( - stake["stake_value"] >= transfer_unsafe_amount - for stake in bob_stakes_after_transfer_unsafe - ) - print("โœ… TEST 4: Transfer without slippage protection completed successfully") - - ################################ - # TEST 5: Swap with slippage protection and partial stake (--safe --tolerance --partial) + # TEST 3: Swap with slippage protection and partial stake (--safe --tolerance --partial) ################################ # Add stake for swap test with partial stake option @@ -764,129 +519,7 @@ def test_stake_slippage_protection(local_chain, wallet_setup): # With partial stake, we expect at least some stake to be swapped assert len(alice_stakes_after_swap_partial) > 0 print( - "โœ… TEST 5: Swap with slippage protection and partial stake completed successfully" - ) - - ################################ - # TEST 6: Transfer with slippage protection and partial stake (--safe --tolerance --partial) - ################################ - - # Add stake for transfer test with partial stake option - transfer_partial_seed_stake_result = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "25", - "--no-prompt", - "--era", - "144", - "--unsafe", - "--no-mev-protection", - ], - ) - assert "โœ… Finalized" in transfer_partial_seed_stake_result.stdout, ( - transfer_partial_seed_stake_result.stderr - ) - print("โœ… Transfer with partial stake seed stake finalized") - - # Verify stake was added - alice_stake_before_transfer_partial = exec_command_alice( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - alice_stake_list_before_transfer_partial = json.loads( - alice_stake_before_transfer_partial.stdout - ) - alice_stakes_before_transfer_partial = find_stake_entries( - alice_stake_list_before_transfer_partial, - netuid=0, - hotkey_ss58=wallet_alice.hotkey.ss58_address, - ) - assert len(alice_stakes_before_transfer_partial) > 0 - assert any( - stake["stake_value"] >= 20 for stake in alice_stakes_before_transfer_partial - ) - - # Transfer stake with slippage protection and partial stake (--safe --tolerance --partial) - transfer_partial_amount = 20 - transfer_partial_result = exec_command_alice( - command="stake", - sub_command="transfer", - extra_args=[ - "--origin-netuid", - "0", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--dest-netuid", - "0", - "--dest", - wallet_bob.coldkeypub.ss58_address, - "--amount", - str(transfer_partial_amount), - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--safe", - "--tolerance", - "0.1", # 10% tolerance - "--partial", # Allow partial stake if rates change - ], - ) - assert "โœ… Sent" in transfer_partial_result.stdout, transfer_partial_result.stderr - - # Verify stake was transferred (may be partial) - bob_stake_after_transfer_partial = exec_command_bob( - command="stake", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_bob, - "--wallet-name", - wallet_bob.name, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", - "--json-output", - ], - ) - bob_stake_list_after_transfer_partial = json.loads( - bob_stake_after_transfer_partial.stdout - ) - bob_stakes_after_transfer_partial = find_stake_entries( - bob_stake_list_after_transfer_partial, - netuid=0, - ) - # With partial stake, we expect at least some stake to be transferred - assert len(bob_stakes_after_transfer_partial) > 0 - print( - "โœ… TEST 6: Transfer with slippage protection and partial stake completed successfully" + "โœ… TEST 3: Swap with slippage protection and partial stake completed successfully" ) - print("โœ… Passed all slippage protection tests for swap and transfer commands") + print("โœ… Passed all slippage protection tests for swap command")