mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2384 from vzotova/remove-sub-stakes
Method to remove unused sub-stakes in StakingEscrowpull/2409/head
commit
e531035ae8
|
@ -83,6 +83,8 @@ All staking-related operations done by Staker are performed through the ``nucyph
|
||||||
+----------------------+-------------------------------------------------------------------------------+
|
+----------------------+-------------------------------------------------------------------------------+
|
||||||
| ``merge`` | Merge two stakes into one |
|
| ``merge`` | Merge two stakes into one |
|
||||||
+----------------------+-------------------------------------------------------------------------------+
|
+----------------------+-------------------------------------------------------------------------------+
|
||||||
|
| ``remove-unused`` | Remove unused stake |
|
||||||
|
+----------------------+-------------------------------------------------------------------------------+
|
||||||
|
|
||||||
**Stake Command Options**
|
**Stake Command Options**
|
||||||
|
|
||||||
|
@ -488,6 +490,18 @@ This can help to decrease gas consumption in some operations. To merge two stake
|
||||||
(nucypher)$ nucypher stake merge --hw-wallet
|
(nucypher)$ nucypher stake merge --hw-wallet
|
||||||
|
|
||||||
|
|
||||||
|
Remove unused sub-stake
|
||||||
|
***********************
|
||||||
|
|
||||||
|
Merging or editing sub-stakes can lead to 'unused', inactive sub-stakes remaining on-chain.
|
||||||
|
These unused sub-stakes add unnecessary gas costs to daily operations.
|
||||||
|
To remove unused sub-stake:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
(nucypher)$ nucypher stake remove-unused --hw-wallet
|
||||||
|
|
||||||
|
|
||||||
Collect rewards earned by the staker
|
Collect rewards earned by the staker
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1275,6 +1275,32 @@ class Staker(NucypherTokenActor):
|
||||||
receipt = self._set_snapshots(value=False)
|
receipt = self._set_snapshots(value=False)
|
||||||
return receipt
|
return receipt
|
||||||
|
|
||||||
|
@only_me
|
||||||
|
@save_receipt
|
||||||
|
def remove_unused_stake(self, stake: Stake) -> TxReceipt:
|
||||||
|
self._ensure_stake_exists(stake)
|
||||||
|
|
||||||
|
# Read on-chain stake and validate
|
||||||
|
stake.sync()
|
||||||
|
if not stake.status().is_child(Stake.Status.INACTIVE):
|
||||||
|
raise ValueError(f"Stake with index {stake.index} is still active")
|
||||||
|
|
||||||
|
receipt = self._remove_unused_stake(stake_index=stake.index)
|
||||||
|
|
||||||
|
# Update staking cache element
|
||||||
|
self.refresh_stakes()
|
||||||
|
return receipt
|
||||||
|
|
||||||
|
@only_me
|
||||||
|
@save_receipt
|
||||||
|
def _remove_unused_stake(self, stake_index: int) -> TxReceipt:
|
||||||
|
# TODO #1497 #1358
|
||||||
|
# if self.is_contract:
|
||||||
|
# else:
|
||||||
|
receipt = self.staking_agent.remove_unused_stake(staker_address=self.checksum_address,
|
||||||
|
stake_index=stake_index)
|
||||||
|
return receipt
|
||||||
|
|
||||||
def non_withdrawable_stake(self) -> NU:
|
def non_withdrawable_stake(self) -> NU:
|
||||||
staked_amount: NuNits = self.staking_agent.non_withdrawable_stake(staker_address=self.checksum_address)
|
staked_amount: NuNits = self.staking_agent.non_withdrawable_stake(staker_address=self.checksum_address)
|
||||||
return NU.from_nunits(staked_amount)
|
return NU.from_nunits(staked_amount)
|
||||||
|
|
|
@ -711,6 +711,13 @@ class StakingEscrowAgent(EthereumContractAgent):
|
||||||
# TODO: Handle SnapshotSet event (see #1193)
|
# TODO: Handle SnapshotSet event (see #1193)
|
||||||
return receipt
|
return receipt
|
||||||
|
|
||||||
|
@contract_api(TRANSACTION)
|
||||||
|
def remove_unused_stake(self, staker_address: ChecksumAddress, stake_index: int) -> TxReceipt:
|
||||||
|
contract_function: ContractFunction = self.contract.functions.removeUnusedSubStake(stake_index)
|
||||||
|
receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||||
|
sender_address=staker_address)
|
||||||
|
return receipt
|
||||||
|
|
||||||
@contract_api(CONTRACT_CALL)
|
@contract_api(CONTRACT_CALL)
|
||||||
def staking_parameters(self) -> StakingEscrowParameters:
|
def staking_parameters(self) -> StakingEscrowParameters:
|
||||||
parameter_signatures = (
|
parameter_signatures = (
|
||||||
|
|
|
@ -45,7 +45,7 @@ interface WorkLockInterface {
|
||||||
/**
|
/**
|
||||||
* @notice Contract holds and locks stakers tokens.
|
* @notice Contract holds and locks stakers tokens.
|
||||||
* Each staker that locks their tokens will receive some compensation
|
* Each staker that locks their tokens will receive some compensation
|
||||||
* @dev |v5.4.4|
|
* @dev |v5.5.1|
|
||||||
*/
|
*/
|
||||||
contract StakingEscrow is Issuer, IERC900History {
|
contract StakingEscrow is Issuer, IERC900History {
|
||||||
|
|
||||||
|
@ -1048,6 +1048,30 @@ contract StakingEscrow is Issuer, IERC900History {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Remove unused sub-stake to decrease gas cost for several methods
|
||||||
|
*/
|
||||||
|
function removeUnusedSubStake(uint16 _index) external onlyStaker {
|
||||||
|
StakerInfo storage info = stakerInfo[msg.sender];
|
||||||
|
|
||||||
|
uint256 lastIndex = info.subStakes.length - 1;
|
||||||
|
SubStakeInfo storage subStake = info.subStakes[_index];
|
||||||
|
require(subStake.lastPeriod != 0 &&
|
||||||
|
(info.currentCommittedPeriod == 0 ||
|
||||||
|
subStake.lastPeriod < info.currentCommittedPeriod) &&
|
||||||
|
(info.nextCommittedPeriod == 0 ||
|
||||||
|
subStake.lastPeriod < info.nextCommittedPeriod));
|
||||||
|
|
||||||
|
if (_index != lastIndex) {
|
||||||
|
SubStakeInfo storage lastSubStake = info.subStakes[lastIndex];
|
||||||
|
subStake.firstPeriod = lastSubStake.firstPeriod;
|
||||||
|
subStake.lastPeriod = lastSubStake.lastPeriod;
|
||||||
|
subStake.periods = lastSubStake.periods;
|
||||||
|
subStake.lockedValue = lastSubStake.lockedValue;
|
||||||
|
}
|
||||||
|
info.subStakes.pop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Withdraw available amount of tokens to staker
|
* @notice Withdraw available amount of tokens to staker
|
||||||
* @param _value Amount of tokens to withdraw
|
* @param _value Amount of tokens to withdraw
|
||||||
|
|
|
@ -73,7 +73,7 @@ from nucypher.cli.literature import (
|
||||||
INSUFFICIENT_BALANCE_TO_CREATE, PROMPT_STAKE_CREATE_VALUE, PROMPT_STAKE_CREATE_LOCK_PERIODS,
|
INSUFFICIENT_BALANCE_TO_CREATE, PROMPT_STAKE_CREATE_VALUE, PROMPT_STAKE_CREATE_LOCK_PERIODS,
|
||||||
ONLY_DISPLAYING_MERGEABLE_STAKES_NOTE, CONFIRM_MERGE, SUCCESSFUL_STAKES_MERGE, SUCCESSFUL_ENABLE_SNAPSHOTS,
|
ONLY_DISPLAYING_MERGEABLE_STAKES_NOTE, CONFIRM_MERGE, SUCCESSFUL_STAKES_MERGE, SUCCESSFUL_ENABLE_SNAPSHOTS,
|
||||||
SUCCESSFUL_DISABLE_SNAPSHOTS, CONFIRM_ENABLE_SNAPSHOTS,
|
SUCCESSFUL_DISABLE_SNAPSHOTS, CONFIRM_ENABLE_SNAPSHOTS,
|
||||||
CONFIRM_STAKE_USE_UNLOCKED)
|
CONFIRM_STAKE_USE_UNLOCKED, CONFIRM_REMOVE_SUBSTAKE, SUCCESSFUL_STAKE_REMOVAL)
|
||||||
from nucypher.cli.options import (
|
from nucypher.cli.options import (
|
||||||
group_options,
|
group_options,
|
||||||
option_config_file,
|
option_config_file,
|
||||||
|
@ -1068,6 +1068,61 @@ def merge(general_config: GroupGeneralConfig,
|
||||||
paint_stakes(emitter=emitter, staker=STAKEHOLDER)
|
paint_stakes(emitter=emitter, staker=STAKEHOLDER)
|
||||||
|
|
||||||
|
|
||||||
|
@stake.command()
|
||||||
|
@group_transacting_staker_options
|
||||||
|
@option_config_file
|
||||||
|
@option_force
|
||||||
|
@group_general_config
|
||||||
|
@click.option('--index', help="Index of unused stake to remove", type=click.INT)
|
||||||
|
def remove_unused(general_config: GroupGeneralConfig,
|
||||||
|
transacting_staker_options: TransactingStakerOptions,
|
||||||
|
config_file, force, index):
|
||||||
|
"""Remove unused stake."""
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
emitter = setup_emitter(general_config)
|
||||||
|
STAKEHOLDER = transacting_staker_options.create_character(emitter, config_file)
|
||||||
|
action_period = STAKEHOLDER.staking_agent.get_current_period()
|
||||||
|
blockchain = transacting_staker_options.get_blockchain()
|
||||||
|
|
||||||
|
client_account, staking_address = select_client_account_for_staking(
|
||||||
|
emitter=emitter,
|
||||||
|
stakeholder=STAKEHOLDER,
|
||||||
|
staking_address=transacting_staker_options.staker_options.staking_address,
|
||||||
|
individual_allocation=STAKEHOLDER.individual_allocation,
|
||||||
|
force=force)
|
||||||
|
|
||||||
|
# Handle stake update and selection
|
||||||
|
if index is not None: # 0 is valid.
|
||||||
|
current_stake = STAKEHOLDER.stakes[index]
|
||||||
|
else:
|
||||||
|
current_stake = select_stake(staker=STAKEHOLDER, emitter=emitter, stakes_status=Stake.Status.INACTIVE)
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
click.confirm(CONFIRM_REMOVE_SUBSTAKE.format(stake_index=current_stake.index), abort=True)
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
password = get_password(stakeholder=STAKEHOLDER,
|
||||||
|
blockchain=blockchain,
|
||||||
|
client_account=client_account,
|
||||||
|
hw_wallet=transacting_staker_options.hw_wallet)
|
||||||
|
STAKEHOLDER.assimilate(password=password)
|
||||||
|
|
||||||
|
# Non-interactive: Consistency check to prevent the above agreement from going stale.
|
||||||
|
last_second_current_period = STAKEHOLDER.staking_agent.get_current_period()
|
||||||
|
if action_period != last_second_current_period:
|
||||||
|
emitter.echo(PERIOD_ADVANCED_WARNING, color='red')
|
||||||
|
raise click.Abort
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
receipt = STAKEHOLDER.remove_unused_stake(stake=current_stake)
|
||||||
|
|
||||||
|
# Report
|
||||||
|
emitter.echo(SUCCESSFUL_STAKE_REMOVAL, color='green', verbosity=1)
|
||||||
|
paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name)
|
||||||
|
paint_stakes(emitter=emitter, staker=STAKEHOLDER)
|
||||||
|
|
||||||
|
|
||||||
@stake.command('collect-reward')
|
@stake.command('collect-reward')
|
||||||
@group_transacting_staker_options
|
@group_transacting_staker_options
|
||||||
@option_config_file
|
@option_config_file
|
||||||
|
|
|
@ -271,6 +271,10 @@ CONFIRM_MERGE = "Publish merging of {stake_index_1} and {stake_index_2} stakes?"
|
||||||
|
|
||||||
SUCCESSFUL_STAKES_MERGE = 'Successfully Merged Stakes'
|
SUCCESSFUL_STAKES_MERGE = 'Successfully Merged Stakes'
|
||||||
|
|
||||||
|
CONFIRM_REMOVE_SUBSTAKE = "Publish removal of {stake_index} stake?"
|
||||||
|
|
||||||
|
SUCCESSFUL_STAKE_REMOVAL = 'Successfully Removed Stake'
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rewards
|
# Rewards
|
||||||
#
|
#
|
||||||
|
|
|
@ -199,7 +199,7 @@ def test_staker_increases_stake(staker, token_economics):
|
||||||
assert stake.value == origin_stake.value + balance
|
assert stake.value == origin_stake.value + balance
|
||||||
|
|
||||||
|
|
||||||
def test_staker_merges_stakes(agency, staker, token_economics):
|
def test_staker_merges_stakes(agency, staker):
|
||||||
stake_index_1 = 0
|
stake_index_1 = 0
|
||||||
stake_index_2 = 3
|
stake_index_2 = 3
|
||||||
origin_stake_1 = staker.stakes[stake_index_1]
|
origin_stake_1 = staker.stakes[stake_index_1]
|
||||||
|
@ -224,6 +224,19 @@ def test_staker_merges_stakes(agency, staker, token_economics):
|
||||||
staker.merge_stakes(stake_1=staker.stakes[1], stake_2=stake)
|
staker.merge_stakes(stake_1=staker.stakes[1], stake_2=stake)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unused_stake(agency, staker):
|
||||||
|
stake_index = 3
|
||||||
|
staker.refresh_stakes()
|
||||||
|
original_stakes = list(staker.stakes)
|
||||||
|
unused_stake = original_stakes[stake_index]
|
||||||
|
assert unused_stake.final_locked_period == 1
|
||||||
|
|
||||||
|
staker.remove_unused_stake(stake=unused_stake)
|
||||||
|
|
||||||
|
stakes = staker.stakes
|
||||||
|
assert stakes == original_stakes[:-1]
|
||||||
|
|
||||||
|
|
||||||
def test_staker_manages_restaking(testerchain, test_registry, staker):
|
def test_staker_manages_restaking(testerchain, test_registry, staker):
|
||||||
# Enable Restaking
|
# Enable Restaking
|
||||||
receipt = staker.enable_restaking()
|
receipt = staker.enable_restaking()
|
||||||
|
|
|
@ -138,7 +138,6 @@ def test_get_swarm(agency, blockchain_ursulas):
|
||||||
assert is_address(staker_addr)
|
assert is_address(staker_addr)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("blockchain_ursulas")
|
@pytest.mark.usefixtures("blockchain_ursulas")
|
||||||
def test_sample_stakers(agency):
|
def test_sample_stakers(agency):
|
||||||
_token_agent, staking_agent, _policy_agent = agency
|
_token_agent, staking_agent, _policy_agent = agency
|
||||||
|
@ -264,16 +263,6 @@ def test_deposit_and_increase(agency, testerchain, test_registry, token_economic
|
||||||
assert staking_agent.get_locked_tokens(staker_account, 1) == locked_tokens + amount
|
assert staking_agent.get_locked_tokens(staker_account, 1) == locked_tokens + amount
|
||||||
|
|
||||||
|
|
||||||
def test_disable_restaking(agency, testerchain, test_registry):
|
|
||||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
|
||||||
staker_account, worker_account, *other = testerchain.unassigned_accounts
|
|
||||||
|
|
||||||
assert staking_agent.is_restaking(staker_account)
|
|
||||||
receipt = staking_agent.set_restaking(staker_account, value=False)
|
|
||||||
assert receipt['status'] == 1
|
|
||||||
assert not staking_agent.is_restaking(staker_account)
|
|
||||||
|
|
||||||
|
|
||||||
def test_lock_restaking(agency, testerchain, test_registry):
|
def test_lock_restaking(agency, testerchain, test_registry):
|
||||||
staker_account, worker_account, *other = testerchain.unassigned_accounts
|
staker_account, worker_account, *other = testerchain.unassigned_accounts
|
||||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||||
|
@ -438,6 +427,33 @@ def test_merge(agency, testerchain, test_registry, token_economics):
|
||||||
assert staking_agent.get_locked_tokens(staker_account, 0) == current_locked_tokens
|
assert staking_agent.get_locked_tokens(staker_account, 0) == current_locked_tokens
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unused_stake(agency, testerchain, test_registry):
|
||||||
|
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||||
|
staker_account = testerchain.unassigned_accounts[0]
|
||||||
|
|
||||||
|
testerchain.time_travel(periods=1)
|
||||||
|
staking_agent.mint(staker_address=staker_account)
|
||||||
|
current_period = staking_agent.get_current_period()
|
||||||
|
original_stakes = list(staking_agent.get_all_stakes(staker_address=staker_account))
|
||||||
|
assert original_stakes[2].last_period == current_period - 1
|
||||||
|
|
||||||
|
current_locked_tokens = staking_agent.get_locked_tokens(staker_account, 0)
|
||||||
|
next_locked_tokens = staking_agent.get_locked_tokens(staker_account, 1)
|
||||||
|
|
||||||
|
receipt = staking_agent.remove_unused_stake(staker_address=staker_account, stake_index=2)
|
||||||
|
assert receipt['status'] == 1
|
||||||
|
|
||||||
|
# Ensure stake was extended by one period.
|
||||||
|
stakes = list(staking_agent.get_all_stakes(staker_address=staker_account))
|
||||||
|
assert len(stakes) == len(original_stakes) - 1
|
||||||
|
assert stakes[0] == original_stakes[0]
|
||||||
|
assert stakes[1] == original_stakes[1]
|
||||||
|
assert stakes[2] == original_stakes[4]
|
||||||
|
assert stakes[3] == original_stakes[3]
|
||||||
|
assert staking_agent.get_locked_tokens(staker_account, 1) == next_locked_tokens
|
||||||
|
assert staking_agent.get_locked_tokens(staker_account, 0) == current_locked_tokens
|
||||||
|
|
||||||
|
|
||||||
def test_batch_deposit(testerchain,
|
def test_batch_deposit(testerchain,
|
||||||
agency,
|
agency,
|
||||||
token_economics,
|
token_economics,
|
||||||
|
@ -448,7 +464,6 @@ def test_batch_deposit(testerchain,
|
||||||
|
|
||||||
amount = token_economics.minimum_allowed_locked
|
amount = token_economics.minimum_allowed_locked
|
||||||
lock_periods = token_economics.minimum_locked_periods
|
lock_periods = token_economics.minimum_locked_periods
|
||||||
current_period = staking_agent.get_current_period()
|
|
||||||
|
|
||||||
stakers = [get_random_checksum_address() for _ in range(4)]
|
stakers = [get_random_checksum_address() for _ in range(4)]
|
||||||
|
|
||||||
|
|
|
@ -286,6 +286,33 @@ def test_merge_stakes(click_runner,
|
||||||
assert stakes[selection_2].last_period == 1
|
assert stakes[selection_2].last_period == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unused(click_runner,
|
||||||
|
stakeholder_configuration_file_location,
|
||||||
|
token_economics,
|
||||||
|
testerchain,
|
||||||
|
agency_local_registry,
|
||||||
|
manual_staker,
|
||||||
|
stake_value):
|
||||||
|
|
||||||
|
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=agency_local_registry)
|
||||||
|
original_stakes = list(staking_agent.get_all_stakes(staker_address=manual_staker))
|
||||||
|
|
||||||
|
selection = 2
|
||||||
|
assert original_stakes[selection].last_period == 1
|
||||||
|
|
||||||
|
stake_args = ('stake', 'remove-unused',
|
||||||
|
'--config-file', stakeholder_configuration_file_location,
|
||||||
|
'--staking-address', manual_staker,
|
||||||
|
'--index', selection,
|
||||||
|
'--force')
|
||||||
|
user_input = f'0\n' + f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + YES_ENTER
|
||||||
|
result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
stakes = list(staking_agent.get_all_stakes(staker_address=manual_staker))
|
||||||
|
assert len(stakes) == len(original_stakes) - 1
|
||||||
|
|
||||||
|
|
||||||
def test_stake_bond_worker(click_runner,
|
def test_stake_bond_worker(click_runner,
|
||||||
testerchain,
|
testerchain,
|
||||||
agency_local_registry,
|
agency_local_registry,
|
||||||
|
|
|
@ -1467,3 +1467,130 @@ def test_snapshots(testerchain, token, escrow_contract):
|
||||||
assert last_balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now).call()
|
assert last_balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now).call()
|
||||||
assert balance_staker1 == escrow.functions.totalStakedForAt(staker1, now - 1).call()
|
assert balance_staker1 == escrow.functions.totalStakedForAt(staker1, now - 1).call()
|
||||||
assert balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now - 1).call()
|
assert balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now - 1).call()
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unused_sub_stakes(testerchain, token, escrow_contract, token_economics):
|
||||||
|
escrow = escrow_contract(token_economics.maximum_allowed_locked, disable_reward=True)
|
||||||
|
creator = testerchain.client.accounts[0]
|
||||||
|
staker = testerchain.client.accounts[1]
|
||||||
|
|
||||||
|
# GIVe staker some tokens
|
||||||
|
stake = 10 * token_economics.minimum_allowed_locked
|
||||||
|
tx = token.functions.transfer(staker, stake).transact({'from': creator})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = token.functions.approve(escrow.address, stake).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
|
||||||
|
# Prepare sub-stakes
|
||||||
|
initial_period = escrow.functions.getCurrentPeriod().call()
|
||||||
|
sub_stake = token_economics.minimum_allowed_locked
|
||||||
|
duration = token_economics.minimum_locked_periods
|
||||||
|
for i in range(3):
|
||||||
|
tx = escrow.functions.deposit(staker, sub_stake, duration).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
for i in range(2):
|
||||||
|
tx = escrow.functions.deposit(staker, sub_stake, duration + 1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
testerchain.time_travel(hours=1)
|
||||||
|
assert escrow.functions.getLockedTokens(staker, 1).call() == 5 * sub_stake
|
||||||
|
|
||||||
|
tx = escrow.functions.mergeStake(1, 0).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.mergeStake(1, 2).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.mergeStake(3, 4).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
assert escrow.functions.getLockedTokens(staker, 1).call() == 5 * sub_stake
|
||||||
|
assert escrow.functions.getSubStakesLength(staker).call() == 5
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 1, 1, 0, sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, 0, duration, 3 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 2).call() == [initial_period + 1, 1, 0, sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 3).call() == [initial_period + 1, initial_period + 1, 0, sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 4).call() == [initial_period + 2, 0, duration + 1, 2 * sub_stake]
|
||||||
|
|
||||||
|
# Can't remove active sub-stakes
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(4).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(5).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
|
||||||
|
# Remove first unused sub-stake
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(0).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
assert escrow.functions.getLockedTokens(staker, 1).call() == 5 * sub_stake
|
||||||
|
assert escrow.functions.getSubStakesLength(staker).call() == 4
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, 0, duration + 1, 2 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, 0, duration, 3 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 2).call() == [initial_period + 1, 1, 0, sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 3).call() == [initial_period + 1, initial_period + 1, 0, sub_stake]
|
||||||
|
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(0).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
|
||||||
|
# Remove unused sub-stake in the middle
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(2).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
assert escrow.functions.getLockedTokens(staker, 1).call() == 5 * sub_stake
|
||||||
|
assert escrow.functions.getSubStakesLength(staker).call() == 3
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, 0, duration + 1, 2 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, 0, duration, 3 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 2).call() == [initial_period + 1, initial_period + 1, 0, sub_stake]
|
||||||
|
|
||||||
|
# Remove last sub-stake
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(2).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
assert escrow.functions.getLockedTokens(staker, 1).call() == 5 * sub_stake
|
||||||
|
assert escrow.functions.getSubStakesLength(staker).call() == 2
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, 0, duration + 1, 2 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, 0, duration, 3 * sub_stake]
|
||||||
|
|
||||||
|
# Prepare other case: when sub-stake is unlocked but still active
|
||||||
|
tx = escrow.functions.initialize(0, creator).transact({'from': creator})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.setWindDown(True).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.bondWorker(staker).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
for i in range(duration):
|
||||||
|
tx = escrow.functions.commitToNextPeriod().transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
testerchain.time_travel(hours=1)
|
||||||
|
|
||||||
|
current_period = escrow.functions.getCurrentPeriod().call()
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, 0, 1, 2 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, current_period, 0, 3 * sub_stake]
|
||||||
|
|
||||||
|
# Can't remove active sub-stakes
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.mint().transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.commitToNextPeriod().transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
|
||||||
|
testerchain.time_travel(hours=1)
|
||||||
|
current_period = escrow.functions.getCurrentPeriod().call()
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, current_period, 0, 2 * sub_stake]
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 1).call() == [initial_period + 1, current_period - 1, 0, 3 * sub_stake]
|
||||||
|
with pytest.raises((TransactionFailed, ValueError)):
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.mint().transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
tx = escrow.functions.removeUnusedSubStake(1).transact({'from': staker})
|
||||||
|
testerchain.wait_for_receipt(tx)
|
||||||
|
assert escrow.functions.getSubStakesLength(staker).call() == 1
|
||||||
|
assert escrow.functions.getSubStakeInfo(staker, 0).call() == [initial_period + 2, current_period, 0, 2 * sub_stake]
|
||||||
|
|
Loading…
Reference in New Issue