WorkLock: small refactoring of methods and tests

pull/1773/head
vzotova 2020-02-28 21:32:08 +03:00
parent 8571f220f4
commit 41cbd542ee
10 changed files with 194 additions and 90 deletions

View File

@ -1734,8 +1734,8 @@ class Bidder(NucypherTokenActor):
receipt = self.worklock_agent.cancel_bid(checksum_address=self.checksum_address)
return receipt
def _get_maximum_allowed_bid(self) -> int:
"""Returns maximum allowed bid for current deposit rate"""
def _get_max_bid_from_max_stake(self) -> int:
"""Returns maximum allowed bid calculated from maximum allowed locked tokens"""
max_tokens = self.economics.maximum_allowed_locked
eth_supply = self.worklock_agent.get_eth_supply()
worklock_supply = self.economics.worklock_supply
@ -1745,20 +1745,24 @@ class Bidder(NucypherTokenActor):
# TODO make public and CLI command to print the list
def _get_incorrect_bids(self) -> List[str]:
bidders = self.worklock_agent.get_bidders()
max_bid = self._get_maximum_allowed_bid()
max_bid_from_max_stake = self._get_max_bid_from_max_stake()
incorrect = list()
if max_bid_from_max_stake >= self.economics.worklock_max_allowed_bid:
return incorrect
for bidder in bidders:
if self.worklock_agent.get_deposited_eth(bidder) > max_bid:
if self.worklock_agent.get_deposited_eth(bidder) > max_bid_from_max_stake:
incorrect.append(bidder)
return incorrect
# TODO better control: max iterations, gas limit for each iteration
# TODO better control: max iterations, interactive mode
def verify_bidding_correctness(self, gas_limit: int) -> dict:
end = self.worklock_agent.end_cancellation_date
error = f"Checking of bidding is allowed only when the cancellation window is closed (closes at {end})."
self._ensure_cancellation_window(ensure_closed=True, message=error)
if self.worklock_agent.is_claiming_available():
if self.worklock_agent.bidders_checked():
raise self.BidderError(f"Check has already done")
incorrect_bidders = self._get_incorrect_bids()
@ -1767,7 +1771,7 @@ class Bidder(NucypherTokenActor):
receipts = dict()
iteration = 1
while not self.worklock_agent.is_claiming_available():
while not self.worklock_agent.bidders_checked():
receipt = self.worklock_agent.verify_bidding_correctness(checksum_address=self.checksum_address,
gas_limit=gas_limit)
receipts[iteration] = receipt

View File

@ -1139,9 +1139,14 @@ class WorkLockAgent(EthereumContractAgent):
return bidders
def is_claiming_available(self) -> bool:
"""Returns True if bidders have been checked and claiming is available"""
"""Returns True if claiming is available"""
return self.contract.functions.isClaimingAvailable().call()
def bidders_checked(self) -> bool:
"""Returns True if bidders have been checked"""
bidders_population = self.get_bidders_population()
return self.contract.functions.nextBidderToCheck().call() == bidders_population
@property
def minimum_allowed_bid(self) -> int:
min_bid = self.contract.functions.minAllowedBid().call()

View File

