Skip to content

Commit

Permalink
Merge pull request #1832 from opentensor/warmfix/add-thresholds-for-u…
Browse files Browse the repository at this point in the history
…nstaking

Add in check for minimum stake for unstaking
  • Loading branch information
thewhaleking committed May 1, 2024
2 parents 5412af0 + 5ca6534 commit 752c1ad
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 15 deletions.
31 changes: 16 additions & 15 deletions bittensor/commands/unstake.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,23 @@ def check_config(cls, config: "bittensor.config"):
hotkeys = str(config.hotkeys).replace("[", "").replace("]", "")
else:
hotkeys = str(config.wallet.hotkey)
if not Confirm.ask(
"Unstake all Tao from: [bold]'{}'[/bold]?".format(hotkeys)
):
amount = Prompt.ask("Enter Tao amount to unstake")
config.unstake_all = False
try:
config.amount = float(amount)
except ValueError:
console.print(
":cross_mark:[red] Invalid Tao amount[/red] [bold white]{}[/bold white]".format(
amount
)
)
sys.exit()
else:
if config.no_prompt:
config.unstake_all = True
else:
# I really don't like this logic flow. It can be a bit confusing to read for something
# as serious as unstaking all.
if Confirm.ask(f"Unstake all Tao from: [bold]'{hotkeys}'[/bold]?"):
config.unstake_all = True
else:
config.unstake_all = False
amount = Prompt.ask("Enter Tao amount to unstake")
try:
config.amount = float(amount)
except ValueError:
console.print(
f":cross_mark:[red] Invalid Tao amount[/red] [bold white]{amount}[/bold white]"
)
sys.exit()

