diff --git a/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol b/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol index f2a8d18c9..63f9e198d 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol @@ -34,7 +34,7 @@ contract WorkLockInterface { /** * @notice Contract holds and locks stakers tokens. * Each staker that locks their tokens will receive some compensation -* @dev |v2.2.4| +* @dev |v2.3.1| */ contract StakingEscrow is Issuer { using AdditionalMath for uint256; @@ -542,6 +542,49 @@ contract StakingEscrow is Issuer { } } + /** + * @notice Batch deposit. Allowed only initial deposit for each staker + * @param _stakers Stakers + * @param _values Amount of tokens to deposit for each staker + * @param _periods Amount of periods during which tokens will be locked for each staker + */ + function batchDeposit( + address[] calldata _stakers, + uint256[] calldata _values, + uint16[] calldata _periods + ) + external + { + require(_stakers.length != 0 && + _stakers.length == _values.length && + _stakers.length == _periods.length); + uint16 previousPeriod = getCurrentPeriod() - 1; + uint16 nextPeriod = previousPeriod + 2; + uint256 sumValue = 0; + + for (uint256 i = 0; i < _stakers.length; i++) { + address staker = _stakers[i]; + uint256 value = _values[i]; + uint16 periods = _periods[i]; + StakerInfo storage info = stakerInfo[staker]; + require(info.subStakes.length == 0 && + value >= minAllowableLockedTokens && + value <= maxAllowableLockedTokens && + periods >= minLockedPeriods); + require(workerToStaker[staker] == address(0) || workerToStaker[staker] == info.worker, + "A staker can't be a worker for another staker"); + stakers.push(staker); + policyManager.register(staker, previousPeriod); + info.value = value; + info.subStakes.push(SubStakeInfo(nextPeriod, 0, periods, value)); + sumValue = sumValue.add(value); + emit Deposited(staker, value, periods); + emit Locked(staker, value, nextPeriod, periods); + } + + token.safeTransferFrom(msg.sender, address(this), sumValue); + } + /** * @notice Implementation of the receiveApproval(address,uint256,address,bytes) method * (see NuCypherToken contract). Deposit all tokens that were approved to transfer diff --git a/tests/blockchain/eth/contracts/integration/test_intercontract_integration.py b/tests/blockchain/eth/contracts/integration/test_intercontract_integration.py index 133e26e2d..02044514c 100644 --- a/tests/blockchain/eth/contracts/integration/test_intercontract_integration.py +++ b/tests/blockchain/eth/contracts/integration/test_intercontract_integration.py @@ -303,10 +303,33 @@ def test_all(testerchain, tx = token.functions.approve(escrow.address, 10000).transact({'from': staker1}) testerchain.wait_for_receipt(tx) - # Staker can't deposit tokens before Escrow initialization + # Check that nothing is locked + assert 0 == escrow.functions.getLockedTokens(staker1, 0).call() + assert 0 == escrow.functions.getLockedTokens(staker2, 0).call() + assert 0 == escrow.functions.getLockedTokens(staker3, 0).call() + assert 0 == escrow.functions.getLockedTokens(staker4, 0).call() + + # Deposit tokens for 1 staker + staker1_tokens = token_economics.minimum_allowed_locked + duration = token_economics.minimum_locked_periods + tx = token.functions.approve(escrow.address, 2 * staker1_tokens).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + tx = escrow.functions.batchDeposit([staker1], [staker1_tokens], [duration]).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + escrow_supply = token_economics.minimum_allowed_locked + assert token.functions.balanceOf(escrow.address).call() == escrow_supply + assert escrow.functions.getAllTokens(staker1).call() == staker1_tokens + assert escrow.functions.getLockedTokens(staker1, 0).call() == 0 + assert escrow.functions.getLockedTokens(staker1, 1).call() == staker1_tokens + assert escrow.functions.getLockedTokens(staker1, duration).call() == staker1_tokens + assert escrow.functions.getLockedTokens(staker1, duration + 1).call() == 0 + + # Can't deposit tokens again for the same staker with pytest.raises((TransactionFailed, ValueError)): - tx = escrow.functions.deposit(1, 1).transact({'from': staker1}) + tx = escrow.functions.batchDeposit([staker1], [staker1_tokens], [duration]).transact({'from': creator}) testerchain.wait_for_receipt(tx) + tx = token.functions.approve(escrow.address, 0).transact({'from': creator}) + testerchain.wait_for_receipt(tx) # Initialize worklock worklock_supply = 3 * token_economics.minimum_allowed_locked + token_economics.maximum_allowed_locked @@ -443,7 +466,7 @@ def test_all(testerchain, assert token.functions.balanceOf(worklock.address).call() == worklock_supply - staker2_tokens tx = escrow.functions.setWorker(staker2).transact({'from': staker2}) testerchain.wait_for_receipt(tx) - escrow_supply = token_economics.erc20_reward_supply + staker2_tokens + escrow_supply += staker2_tokens assert escrow.functions.getAllTokens(staker2).call() == staker2_tokens assert escrow.functions.getCompletedWork(staker2).call() == 0 tx = escrow.functions.setWindDown(True).transact({'from': staker2}) @@ -453,9 +476,10 @@ def test_all(testerchain, tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0}) testerchain.wait_for_receipt(tx) assert worklock.functions.workInfo(staker1).call()[2] - staker1_tokens = worklock.functions.ethToTokens(2 * deposited_eth_2).call() + staker1_claims = worklock.functions.ethToTokens(2 * deposited_eth_2).call() + staker1_tokens += staker1_claims assert escrow.functions.getLockedTokens(staker1, 1).call() == staker1_tokens - escrow_supply += staker1_tokens + escrow_supply += staker1_claims # Staker prolongs lock duration tx = escrow.functions.prolongStake(0, 3).transact({'from': staker2, 'gas_price': 0}) @@ -534,10 +558,6 @@ def test_all(testerchain, testerchain.wait_for_receipt(tx) # Check that nothing is locked - assert 0 == escrow.functions.getLockedTokens(staker1, 0).call() - assert 0 == escrow.functions.getLockedTokens(staker2, 0).call() - assert 0 == escrow.functions.getLockedTokens(staker3, 0).call() - assert 0 == escrow.functions.getLockedTokens(staker4, 0).call() assert 0 == escrow.functions.getLockedTokens(preallocation_escrow_1.address, 0).call() assert 0 == escrow.functions.getLockedTokens(preallocation_escrow_2.address, 0).call() assert 0 == escrow.functions.getLockedTokens(contracts_owners[0], 0).call() @@ -572,6 +592,7 @@ def test_all(testerchain, tx = escrow.functions.initialize(token_economics.erc20_reward_supply) \ .buildTransaction({'from': multisig.address, 'gasPrice': 0}) execute_multisig_transaction(testerchain, multisig, [contracts_owners[0], contracts_owners[1]], tx) + escrow_supply += token_economics.erc20_reward_supply # Grant access to transfer tokens tx = token.functions.approve(escrow.address, 10000).transact({'from': creator}) @@ -590,12 +611,11 @@ def test_all(testerchain, tx = escrow.functions.confirmActivity().transact({'from': staker1}) testerchain.wait_for_receipt(tx) escrow_supply += 1000 - first_sub_stake = staker1_tokens second_sub_stake = 1000 staker1_tokens += second_sub_stake assert token.functions.balanceOf(escrow.address).call() == escrow_supply assert token.functions.balanceOf(staker1).call() == 9000 - assert escrow.functions.getLockedTokens(staker1, 0).call() == 0 + assert escrow.functions.getLockedTokens(staker1, 0).call() == token_economics.minimum_allowed_locked assert escrow.functions.getLockedTokens(staker1, 1).call() == staker1_tokens assert escrow.functions.getLockedTokens(staker1, token_economics.minimum_locked_periods).call() == staker1_tokens assert escrow.functions.getLockedTokens(staker1, token_economics.minimum_locked_periods + 1).call() == second_sub_stake @@ -649,7 +669,7 @@ def test_all(testerchain, # Divide stakes tx = escrow.functions.divideStake(0, 500, 6).transact({'from': staker2}) testerchain.wait_for_receipt(tx) - tx = escrow.functions.divideStake(1, 500, 9).transact({'from': staker1}) + tx = escrow.functions.divideStake(2, 500, 9).transact({'from': staker1}) testerchain.wait_for_receipt(tx) tx = preallocation_escrow_interface_1.functions.divideStake(0, 500, 6).transact({'from': staker3}) testerchain.wait_for_receipt(tx) diff --git a/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py b/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py index 7481b3a3f..bb3fdbecd 100644 --- a/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py +++ b/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py @@ -19,6 +19,7 @@ along with nucypher. If not, see . import pytest from eth_tester.exceptions import TransactionFailed from eth_utils import to_checksum_address +from web3.contract import Contract MAX_SUB_STAKES = 30 MAX_UINT16 = 65535 @@ -712,3 +713,113 @@ def test_allowable_locked_tokens(testerchain, token_economics, token, escrow_con staker1_lock += minimum_allowed tx = escrow.functions.lock(maximum_allowed - staker1_lock, duration).transact({'from': staker1}) testerchain.wait_for_receipt(tx) + + +@pytest.mark.slow +def test_batch_deposit(testerchain, token, escrow_contract, deploy_contract): + escrow = escrow_contract(1500) + policy_manager_interface = testerchain.get_contract_factory('PolicyManagerForStakingEscrowMock') + policy_manager = testerchain.client.get_contract( + abi=policy_manager_interface.abi, + address=escrow.functions.policyManager().call(), + ContractFactoryClass=Contract) + + creator = testerchain.client.accounts[0] + deposit_log = escrow.events.Deposited.createFilter(fromBlock='latest') + lock_log = escrow.events.Locked.createFilter(fromBlock='latest') + + # Grant access to transfer tokens + tx = token.functions.approve(escrow.address, 10000).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + + # Deposit tokens for 1 staker + staker = testerchain.client.accounts[1] + tx = escrow.functions.batchDeposit([staker], [1000], [10]).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + assert token.functions.balanceOf(escrow.address).call() == 1000 + assert escrow.functions.getAllTokens(staker).call() == 1000 + assert escrow.functions.getLockedTokens(staker, 0).call() == 0 + assert escrow.functions.getLockedTokens(staker, 1).call() == 1000 + assert escrow.functions.getLockedTokens(staker, 10).call() == 1000 + assert escrow.functions.getLockedTokens(staker, 11).call() == 0 + current_period = escrow.functions.getCurrentPeriod().call() + assert policy_manager.functions.getPeriodsLength(staker).call() == 1 + assert policy_manager.functions.getPeriod(staker, 0).call() == current_period - 1 + assert escrow.functions.getPastDowntimeLength(staker).call() == 0 + assert escrow.functions.getLastActivePeriod(staker).call() == 0 + + deposit_events = deposit_log.get_all_entries() + assert len(deposit_events) == 1 + event_args = deposit_events[-1]['args'] + assert event_args['staker'] == staker + assert event_args['value'] == 1000 + assert event_args['periods'] == 10 + + lock_events = lock_log.get_all_entries() + assert len(lock_events) == 1 + event_args = lock_events[-1]['args'] + assert event_args['staker'] == staker + assert event_args['value'] == 1000 + assert event_args['firstPeriod'] == current_period + 1 + assert event_args['periods'] == 10 + + # Can't deposit tokens again for the same staker twice + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.batchDeposit([testerchain.client.accounts[1]], [1000], [10])\ + .transact({'from': creator}) + testerchain.wait_for_receipt(tx) + + # Can't deposit tokens with too low or too high value + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.batchDeposit([testerchain.client.accounts[2]], [1], [10])\ + .transact({'from': creator}) + testerchain.wait_for_receipt(tx) + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.batchDeposit([testerchain.client.accounts[2]], [1501], [10])\ + .transact({'from': creator}) + testerchain.wait_for_receipt(tx) + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.batchDeposit([testerchain.client.accounts[2]], [500], [1])\ + .transact({'from': creator}) + testerchain.wait_for_receipt(tx) + + # Initialize Escrow contract + tx = escrow.functions.initialize(0).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + + # Deposit tokens for multiple stakers + stakers = testerchain.client.accounts[2:7] + tx = escrow.functions.batchDeposit( + stakers, [100, 200, 300, 400, 500], [50, 100, 150, 200, 250]).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + + assert 2500 == token.functions.balanceOf(escrow.address).call() + current_period = escrow.functions.getCurrentPeriod().call() + deposit_events = deposit_log.get_all_entries() + lock_events = lock_log.get_all_entries() + + assert len(deposit_events) == 6 + assert len(lock_events) == 6 + + for index, staker in enumerate(stakers): + value = 100 * (index + 1) + duration = 50 * (index + 1) + assert escrow.functions.getAllTokens(staker).call() == value + assert escrow.functions.getLockedTokens(staker, 1).call() == value + assert escrow.functions.getLockedTokens(staker, duration).call() == value + assert escrow.functions.getLockedTokens(staker, duration + 1).call() == 0 + assert policy_manager.functions.getPeriodsLength(staker).call() == 1 + assert policy_manager.functions.getPeriod(staker, 0).call() == current_period - 1 + assert escrow.functions.getPastDowntimeLength(staker).call() == 0 + assert escrow.functions.getLastActivePeriod(staker).call() == 0 + + event_args = deposit_events[index + 1]['args'] + assert event_args['staker'] == staker + assert event_args['value'] == value + assert event_args['periods'] == duration + + event_args = lock_events[index + 1]['args'] + assert event_args['staker'] == staker + assert event_args['value'] == value + assert event_args['firstPeriod'] == current_period + 1 + assert event_args['periods'] == duration diff --git a/tests/metrics/estimate_gas.py b/tests/metrics/estimate_gas.py index 0036d6480..b38f7af9e 100755 --- a/tests/metrics/estimate_gas.py +++ b/tests/metrics/estimate_gas.py @@ -206,6 +206,16 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None: transact(token_functions.transfer(ursula2, MIN_ALLOWED_LOCKED * 10), {'from': origin}) transact(token_functions.transfer(ursula3, MIN_ALLOWED_LOCKED * 10), {'from': origin}) + # + # Batch deposit tokens + # + transact(token_functions.approve(staking_agent.contract_address, MIN_ALLOWED_LOCKED * 5), {'from': origin}) + transact_and_log("Batch deposit tokens for 5 owners", + staker_functions.batchDeposit(everyone_else[0:5], + [MIN_ALLOWED_LOCKED] * 5, + [MIN_LOCKED_PERIODS] * 5), + {'from': origin}) + # # Ursula and Alice give Escrow rights to transfer #