StakingInterface: worklock commands

pull/1788/head
vzotova 2020-03-23 17:56:47 +03:00
parent 536d55cfed
commit ce684cce54
8 changed files with 333 additions and 36 deletions

View File

@ -48,8 +48,8 @@ from nucypher.blockchain.eth.decorators import validate_secret, validate_checksu
from nucypher.blockchain.eth.interfaces import (
BlockchainDeployerInterface,
BlockchainInterfaceFactory,
VersionedContract
)
VersionedContract,
BlockchainInterface)
from nucypher.blockchain.eth.registry import AllocationRegistry
from nucypher.blockchain.eth.registry import BaseContractRegistry
@ -870,11 +870,20 @@ class StakingInterfaceDeployer(BaseContractDeployer, UpgradeableContractMixin):
contract_name=policy_contract_name,
proxy_name=policy_proxy_name)
worklock_name = WorklockDeployer.contract_name
try:
self.worklock_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=worklock_name)
except BaseContractRegistry.UnknownContract:
self.worklock_contract = None
def _deploy_essential(self, contract_version: str, gas_limit: int = None, confirmations: int = 0):
"""Note: These parameters are order-sensitive"""
worklock_address = self.worklock_contract.address if self.worklock_contract else BlockchainInterface.NULL_ADDRESS
constructor_args = (self.token_contract.address,
self.staking_contract.address,
self.policy_contract.address)
self.policy_contract.address,
worklock_address)
contract, deployment_receipt = self.blockchain.deploy_contract(self.deployer_address,
self.registry,

View File

@ -194,7 +194,7 @@ contract PolicyManager is Upgradeable {
/**
* @notice Get the minimum reward rate acceptable by node
*/
function getMinRewardRate(NodeInfo storage _nodeInfo) internal returns (uint256) {
function getMinRewardRate(NodeInfo storage _nodeInfo) internal view returns (uint256) {
// if minRewardRate has not been set or is outside the acceptable range
if (_nodeInfo.minRewardRate == 0 ||
_nodeInfo.minRewardRate < minRewardRateRange.min ||
@ -208,7 +208,7 @@ contract PolicyManager is Upgradeable {
/**
* @notice Get the minimum reward rate acceptable by node
*/
function getMinRewardRate(address _node) public returns (uint256) {
function getMinRewardRate(address _node) public view returns (uint256) {
NodeInfo storage nodeInfo = nodes[_node];
return getMinRewardRate(nodeInfo);
}

View File

@ -62,7 +62,7 @@ contract AbstractStakingContract {
* @dev Checks permission for calling fallback function
*/
function isFallbackAllowed() public returns (bool);
/**
* @dev Withdraw tokens from staking contract
*/

View File

@ -5,13 +5,14 @@ import "contracts/staking_contracts/AbstractStakingContract.sol";
import "contracts/NuCypherToken.sol";
import "contracts/StakingEscrow.sol";
import "contracts/PolicyManager.sol";
import "contracts/WorkLock.sol";
/**
* @notice Interface for accessing main contracts from a staking contract
* @dev All methods must be stateless because this code will be executed by delegatecall call.
* If state is needed - use getStateContract() method to access state of this contract.
* @dev |v1.3.1|
* @dev |v1.4.1|
*/
contract StakingInterface {
@ -27,30 +28,41 @@ contract StakingInterface {
event WorkerSet(address indexed sender, address worker);
event Prolonged(address indexed sender, uint256 index, uint16 periods);
event WindDownSet(address indexed sender, bool windDown);
event Bid(address indexed sender, uint256 depositedETH);
event Claimed(address indexed sender, uint256 claimedTokens);
event Refund(address indexed sender, uint256 refundETH);
event BidCanceled(address indexed sender);
event CompensationWithdrawn(address indexed sender);
NuCypherToken public token;
StakingEscrow public escrow;
PolicyManager public policyManager;
WorkLock public workLock;
/**
* @notice Constructor sets addresses of the contracts
* @param _token Token contract
* @param _escrow Escrow contract
* @param _policyManager PolicyManager contract
* @param _workLock WorkLock contract
*/
constructor(
NuCypherToken _token,
StakingEscrow _escrow,
PolicyManager _policyManager
PolicyManager _policyManager,
WorkLock _workLock
)
public
{
require(_token.totalSupply() > 0 &&
_escrow.secondsPerPeriod() > 0 &&
_policyManager.secondsPerPeriod() > 0);
_policyManager.secondsPerPeriod() > 0 &&
// in case there is no worklock contract
(address(_workLock) == address(0) || _workLock.startBidDate() > 0));
token = _token;
escrow = _escrow;
policyManager = _policyManager;
workLock = _workLock;
}
/**
@ -185,4 +197,54 @@ contract StakingInterface {
emit WindDownSet(msg.sender, _windDown);
}
/**
* @notice Bid for tokens by transferring ETH
*/
function bid(uint256 _value) public payable {
WorkLock workLockFromState = getStateContract().workLock();
require(address(workLockFromState) != address(0));
workLockFromState.bid.value(_value)();
emit Bid(msg.sender, _value);
}
/**
* @notice Cancel bid and refund deposited ETH
*/
function cancelBid() public {
WorkLock workLockFromState = getStateContract().workLock();
require(address(workLockFromState) != address(0));
workLockFromState.cancelBid();
emit BidCanceled(msg.sender);
}
/**
* @notice Withdraw compensation after force refund
*/
function withdrawCompensation() public {
WorkLock workLockFromState = getStateContract().workLock();
require(address(workLockFromState) != address(0));
workLockFromState.withdrawCompensation();
emit CompensationWithdrawn(msg.sender);
}
/**
* @notice Claimed tokens will be deposited and locked as stake in the StakingEscrow contract.
*/
function claim() public {
WorkLock workLockFromState = getStateContract().workLock();
require(address(workLockFromState) != address(0));
uint256 claimedTokens = workLockFromState.claim();
emit Claimed(msg.sender, claimedTokens);
}
/**
* @notice Refund ETH for the completed work
*/
function refund() public {
WorkLock workLockFromState = getStateContract().workLock();
require(address(workLockFromState) != address(0));
uint256 refundETH = workLockFromState.refund();
emit Refund(msg.sender, refundETH);
}
}

View File

@ -113,6 +113,56 @@ contract PolicyManagerForStakingContractMock {
}
/**
* @notice Contract for staking contract tests
*/
contract WorkLockForStakingContractMock {
uint256 public startBidDate = 1;
uint256 public claimed;
uint256 public depositedETH;
uint256 public compensation;
uint256 public refundETH;
function bid() external payable {
depositedETH = msg.value;
}
function cancelBid() external {
uint256 value = depositedETH;
depositedETH = 0;
msg.sender.transfer(value);
}
function sendCompensation() external payable {
compensation = msg.value;
}
function withdrawCompensation() external {
uint256 value = compensation;
compensation = 0;
msg.sender.transfer(value);
}
function claim() external returns (uint256) {
claimed += 1;
return claimed;
}
function sendRefund() external payable {
refundETH = msg.value;
}
function refund() external returns (uint256) {
uint256 value = refundETH;
refundETH = 0;
msg.sender.transfer(value);
return value;
}
}
/**
* @notice Contract for staking contract tests
*/

View File

@ -156,13 +156,13 @@ def generate_args_for_slashing(mock_ursula_reencrypts, ursula):
@pytest.fixture()
def staking_interface(testerchain, token, escrow, policy_manager, deploy_contract):
def staking_interface(testerchain, token, escrow, policy_manager, worklock, deploy_contract):
escrow, _ = escrow
policy_manager, _ = policy_manager
secret_hash = testerchain.w3.keccak(router_secret)
# Creator deploys the staking interface
staking_interface, _ = deploy_contract(
'StakingInterface', token.address, escrow.address, policy_manager.address)
'StakingInterface', token.address, escrow.address, policy_manager.address, worklock.address)
router, _ = deploy_contract(
'StakingInterfaceRouter', staking_interface.address, secret_hash)
return staking_interface, router
@ -331,6 +331,16 @@ def test_all(testerchain,
tx = token.functions.approve(escrow.address, 0).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Create the first preallocation escrow
preallocation_escrow_1, _ = deploy_contract(
'PreallocationEscrow', staking_interface_router.address, token.address, escrow.address)
preallocation_escrow_interface_1 = testerchain.client.get_contract(
abi=staking_interface.abi,
address=preallocation_escrow_1.address,
ContractFactoryClass=Contract)
tx = preallocation_escrow_1.functions.transferOwnership(staker3).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Initialize worklock
worklock_supply = 3 * token_economics.minimum_allowed_locked + token_economics.maximum_allowed_locked
tx = token.functions.approve(worklock.address, worklock_supply).transact({'from': creator})
@ -366,10 +376,15 @@ def test_all(testerchain,
testerchain.wait_for_receipt(tx)
# Other stakers do bid
assert worklock.functions.workInfo(staker4).call()[0] == 0
tx = worklock.functions.bid().transact({'from': staker4, 'value': deposited_eth_2, 'gas_price': 0})
assert worklock.functions.workInfo(preallocation_escrow_1.address).call()[0] == 0
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': preallocation_escrow_1.address, 'value': deposited_eth_2})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker4).call()[0] == deposited_eth_2
assert testerchain.w3.eth.getBalance(preallocation_escrow_1.address) == deposited_eth_2
tx = preallocation_escrow_interface_1.functions.bid(deposited_eth_2).transact({'from': staker3, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(preallocation_escrow_1.address) == 0
assert worklock.functions.workInfo(preallocation_escrow_1.address).call()[0] == deposited_eth_2
worklock_balance += deposited_eth_2
bonus_worklock_supply -= min_stake
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance
@ -396,11 +411,12 @@ def test_all(testerchain,
# One of stakers cancels bid
assert worklock.functions.getBiddersLength().call() == 3
tx = worklock.functions.cancelBid().transact({'from': staker4, 'gas_price': 0})
tx = preallocation_escrow_interface_1.functions.cancelBid().transact({'from': staker3, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker4).call()[0] == 0
assert worklock.functions.workInfo(preallocation_escrow_1.address).call()[0] == 0
worklock_balance -= deposited_eth_2
bonus_worklock_supply += min_stake
assert testerchain.w3.eth.getBalance(preallocation_escrow_1.address) == deposited_eth_2
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance
assert worklock.functions.ethToTokens(deposited_eth_2).call() == min_stake
assert worklock.functions.ethToTokens(2 * deposited_eth_2).call() == min_stake + bonus_worklock_supply // 18
@ -499,17 +515,7 @@ def test_all(testerchain,
tx = worklock.functions.refund().transact({'from': staker2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Create the first preallocation escrow
preallocation_escrow_1, _ = deploy_contract(
'PreallocationEscrow', staking_interface_router.address, token.address, escrow.address)
preallocation_escrow_interface_1 = testerchain.client.get_contract(
abi=staking_interface.abi,
address=preallocation_escrow_1.address,
ContractFactoryClass=Contract)
# Set and lock re-stake parameter in first preallocation escrow
tx = preallocation_escrow_1.functions.transferOwnership(staker3).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert not escrow.functions.stakerInfo(preallocation_escrow_1.address).call()[DISABLE_RE_STAKE_FIELD]
current_period = escrow.functions.getCurrentPeriod().call()
tx = preallocation_escrow_interface_1.functions.lockReStake(current_period + 22).transact({'from': staker3})
@ -948,7 +954,7 @@ def test_all(testerchain,
# Upgrade the preallocation escrow library
# Deploy the same contract as the second version
staking_interface_v2, _ = deploy_contract(
'StakingInterface', token.address, escrow.address, policy_manager.address)
'StakingInterface', token.address, escrow.address, policy_manager.address, worklock.address)
router_secret2 = os.urandom(SECRET_LENGTH)
router_secret2_hash = testerchain.w3.keccak(router_secret2)
# Staker and Alice can't upgrade library, only owner can

View File

@ -50,10 +50,16 @@ def policy_manager(testerchain, deploy_contract):
@pytest.fixture()
def staking_interface(testerchain, token, escrow, policy_manager, deploy_contract):
def worklock(testerchain, deploy_contract):
contract, _ = deploy_contract('WorkLockForStakingContractMock')
return contract
@pytest.fixture()
def staking_interface(testerchain, token, escrow, policy_manager, worklock, deploy_contract):
# Creator deploys the staking interface
contract, _ = deploy_contract(
'StakingInterface', token.address, escrow.address, policy_manager.address)
'StakingInterface', token.address, escrow.address, policy_manager.address, worklock.address)
return contract

View File

@ -20,6 +20,9 @@ import os
import pytest
from eth_utils import keccak
from eth_tester.exceptions import TransactionFailed
from web3.contract import Contract
from nucypher.blockchain.eth.interfaces import BlockchainInterface
@pytest.mark.slow
@ -407,17 +410,13 @@ def test_policy(testerchain, policy_manager, preallocation_escrow, preallocation
@pytest.mark.slow
def test_reentrancy(testerchain, deploy_contract, token, escrow, policy_manager):
proxy, _ = deploy_contract('StakingInterfaceMockV2', token.address, escrow.address, policy_manager.address)
secret = os.urandom(32)
secret_hash = keccak(secret)
router, _ = deploy_contract('StakingInterfaceRouter', proxy.address, secret_hash)
def test_reentrancy(testerchain, preallocation_escrow, deploy_contract):
owner = testerchain.client.accounts[1]
# Prepare contracts
reentrancy_contract, _ = deploy_contract('ReentrancyTest')
contract_address = reentrancy_contract.address
preallocation_escrow, _ = deploy_contract('PreallocationEscrow', router.address, token.address, escrow.address)
tx = preallocation_escrow.functions.transferOwnership(contract_address).transact()
tx = preallocation_escrow.functions.transferOwnership(contract_address).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Transfer ETH to user escrow
@ -438,3 +437,168 @@ def test_reentrancy(testerchain, deploy_contract, token, escrow, policy_manager)
tx = testerchain.client.send_transaction({'to': contract_address})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(contract_address) == balance
@pytest.mark.slow
def test_worklock(testerchain, worklock, preallocation_escrow, preallocation_escrow_interface, staking_interface):
"""
Test worklock functions in the preallocation escrow
"""
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
bids = preallocation_escrow_interface.events.Bid.createFilter(fromBlock='latest')
claims = preallocation_escrow_interface.events.Claimed.createFilter(fromBlock='latest')
refunds = preallocation_escrow_interface.events.Refund.createFilter(fromBlock='latest')
cancellations = preallocation_escrow_interface.events.BidCanceled.createFilter(fromBlock='latest')
compensations = preallocation_escrow_interface.events.CompensationWithdrawn.createFilter(fromBlock='latest')
# Owner can't use the staking interface directly
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.bid(0).transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.cancelBid().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.withdrawCompensation().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.claim().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.refund().transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Send ETH to to the escrow
bid = 10000
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': preallocation_escrow.address, 'value': 2 * bid})
testerchain.wait_for_receipt(tx)
# Bid
assert worklock.functions.depositedETH().call() == 0
assert testerchain.client.get_balance(preallocation_escrow.address) == 2 * bid
tx = preallocation_escrow_interface.functions.bid(bid).transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.depositedETH().call() == bid
assert testerchain.client.get_balance(preallocation_escrow.address) == bid
events = bids.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['depositedETH'] == bid
# Cancel bid
tx = preallocation_escrow_interface.functions.cancelBid().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.depositedETH().call() == 0
assert testerchain.client.get_balance(preallocation_escrow.address) == 2 * bid
events = cancellations.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
# Withdraw compensation
compensation = 11000
tx = worklock.functions.sendCompensation().transact({'from': creator, 'value': compensation, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.compensation().call() == compensation
tx = preallocation_escrow_interface.functions.withdrawCompensation().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.compensation().call() == 0
assert testerchain.client.get_balance(preallocation_escrow.address) == 2 * bid + compensation
events = compensations.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
# Claim
assert worklock.functions.claimed().call() == 0
tx = preallocation_escrow_interface.functions.claim().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.claimed().call() == 1
events = claims.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['claimedTokens'] == 1
# Withdraw refund
refund = 12000
tx = worklock.functions.sendRefund().transact({'from': creator, 'value': refund, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.refundETH().call() == refund
tx = preallocation_escrow_interface.functions.refund().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.refundETH().call() == 0
assert testerchain.client.get_balance(preallocation_escrow.address) == 2 * bid + compensation + refund
events = refunds.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['refundETH'] == refund
@pytest.mark.slow
def test_interface_without_worklock(testerchain, deploy_contract, token, escrow, policy_manager, worklock):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
staking_interface, _ = deploy_contract(
'StakingInterface', token.address, escrow.address, policy_manager.address, worklock.address)
secret = os.urandom(32)
secret_hash = keccak(secret)
router, _ = deploy_contract('StakingInterfaceRouter', staking_interface.address, secret_hash)
preallocation_escrow, _ = deploy_contract('PreallocationEscrow', router.address, token.address, escrow.address)
# Transfer ownership
tx = preallocation_escrow.functions.transferOwnership(owner).transact({'from': creator})
testerchain.wait_for_receipt(tx)
preallocation_escrow_interface = testerchain.client.get_contract(
abi=staking_interface.abi,
address=preallocation_escrow.address,
ContractFactoryClass=Contract)
# All worklock methods work
tx = preallocation_escrow_interface.functions.bid(0).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = preallocation_escrow_interface.functions.cancelBid().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = preallocation_escrow_interface.functions.withdrawCompensation().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = preallocation_escrow_interface.functions.claim().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = preallocation_escrow_interface.functions.refund().transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Test interface without worklock
secret2 = os.urandom(32)
secret2_hash = keccak(secret2)
staking_interface, _ = deploy_contract(
'StakingInterface', token.address, escrow.address, policy_manager.address, BlockchainInterface.NULL_ADDRESS)
tx = router.functions.upgrade(staking_interface.address, secret, secret2_hash).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Current version of interface doesn't have worklock contract
with pytest.raises((TransactionFailed, ValueError)):
tx = preallocation_escrow_interface.functions.bid(0).transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = preallocation_escrow_interface.functions.cancelBid().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = preallocation_escrow_interface.functions.withdrawCompensation().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = preallocation_escrow_interface.functions.claim().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = preallocation_escrow_interface.functions.refund().transact({'from': owner})
testerchain.wait_for_receipt(tx)