@ -25,6 +25,7 @@ contract WorkLock {
event Refund(address indexed sender, uint256 refundETH, uint256 completedWork);
event Canceled(address indexed sender, uint256 value);
event BiddersChecked(address indexed sender, uint256 startIndex, uint256 endIndex);
event ClaimingEnabled(address indexed sender);
struct WorkInfo {
uint256 depositedETH;
@ -221,12 +222,12 @@ contract WorkLock {
"Checking bidders is allowed when bidding and cancellation phases are over");
require(nextBidderToCheck != bidders.length, "Bidders have already been checked");
uint256 maxAllowableBid = maxAllowableLockedTokens.mul(ethSupply).div(tokenSupply);
uint256 maxBidFromMaxStake = maxAllowableLockedTokens.mul(ethSupply).div(tokenSupply);
uint256 index = nextBidderToCheck;
while (index < bidders.length && gasleft() > _gasToSaveState) {
address bidder = bidders[index];
require(workInfo[bidder].depositedETH <= maxAllowableBid);
require(workInfo[bidder].depositedETH <= maxBidFromMaxStake);
index++;
}
@ -251,7 +252,7 @@ contract WorkLock {
function claim() external returns (uint256 claimedTokens) {
require(block.timestamp >= endCancellationDate,
"Claiming tokens is allowed when bidding and cancellation phases are over");
require(nextBidderToCheck == bidders.length, "Bidders have not been checked");
require(isClaimingAvailable(), "Claiming has not been enabled yet");
WorkInfo storage info = workInfo[msg.sender];
require(!info.claimed, "Tokens are already claimed");
claimedTokens = ethToTokens(info.depositedETH);

View File

@ -271,9 +271,11 @@ def refund(general_config, worklock_options, registry_options, force, hw_wallet)
@group_worklock_options
@option_force
@option_hw_wallet
@click.option('--gas-limit', help="Gas limit per transaction", type=click.IntRange(min=1))
def verify_correctness(general_config, registry_options, worklock_options, force, hw_wallet, gas_limit):
"""Verify correctness of bidding"""
@click.option('--gas-limit', help="Gas limit per each verification transaction", type=click.IntRange(min=60000))
# TODO: Consider moving to administrator (nucypher-deploy)
# TODO: interactive mode for each step, choosing only specified steps
def post_initialization(general_config, registry_options, worklock_options, force, hw_wallet, gas_limit):
"""Ensure correctness of bidding and enable claiming"""
emitter = _setup_emitter(general_config)
if not worklock_options.bidder_address: # TODO: Consider bundle this in worklock_options
worklock_options.bidder_address = select_client_account(emitter=emitter,
@ -285,15 +287,16 @@ def verify_correctness(general_config, registry_options, worklock_options, force
if not gas_limit:
# TODO print gas estimations
gas_limit = click.prompt(f"Enter gas limit per each transaction", type=click.IntRange(min=1))
gas_limit = click.prompt(f"Enter gas limit per each verification transaction", type=click.IntRange(min=60000))
if not force:
click.confirm(f"Confirm verifying of bidding from {worklock_options.bidder_address} "
f"using {gas_limit} gas per each transaction?", abort=True)
receipts = bidder.verify_bidding_correctness(gas_limit=gas_limit)
verification_receipts = bidder.verify_bidding_correctness(gas_limit=gas_limit)
emitter.echo("Bidding has been checked\n", color='green')
for iteration, receipt in receipts.items():
for iteration, receipt in verification_receipts.items():
paint_receipt_summary(receipt=receipt,
emitter=emitter,
chain_name=bidder.staking_agent.blockchain.client.chain_name,

View File

@ -37,22 +37,22 @@ contract StakingEscrowForWorkLockMock {
minLockedPeriods = _minLockedPeriods;
}
function getCompletedWork(address _staker) public view returns (uint256) {
function getCompletedWork(address _staker) external view returns (uint256) {
return stakerInfo[_staker].completedWork;
}
function setWorkMeasurement(address _staker, bool _measureWork) public returns (uint256) {
function setWorkMeasurement(address _staker, bool _measureWork) external returns (uint256) {
stakerInfo[_staker].measureWork = _measureWork;
return stakerInfo[_staker].completedWork;
}
function deposit(address _staker, uint256 _value, uint16 _periods) public {
function deposit(address _staker, uint256 _value, uint16 _periods) external {
stakerInfo[_staker].value = _value;
stakerInfo[_staker].periods = _periods;
token.transferFrom(msg.sender, address(this), _value);
}
function setCompletedWork(address _staker, uint256 _completedWork) public {
function setCompletedWork(address _staker, uint256 _completedWork) external {
stakerInfo[_staker].completedWork = _completedWork;
}

View File

@ -401,6 +401,7 @@ def test_all(testerchain,
# Check all bidders
assert worklock.functions.getBiddersLength().call() == 2
assert worklock.functions.nextBidderToCheck().call() == 0
tx = worklock.functions.verifyBiddingCorrectness(30000).transact()
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 2
@ -432,7 +433,6 @@ def test_all(testerchain,
assert escrow.functions.stakerInfo(staker2).call()[WIND_DOWN_FIELD]
staker1_tokens = worklock_supply // 10
assert escrow.functions.minAllowableLockedTokens().call() == token_economics.minimum_allowed_locked
tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker1).call()[2]

View File

@ -79,8 +79,8 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
checks_log = worklock.events.BiddersChecked.createFilter(fromBlock='latest')
# Transfer tokens to WorkLock
worklock_supply_1 = 2 * token_economics.maximum_allowed_locked // 10 + 1
worklock_supply_2 = token_economics.maximum_allowed_locked // 10 - 1
worklock_supply_1 = token_economics.maximum_allowed_locked // 2 + 1
worklock_supply_2 = token_economics.maximum_allowed_locked // 2 - 1
worklock_supply = worklock_supply_1 + worklock_supply_2
tx = token.functions.approve(worklock.address, worklock_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
@ -103,7 +103,7 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
# Give stakers some ETH
deposit_eth_1 = 4 * min_allowed_bid
deposit_eth_2 = min_allowed_bid
staker1_balance = 10 * deposit_eth_1
staker1_balance = 100 * deposit_eth_1
tx = testerchain.w3.eth.sendTransaction(
{'from': testerchain.etherbase_account, 'to': staker1, 'value': staker1_balance})
testerchain.wait_for_receipt(tx)
@ -333,6 +333,10 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Can't check before end of cancellation window
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
testerchain.wait_for_receipt(tx)
# But can cancel during cancellation window
staker3_balance = testerchain.w3.eth.getBalance(staker3)
@ -357,11 +361,6 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
assert event_args['sender'] == staker3
assert event_args['value'] == staker3_bid
# Can't check before end of cancellation window
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
testerchain.wait_for_receipt(tx)
# Wait for the end of cancellation window
testerchain.time_travel(seconds=3600) # Wait exactly 1 hour
@ -370,48 +369,19 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Too low value for remaining gas
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(0).transact({'from': staker1, 'gas': 35000})
testerchain.wait_for_receipt(tx)
# Too low value for gas limit
assert worklock.functions.nextBidderToCheck().call() == 0
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact({'gas': gas_to_save_state + 5000})
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 0
# Set gas only for one check
# TODO failed cases
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state)\
.transact({'from': staker1, 'gas': 60000, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 1
events = checks_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert event_args['sender'] == staker1
assert event_args['startIndex'] == 0
assert event_args['endIndex'] == 1
# Still can't claim because checked only portion of bidders
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Check all others
# Check all bidders
# TODO failed cases with force refund
assert not worklock.functions.isClaimingAvailable().call()
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 3
assert worklock.functions.isClaimingAvailable().call()
assert not worklock.functions.isClaimingAvailable().call()
events = checks_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert 1 == len(events)
event_args = events[-1]['args']
assert event_args['sender'] == creator
assert event_args['startIndex'] == 1
assert event_args['startIndex'] == 0
assert event_args['endIndex'] == 3
# Can't check again
@ -419,6 +389,13 @@ def test_worklock(testerchain, token_economics, deploy_contract, token, escrow):
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
testerchain.wait_for_receipt(tx)
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
assert worklock.functions.isClaimingAvailable().call()
assert 2 == len(events)
# Can't check again
# Staker claims tokens
value, measure_work, _completed_work, periods = escrow.functions.stakerInfo(staker1).call()
assert not measure_work
@ -647,18 +624,23 @@ def test_reentrancy(testerchain, token_economics, deploy_contract, token, escrow
@pytest.mark.slow
def test_max_allowed(testerchain, token_economics, deploy_contract, token, escrow):
creator, bidder1, bidder2, *everyone_else = testerchain.w3.eth.accounts
def test_verifying_correctness(testerchain, token_economics, deploy_contract, token, escrow):
creator, bidder1, bidder2, bidder3, *everyone_else = testerchain.w3.eth.accounts
gas_to_save_state = 30000
# Deploy WorkLock
boosting_refund = 100
staking_periods = token_economics.minimum_locked_periods
min_allowed_bid = to_wei(1, 'ether')
def deploy_worklock(supply):
now = testerchain.w3.eth.getBlock(block_identifier='latest').timestamp
start_bid_date = now
end_bid_date = start_bid_date + (60 * 60)
end_cancellation_date = end_bid_date
boosting_refund = 100
staking_periods = token_economics.minimum_locked_periods
min_allowed_bid = to_wei(1, 'ether')
worklock, _ = deploy_contract(
contract, _ = deploy_contract(
contract_name='WorkLock',
_token=token.address,
_escrow=escrow.address,
@ -669,11 +651,18 @@ def test_max_allowed(testerchain, token_economics, deploy_contract, token, escro
_stakingPeriods=staking_periods,
_minAllowedBid=min_allowed_bid
)
tx = token.functions.approve(contract.address, supply).transact()
testerchain.wait_for_receipt(tx)
tx = contract.functions.tokenDeposit(supply).transact()
testerchain.wait_for_receipt(tx)
log = contract.events.BiddersChecked.createFilter(fromBlock='latest')
return contract, log
# Test: bidder has too much tokens to claim
worklock_supply = token_economics.maximum_allowed_locked + 1
tx = token.functions.approve(worklock.address, worklock_supply).transact()
testerchain.wait_for_receipt(tx)
tx = worklock.functions.tokenDeposit(worklock_supply).transact()
testerchain.wait_for_receipt(tx)
worklock, _checks_log = deploy_worklock(worklock_supply, min_allowed_bid)
# Bid
tx = testerchain.w3.eth.sendTransaction(
@ -685,6 +674,102 @@ def test_max_allowed(testerchain, token_economics, deploy_contract, token, escro
# Check will fail because bidder has too much tokens to claim
testerchain.time_travel(seconds=3600) # Wait exactly 1 hour
worklock_balance = testerchain.w3.eth.getBalance(worklock.address)
default_max = worklock.functions.maxAllowableLockedTokens().call()
assert default_max * worklock_balance // worklock_supply < min_allowed_bid
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(30000).transact()
testerchain.wait_for_receipt(tx)
# Test: bidder will get tokens as much as possible without force refund
worklock_supply = 3 * token_economics.maximum_allowed_locked
worklock, checks_log = deploy_worklock(worklock_supply, min_allowed_bid)
# Bids
for bidder in [bidder1, bidder2, bidder3]:
tx = testerchain.w3.eth.sendTransaction(
{'from': testerchain.etherbase_account, 'to': bidder, 'value': min_allowed_bid})
testerchain.wait_for_receipt(tx)
tx = worklock.functions.bid().transact({'from': bidder, 'value': min_allowed_bid, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.ethToTokens(min_allowed_bid).call() == token_economics.maximum_allowed_locked
# Wait exactly 1 hour
testerchain.time_travel(seconds=3600)
worklock_balance = testerchain.w3.eth.getBalance(worklock.address)
default_max = worklock.functions.defaultMaxAllowableLockedTokens().call()
assert default_max * worklock_balance // worklock_supply == min_allowed_bid
# Too low value for gas limit
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state)\
.transact({'from': bidder1, 'gas': gas_to_save_state + 20000})
testerchain.wait_for_receipt(tx)
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state)\
.transact({'from': bidder1, 'gas': gas_to_save_state + 25000})
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 3
events = checks_log.get_all_entries()
assert 1 == len(events)
event_args = events[-1]['args']
assert event_args['sender'] == bidder1
assert event_args['startIndex'] == 0
assert event_args['endIndex'] == 3
# Test: partial verification with low amount of gas limit
worklock_supply = 3 * token_economics.maximum_allowed_locked
worklock, checks_log = deploy_worklock(worklock_supply)
# Bids
for bidder in [bidder1, bidder2, bidder3]:
tx = testerchain.w3.eth.sendTransaction(
{'from': testerchain.etherbase_account, 'to': bidder, 'value': min_allowed_bid})
testerchain.wait_for_receipt(tx)
tx = worklock.functions.bid().transact({'from': bidder, 'value': min_allowed_bid, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.ethToTokens(min_allowed_bid).call() == worklock_supply // 3
# Wait exactly 1 hour
testerchain.time_travel(seconds=3600) # Wait exactly 1 hour
worklock_balance = testerchain.w3.eth.getBalance(worklock.address)
default_max = worklock.functions.maxAllowableLockedTokens().call()
max_bid_from_max_stake = default_max * worklock_balance // worklock_supply
assert max_bid_from_max_stake >= min_allowed_bid
# Too low value for remaining gas
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.verifyBiddingCorrectness(0)\
.transact({'from': bidder1, 'gas': gas_to_save_state + 25000})
testerchain.wait_for_receipt(tx)
# Too low value for gas limit
assert worklock.functions.nextBidderToCheck().call() == 0
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact({'gas': gas_to_save_state + 25000})
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 0
# Set gas only for one check
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state)\
.transact({'gas': gas_to_save_state + 30000, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 1
events = checks_log.get_all_entries()
assert 1 == len(events)
event_args = events[-1]['args']
assert event_args['sender'] == creator
assert event_args['startIndex'] == 0
assert event_args['endIndex'] == 1
# Check others
tx = worklock.functions.verifyBiddingCorrectness(gas_to_save_state).transact()
testerchain.wait_for_receipt(tx)
assert worklock.functions.nextBidderToCheck().call() == 3
events = checks_log.get_all_entries()
assert 2 == len(events)
event_args = events[-1]['args']
assert event_args['sender'] == creator
assert event_args['startIndex'] == 1
assert event_args['endIndex'] == 3

View File

@ -71,12 +71,14 @@ def test_verify_correctness(testerchain, agency, token_economics, test_registry)
# Wait until the cancellation window closes...
testerchain.time_travel(seconds=token_economics.cancellation_window_duration+1)
assert not worklock_agent.is_claiming_available()
assert not worklock_agent.bidders_checked()
assert not worklock_agent.is_claiming_available(
with pytest.raises(Bidder.BidderError):
_receipt = bidder.claim()
receipts = bidder.verify_bidding_correctness(gas_limit=100000)
assert worklock_agent.is_claiming_available()
assert worklock_agent.bidders_checked()
assert worklock_agent.is_claiming_available(
for iteration, receipt in receipts.items():
assert receipt['status'] == 1

View File

@ -102,8 +102,10 @@ def test_claim_before_checking(testerchain, agency, token_economics, test_regist
def test_verify_correctness(testerchain, agency, token_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
caller = testerchain.unassigned_accounts[0]
assert not agent.bidders_checked()
receipt = agent.verify_bidding_correctness(checksum_address=caller, gas_limit=100000)
assert receipt['status'] == 1
assert agent.bidders_checked()
assert agent.is_claiming_available()

View File

@ -115,7 +115,7 @@ def test_cancel_bid(click_runner, testerchain, agency_local_registry, token_econ
assert not agent.get_deposited_eth(bidder) # No more bid
def test_verify_correctness(click_runner, testerchain, agency_local_registry, token_economics):
def test_post_initialization(click_runner, testerchain, agency_local_registry, token_economics):
# Wait until the end of the cancellation period
testerchain.time_travel(seconds=token_economics.cancellation_window_duration+2)
@ -123,8 +123,9 @@ def test_verify_correctness(click_runner, testerchain, agency_local_registry, to
bidder = testerchain.unassigned_accounts[0]
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
assert not agent.is_claiming_available()
assert not agent.bidders_checked()
command = ('verify-correctness',
command = ('post-initialization',
'--bidder-address', bidder,
'--registry-filepath', agency_local_registry.filepath,
'--provider', TEST_PROVIDER_URI,
@ -136,6 +137,7 @@ def test_verify_correctness(click_runner, testerchain, agency_local_registry, to
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert agent.is_claiming_available()
assert agent.bidders_checked()
def test_claim(click_runner, testerchain, agency_local_registry, token_economics):