@staticmethod
def add_args(command_parser):
Expand Down
36 changes: 36 additions & 0 deletions bittensor/extrinsics/unstaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ def __do_remove_stake_single(
return success


def check_threshold_amount(
subtensor: "bittensor.subtensor", unstaking_balance: Balance
) -> bool:
"""
Checks if the unstaking amount is above the threshold or 0
Args:
unstaking_balance (Balance):
the balance to check for threshold limits.
Returns:
success (bool):
``true`` if the unstaking is above the threshold or 0, or ``false`` if the
unstaking is below the threshold, but not 0.
"""
min_req_stake: Balance = subtensor.get_minimum_required_stake()

if min_req_stake > unstaking_balance > 0:
bittensor.__console__.print(
f":cross_mark: [red]Unstaking balance of {unstaking_balance} less than minimum of {min_req_stake} TAO[/red]"
)
return False
else:
return True


def unstake_extrinsic(
subtensor: "bittensor.subtensor",
wallet: "bittensor.wallet",
Expand Down Expand Up @@ -134,6 +160,11 @@ def unstake_extrinsic(
)
return False

if not check_threshold_amount(
subtensor=subtensor, unstaking_balance=unstaking_balance
):
return False

# Ask before moving on.
if prompt:
if not Confirm.ask(
Expand Down Expand Up @@ -305,6 +336,11 @@ def unstake_multiple_extrinsic(
)
continue

if not check_threshold_amount(
subtensor=subtensor, unstaking_balance=unstaking_balance
):
return False

# Ask before moving on.
if prompt:
if not Confirm.ask(
Expand Down
12 changes: 12 additions & 0 deletions bittensor/mock/subtensor_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,18 @@ def _do_unstake(

return True

@staticmethod
def min_required_stake():
"""
As the minimum required stake may change, this method allows us to dynamically
update the amount in the mock without updating the tests
"""
# valid minimum threshold as of 2024/05/01
return 100_000_000 # RAO

def get_minimum_required_stake(self):
return Balance.from_rao(self.min_required_stake())

def get_delegate_by_hotkey(
self, hotkey_ss58: str, block: Optional[int] = None
) -> Optional["DelegateInfo"]:
Expand Down
12 changes: 12 additions & 0 deletions bittensor/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,18 @@ def get_stake_info_for_coldkeys(

return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore

def get_minimum_required_stake(
self,
):
@retry(delay=2, tries=3, backoff=2, max_delay=4, logger=logger)
def make_substrate_call_with_retry():
return self.substrate.query(
module="SubtensorModule", storage_function="NominatorMinRequiredStake"
)

result = make_substrate_call_with_retry()
return Balance.from_rao(result.decode())

########################################
#### Neuron information per subnet ####
########################################
Expand Down
141 changes: 141 additions & 0 deletions tests/integration_tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,147 @@ def mock_get_wallet(*args, **kwargs):
stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4
)

def test_unstake_with_thresholds(self, _):
config = self.config
config.command = "stake"
config.subcommand = "remove"
config.no_prompt = True
# as the minimum required stake may change, this method allows us to dynamically
# update the amount in the mock without updating the tests
config.amount = Balance.from_rao(_subtensor_mock.min_required_stake() - 1)
config.wallet.name = "fake_wallet"
config.hotkeys = ["hk0", "hk1", "hk2"]
config.all_hotkeys = False
# Notice no max_stake specified

mock_stakes: Dict[str, Balance] = {
"hk0": Balance.from_float(10.0),
"hk1": Balance.from_float(11.1),
"hk2": Balance.from_float(12.2),
}

mock_coldkey_kp = _get_mock_keypair(0, self.id())

mock_wallets = [
SimpleNamespace(
name=config.wallet.name,
coldkey=mock_coldkey_kp,
coldkeypub=mock_coldkey_kp,
hotkey_str=hk,
hotkey=_get_mock_keypair(idx + 100, self.id()),
)
for idx, hk in enumerate(config.hotkeys)
]

# Register mock wallets and give them stakes

for wallet in mock_wallets:
_ = _subtensor_mock.force_register_neuron(
netuid=1,
hotkey=wallet.hotkey.ss58_address,
coldkey=wallet.coldkey.ss58_address,
stake=mock_stakes[wallet.hotkey_str].rao,
)

cli = bittensor.cli(config)

def mock_get_wallet(*args, **kwargs):
if kwargs.get("hotkey"):
for wallet in mock_wallets:
if wallet.hotkey_str == kwargs.get("hotkey"):
return wallet
else:
return mock_wallets[0]

with patch("bittensor.wallet") as mock_create_wallet:
mock_create_wallet.side_effect = mock_get_wallet

# Check stakes before unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao)

cli.run()

# Check stakes after unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
# because the amount is less than the threshold, none of these should unstake
self.assertEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao)

def test_unstake_all(self, _):
config = self.config
config.command = "stake"
config.subcommand = "remove"
config.no_prompt = True
config.amount = 0.0 # 0 implies full unstake
config.wallet.name = "fake_wallet"
config.hotkeys = ["hk0"]
config.all_hotkeys = False

mock_stakes: Dict[str, Balance] = {"hk0": Balance.from_float(10.0)}

mock_coldkey_kp = _get_mock_keypair(0, self.id())

mock_wallets = [
SimpleNamespace(
name=config.wallet.name,
coldkey=mock_coldkey_kp,
coldkeypub=mock_coldkey_kp,
hotkey_str=hk,
hotkey=_get_mock_keypair(idx + 100, self.id()),
)
for idx, hk in enumerate(config.hotkeys)
]

# Register mock wallets and give them stakes

for wallet in mock_wallets:
_ = _subtensor_mock.force_register_neuron(
netuid=1,
hotkey=wallet.hotkey.ss58_address,
coldkey=wallet.coldkey.ss58_address,
stake=mock_stakes[wallet.hotkey_str].rao,
)

cli = bittensor.cli(config)

def mock_get_wallet(*args, **kwargs):
if kwargs.get("hotkey"):
for wallet in mock_wallets:
if wallet.hotkey_str == kwargs.get("hotkey"):
return wallet
else:
return mock_wallets[0]

with patch("bittensor.wallet") as mock_create_wallet:
mock_create_wallet.side_effect = mock_get_wallet

# Check stakes before unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao)

cli.run()

# Check stakes after unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
# because the amount is less than the threshold, none of these should unstake
self.assertEqual(stake.tao, Balance.from_tao(0))

def test_stake_with_specific_hotkeys(self, _):
config = self.config
config.command = "stake"
Expand Down

0 comments on commit 752c1ad

Please sign in to comment.