Merge pull request #2866 from vzotova/cleaning-contracts-pt1

Removes `PolicyManager` and `StakingInterface` contracts
pull/2868/head
KPrasch 2022-02-09 12:52:08 -08:00 committed by GitHub
commit 19cb0486dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 10 additions and 6526 deletions

View File

@ -1,159 +0,0 @@
.. _staking_contracts:
Staking Contracts
=================
Architecture
------------
NuCypher :doc:`Main Contracts</contracts_api/index>` enforce a 1:1 relationship between Staker and Worker roles. Even
though the ``StakingEscrow`` smart contract forces the Worker address to be an Ethereum account, the Staker address
can be that of a smart contract. Any staking contract must fulfil the requirements of the Main contracts, which
can be subsequently updated, including ABIs. Therefore, staking contracts must either be upgradeable or
have the ability to route to new methods in the Main contracts.
Development
-----------
To minimize development efforts, developers can use the :doc:`AbstractStakingContract </contracts_api/staking/AbstractStakingContract>`
smart contract API. It accesses Main Contracts through
:doc:`StakingInterface </contracts_api/staking/StakingInterface>` and
:doc:`StakingInterfaceRouter </contracts_api/staking/StakingInterfaceRouter>` so that developers do not need to access
the Main contracts directly or be concerned about upgraded or incompatible ABIs; instead, they can concentrate on
the core logic of their staking contract.
This feature makes it possible for the developer's staking smart contract to not be upgradeable. The
benefit of a non-upgradeable contract is that it reassures users that once the contract is deployed, it cannot
be modified.
Implementation
^^^^^^^^^^^^^^
Currently, there are two versions of ``AbstractStakingContract`` which are very similar except for how they
are initialized:
* ``AbstractStakingContract`` - the constructor is used to initialize values. It is a simplified version that uses
less gas but is not suitable to use with OpenZeppelin's ``Proxy`` contract because of the initialization logic in the
constructor
* ``InitializableStakingContract`` - the initialization logic was extracted from the constructor and
instead provided via an initialization method which allows it to be used with
OpenZeppelin's ``Proxy`` contract
These contracts can be inherited, and the following methods implemented:
* ``isFallbackAllowed()`` - defines who is allowed to call Staker methods. A simple example to allow only the owner:
.. code::
function isFallbackAllowed() public override view returns (bool) {
return msg.sender == owner();
}
* ``withdrawTokens(uint256 _value)`` - the process of withdrawal of NU from the contract
* ``withdrawETH()`` - the process of withdrawal of ETH from the contract
This implementation will result in two ABIs:
* The developer's staking contract implementation
* ``StakingInterface`` to access main contracts
Staking Pool Contract
---------------------
In an effort to reduce the friction associated with staking NU and running a Worker,
a :doc:`simple staking pool smart contract </contracts_api/staking/PoolingStakingContractV2>` is provided.
.. note::
While NuCypher had the staking pool contract `audited by MixBytes <https://github.com/mixbytes/audits_public/tree/master/NuCypher>`_,
there remains smart contract risk and there are no guarantees about the logic. There is also the risk of trust in
the *Owner* of the contract to not be malicious. See the `Risks`_ section below.
The staking pool smart contract organizes multiple NU holders into one large Staker which delegates to a
single Worker. It reduces overall Worker gas costs by combining multiple Stakers, each of whom would
have needed to run a Worker otherwise, into one large Staker that uses a single Worker to perform work on
behalf of the staking pool.
Each token holder can deposit any amount of NU into the pool and will be entitled to the pro-rata
share of the pool and proceeds without needing to maintain and run their own Worker. Token holders will pay a
percentage of NU staking rewards received to the owner of the Worker for running a node. For example, if a token holder
deposits 5% of the total NU deposited to the NU they are entitled to 5% of the staking rewards (after fees) and 5%
of the ETH policy fees received.
Contract Roles
^^^^^^^^^^^^^^
The pooling contract has several roles:
* *Owner* - controls the staking functions including all parameters of staking e.g. ``restake``, ``winddown``, etc.
.. important::
It is recommended to use a multisig, DAO or other decentralized governance mechanism.
* *Worker Owner* - the owner of the Worker that runs on behalf of the staking pool; only this address can
withdraw the worker's collected fee
* *Delegators* - the NU holders who deposit NU into the pooling contract
* *Administrator (Optional)* - oversees contract upgrades which allow modification to all contract logic
and behaviour; this role only applies when the contract is upgradeable and the OpenZeppelin's ``Proxy`` contract
is utilized
.. warning::
Be cautious of who is bestowed the Administrator role because even if the pooling contract was audited,
the Administrator can modify/upgrade the contract code after deployment. **It is recommended to use a
multisig, DAO or other decentralized governance mechanism.**
Worker Fees
^^^^^^^^^^^
The *Worker Owner* receives a percentage of NU staking rewards as a fee for running a Worker on behalf of the
staking pool. This percentage is configured during contract deployment. The implication here is that if the pool
does not generate any rewards then the *Worker Owner* will not receive any fee.
Contract Lifecycle
^^^^^^^^^^^^^^^^^^
* The *Owner* deploys the contract and initializes it by specifying the Worker fee percentage and the *Worker Owner*
address.
* Once deployed, deposits are enabled by default to start accepting deposits from *Delegators*.
* At any point before the *Owner* creates a stake, a Delegator can exit the pool and recoup their original deposit via
the ``withdrawAll`` function.
* After the intended deposits have been received, the *Owner* specifies staking parameters to create a stake: size,
duration, restaking, winddown etc., and bonds the stake to the Worker address.
* Once the *Owner* creates a stake, the ability to deposit into the pool is automatically disabled to prevent any
new deposits. Conditions for disabled deposits are enforced via the use of ``isDepositAllowed`` function checks
within the contract. Disabling deposits ensures that there is clear proportional ownership of the pool and its received
rewards i.e. if a *Delegator* provided 5% of the deposits, they will receive 5% of the proceeds from the staking pool
- staking rewards (after fees) and policy fees. This is a much simpler model for determining proportional ownership
than allowing deposits after staking has started and after staking rewards and policy fees have already been received.
* Once staking rewards and policy fees have been generated, the *Owner* can withdraw these from ``StakingEscrow``
to the pool; staking rewards via ``withdrawAsStaker``, and policy fees via ``withdrawPolicyFee``. *Delegators* can
determine the current value of their proportional share of rewards and fees via the ``getAvailableDelegatorReward``
and ``getAvailableDelegatorETH`` functions respectively. Their share of the proceeds can be withdrawn from the pool
via the ``withdrawTokens`` and ``withdrawETH`` contract functions. Note that this is only for staking rewards and
policy fees, **not** their original deposit. The original deposit can only be withdrawn once the stake has expired.
* Throughout this process, the *Worker Owner* can determine their Worker commission via the ``getAvailableWorkerReward``
function and retrieve it via the ``withdrawWorkerReward`` function.
* When the stake eventually becomes expired and the *Owner* withdraws the pool's escrowed NU from ``StakingEscrow`` via
``withdrawAsStaker``, then the withdrawn NU will be available for *Delegators* to withdraw including their
deposit, and proportional to their share.
* *Delegators* that want to withdraw their original deposit, NU rewards and ETH fees i.e. exit
the pool, they can do so via the ``withdrawAll`` function.
Risks
^^^^^
* The *Owner* / *Administrator* making the pooling contract Upgradeable in which case all logic can be modified.
* The *Owner* disabling ``winddown`` when creating the stake, and never turning it on, thereby keeping the locked
duration constant until ``winddown`` is enabled, potentially never.
* The *Owner* not running a Worker after creating the stake; by not running a node, the stake will be locked until the
work is performed.

View File

@ -28,12 +28,3 @@ Upgradeability and proxies
:glob:
proxy/*
Staking and Pooling Contracts
=============================
.. toctree::
:maxdepth: 1
:glob:
staking/*

View File

@ -137,7 +137,6 @@ Whitepapers
architecture/sub_stakes
architecture/slashing
architecture/periods
architecture/staking_contracts
.. TODO perhaps categorize architecture section
.. toctree::

View File

@ -41,7 +41,6 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
PolicyManagerAgent,
StakingEscrowAgent,
WorkLockAgent,
PREApplicationAgent
@ -61,10 +60,10 @@ from nucypher.blockchain.eth.deployers import (
AdjudicatorDeployer,
BaseContractDeployer,
NucypherTokenDeployer,
PolicyManagerDeployer,
StakingEscrowDeployer,
StakingInterfaceDeployer,
WorklockDeployer, PREApplicationDeployer, SubscriptionManagerDeployer
WorklockDeployer,
PREApplicationDeployer,
SubscriptionManagerDeployer
)
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
@ -194,13 +193,11 @@ class ContractAdministrator(BaseActor):
dispatched_upgradeable_deployer_classes = (
StakingEscrowDeployer,
PolicyManagerDeployer,
AdjudicatorDeployer,
)
upgradeable_deployer_classes = (
*dispatched_upgradeable_deployer_classes,
StakingInterfaceDeployer,
)
aux_deployer_classes = (
@ -208,8 +205,7 @@ class ContractAdministrator(BaseActor):
)
# For ownership transfers.
ownable_deployer_classes = (*dispatched_upgradeable_deployer_classes,
StakingInterfaceDeployer)
ownable_deployer_classes = (*dispatched_upgradeable_deployer_classes,)
# Used in the automated deployment series.
primary_deployer_classes = (*standard_deployer_classes,
@ -335,21 +331,6 @@ class ContractAdministrator(BaseActor):
file.write(data)
return filepath
def set_fee_rate_range(self,
minimum: int,
default: int,
maximum: int,
transaction_gas_limit: int = None) -> TxReceipt:
if not self.transacting_power:
raise self.ActorError('No transacting power available.')
policy_manager_deployer = PolicyManagerDeployer(registry=self.registry, economics=self.economics)
receipt = policy_manager_deployer.set_fee_rate_range(transacting_power=self.transacting_power,
minimum=minimum,
default=default,
maximum=maximum,
gas_limit=transaction_gas_limit)
return receipt
class Staker(NucypherTokenActor):
"""
@ -369,7 +350,6 @@ class Staker(NucypherTokenActor):
self._operator_address = None
# Blockchain
self.policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=self.registry)
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry)
self.economics = EconomicsFactory.get_economics(registry=self.registry)

View File

@ -812,125 +812,6 @@ class StakingEscrowAgent(EthereumContractAgent):
return self.contract.functions.policyManager().call()
class PolicyManagerAgent(EthereumContractAgent):
contract_name: str = POLICY_MANAGER_CONTRACT_NAME
_proxy_name: str = DISPATCHER_CONTRACT_NAME
_excluded_interfaces = (
'verifyState',
'finishUpgrade'
)
@contract_api(TRANSACTION)
def create_policy(self,
policy_id: bytes,
transacting_power: TransactingPower,
value: Wei,
end_timestamp: Timestamp,
node_addresses: List[ChecksumAddress],
owner_address: Optional[ChecksumAddress] = None) -> TxReceipt:
owner_address = owner_address or transacting_power.account
payload = {'value': value}
contract_function: ContractFunction = self.contract.functions.createPolicy(
policy_id,
owner_address,
end_timestamp,
node_addresses)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
payload=payload,
transacting_power=transacting_power) # TODO: Gas management - #842
return receipt
@contract_api(CONTRACT_CALL)
def fetch_policy(self, policy_id: bytes) -> PolicyInfo:
"""
Fetch raw stored blockchain data regarding the policy with the given policy ID.
If `with_owner=True`, this method executes the equivalent of `getPolicyOwner`
to avoid another call.
"""
record = self.contract.functions.policies(policy_id).call()
policy = PolicyInfo(disabled=record[0],
sponsor=record[1],
# If the policyOwner addr is null, we return the sponsor addr instead of the owner.
owner=record[1] if record[2] == NULL_ADDRESS else record[2],
fee_rate=record[3],
start_timestamp=record[4],
end_timestamp=record[5])
return policy
@contract_api(TRANSACTION)
def revoke_policy(self, policy_id: bytes, transacting_power: TransactingPower) -> TxReceipt:
"""Revoke by arrangement ID; Only the policy's author_address can revoke the policy."""
contract_function: ContractFunction = self.contract.functions.revokePolicy(policy_id)
receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def collect_policy_fee(self, collector_address: ChecksumAddress, transacting_power: TransactingPower) -> TxReceipt:
"""Collect fees (ETH) earned since last withdrawal"""
contract_function: ContractFunction = self.contract.functions.withdraw(collector_address)
receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power)
return receipt
@contract_api(CONTRACT_CALL)
def fetch_policy_arrangements(self, policy_id: bytes) -> Iterator[ArrangementInfo]:
record_count = self.contract.functions.getArrangementsLength(policy_id).call()
for index in range(record_count):
arrangement = self.contract.functions.getArrangementInfo(policy_id, index).call()
yield ArrangementInfo(node=arrangement[0],
downtime_index=arrangement[1],
last_refunded_period=arrangement[2])
@contract_api(TRANSACTION)
def revoke_arrangement(self, policy_id: bytes, node_address: ChecksumAddress, transacting_power: TransactingPower) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.revokeArrangement(policy_id, node_address)
receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def calculate_refund(self, policy_id: bytes, transacting_power: TransactingPower) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.calculateRefundValue(policy_id)
receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def collect_refund(self, policy_id: bytes, transacting_power: TransactingPower) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.refund(policy_id)
receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power)
return receipt
@contract_api(CONTRACT_CALL)
def get_fee_amount(self, staker_address: ChecksumAddress) -> Wei:
fee_amount = self.contract.functions.nodes(staker_address).call()[0]
return fee_amount
@contract_api(CONTRACT_CALL)
def get_fee_rate_range(self) -> Tuple[Wei, Wei, Wei]:
"""Check minimum, default & maximum fee rate for all policies ('global fee range')"""
minimum, default, maximum = self.contract.functions.feeRateRange().call()
return minimum, default, maximum
@contract_api(CONTRACT_CALL)
def get_min_fee_rate(self, staker_address: ChecksumAddress) -> Wei:
"""Check minimum fee rate that staker accepts"""
min_rate = self.contract.functions.getMinFeeRate(staker_address).call()
return min_rate
@contract_api(CONTRACT_CALL)
def get_raw_min_fee_rate(self, staker_address: ChecksumAddress) -> Wei:
"""Check minimum acceptable fee rate set by staker for their associated worker"""
min_rate = self.contract.functions.nodes(staker_address).call()[3]
return min_rate
@contract_api(TRANSACTION)
def set_min_fee_rate(self, transacting_power: TransactingPower, min_rate: Wei) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.setMinFeeRate(min_rate)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt
class SubscriptionManagerAgent(EthereumContractAgent):
contract_name: str = SUBSCRIPTION_MANAGER_CONTRACT_NAME

View File

@ -35,7 +35,6 @@ from nucypher.blockchain.eth.agents import (
ContractAgency,
EthereumContractAgent,
NucypherTokenAgent,
PolicyManagerAgent,
StakingEscrowAgent,
WorkLockAgent,
PREApplicationAgent,
@ -730,154 +729,6 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
return preparation_receipts
# TODO delete me
class PolicyManagerDeployer(BaseContractDeployer, UpgradeableContractMixin, OwnableContractMixin):
"""
Depends on StakingEscrow and NucypherTokenAgent
"""
agency = PolicyManagerAgent
contract_name = agency.contract_name
deployment_steps = ('deployment', 'dispatcher_deployment')
_proxy_deployer = DispatcherDeployer
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
proxy_name = StakingEscrowDeployer._proxy_deployer.contract_name
staking_contract_name = StakingEscrowDeployer.contract_name
try:
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=staking_contract_name,
proxy_name=proxy_name)
# If migration is happening then we should get latest StakingEscrow
# but this contract is not yet targeted by Dispatcher
self.staking_implementation = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=staking_contract_name,
enrollment_version='latest')
except self.registry.UnknownContract:
staking_contract_name = StakingEscrowDeployer.contract_name_stub
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=staking_contract_name,
proxy_name=proxy_name)
self.staking_implementation = self.staking_contract
def check_deployment_readiness(self, deployer_address: ChecksumAddress, *args, **kwargs) -> Tuple[bool, list]:
staking_escrow_owner = self.staking_contract.functions.owner().call()
policy_manager_deployment_rules = [
(deployer_address == staking_escrow_owner,
f'{self.contract_name} must be deployed by the owner of {STAKING_ESCROW_CONTRACT_NAME} ({staking_escrow_owner})')
]
return super().check_deployment_readiness(deployer_address=deployer_address,
additional_rules=policy_manager_deployment_rules,
*args, **kwargs)
def _deploy_essential(self,
transacting_power: TransactingPower,
contract_version: str,
gas_limit: int = None,
confirmations: int = 0
) -> tuple:
constructor_kwargs = {"_escrowDispatcher": self.staking_contract.address,
"_escrowImplementation": self.staking_implementation.address}
policy_manager_contract, deploy_receipt = self.blockchain.deploy_contract(transacting_power,
self.registry,
self.contract_name,
gas_limit=gas_limit,
contract_version=contract_version,
confirmations=confirmations,
**constructor_kwargs)
return policy_manager_contract, deploy_receipt
def deploy(self,
transacting_power: TransactingPower,
deployment_mode=FULL,
gas_limit: int = None,
progress=None,
contract_version: str = "latest",
ignore_deployed: bool = False,
confirmations: int = 0,
emitter=None,
) -> Dict[str, dict]:
if deployment_mode not in (BARE, IDLE, FULL):
raise ValueError(f"Invalid deployment mode ({deployment_mode})")
self.check_deployment_readiness(contract_version=contract_version,
ignore_deployed=ignore_deployed,
deployer_address=transacting_power.account)
# Creator deploys the policy manager
if emitter:
emitter.message(f"\nNext Transaction: {self.contract_name} Contract Creation", color='blue', bold=True)
policy_manager_contract, deploy_receipt = self._deploy_essential(transacting_power=transacting_power,
contract_version=contract_version,
gas_limit=gas_limit,
confirmations=confirmations)
# This is the end of bare deployment.
if deployment_mode is BARE:
self._contract = policy_manager_contract
return self._finish_bare_deployment(deployment_receipt=deploy_receipt,
progress=progress)
if progress:
progress.update(1) # how YOU doin?
if emitter:
emitter.message(f"\nNext Transaction: {self._proxy_deployer.contract_name} Contract Creation for {self.contract_name}", color='blue', bold=True)
proxy_deployer = self._proxy_deployer(registry=self.registry, target_contract=policy_manager_contract)
proxy_deploy_receipt = proxy_deployer.deploy(transacting_power=transacting_power,
gas_limit=gas_limit,
confirmations=confirmations)
proxy_deploy_receipt = proxy_deploy_receipt[proxy_deployer.deployment_steps[0]]
if progress:
progress.update(1)
# Cache the dispatcher contract
proxy_contract = proxy_deployer.contract
self.__proxy_contract = proxy_contract
# Wrap the PolicyManager contract, and use this wrapper
wrapped_contract = self.blockchain._wrap_contract(wrapper_contract=proxy_contract,
target_contract=policy_manager_contract)
# Gather the transaction receipts
ordered_receipts = (deploy_receipt, proxy_deploy_receipt)
deployment_receipts = dict(zip(self.deployment_steps, ordered_receipts))
self.deployment_receipts = deployment_receipts
self._contract = wrapped_contract
return deployment_receipts
def set_fee_rate_range(self,
transacting_power: TransactingPower,
minimum: int,
default: int,
maximum: int,
gas_limit: int = None,
confirmations: int = 0) -> dict:
if minimum > default or default > maximum:
raise ValueError(f"Default fee rate ({default}) must fall within the global fee range by satisfying the condition: "
f"minimum ({minimum}) <= default ({default}) <= maximum ({maximum})")
policy_manager = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=self.contract_name,
proxy_name=self._proxy_deployer.contract_name)
tx_args = {}
if gas_limit:
tx_args.update({'gas': gas_limit}) # TODO: Gas management - 842
set_range_function = policy_manager.functions.setFeeRateRange(minimum, default, maximum)
set_range_receipt = self.blockchain.send_transaction(contract_function=set_range_function,
transacting_power=transacting_power,
payload=tx_args,
confirmations=confirmations)
return set_range_receipt
class SubscriptionManagerDeployer(BaseContractDeployer, OwnableContractMixin):
agency = SubscriptionManagerAgent
@ -909,136 +760,6 @@ class SubscriptionManagerDeployer(BaseContractDeployer, OwnableContractMixin):
return {self.deployment_steps[0]: deployment_receipt}
class StakingInterfaceRouterDeployer(OwnableContractMixin, ProxyContractDeployer):
contract_name = 'StakingInterfaceRouter'
# Overwrites rollback method from ProxyContractDeployer since StakingInterfaceRouter doesn't support rollback
def rollback(self, transacting_power: TransactingPower, gas_limit: int = None) -> dict:
raise NotImplementedError
class StakingInterfaceDeployer(BaseContractDeployer, UpgradeableContractMixin, OwnableContractMixin):
contract_name = 'StakingInterface'
deployment_steps = ('contract_deployment', 'router_deployment')
number_of_deployment_transactions = 2
_proxy_deployer = StakingInterfaceRouterDeployer
# _ownable = False # TODO: This contract is not truly ownable but we need the logic of the mixin to execute
def __init__(self, staking_interface: ChecksumAddress, *args, **kwargs):
super().__init__(*args, **kwargs)
token_contract_name = NucypherTokenDeployer.contract_name
self.token_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=token_contract_name)
staking_contract_name = StakingEscrowDeployer.contract_name
staking_proxy_name = StakingEscrowDeployer._proxy_deployer.contract_name
try:
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=staking_contract_name,
proxy_name=staking_proxy_name)
except self.registry.UnknownContract:
staking_contract_name = StakingEscrowDeployer.contract_name_stub
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=staking_contract_name,
proxy_name=staking_proxy_name)
policy_contract_name = PolicyManagerDeployer.contract_name
policy_proxy_name = PolicyManagerDeployer._proxy_deployer.contract_name
self.policy_contract = self.blockchain.get_contract_by_name(registry=self.registry,
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
self.threshold_staking_interface = staking_interface
def _deploy_essential(self,
transacting_power: TransactingPower,
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 NULL_ADDRESS
constructor_args = (self.token_contract.address,
self.staking_contract.address,
self.policy_contract.address,
worklock_address,
self.threshold_staking_interface)
contract, deployment_receipt = self.blockchain.deploy_contract(transacting_power,
self.registry,
self.contract_name,
*constructor_args,
gas_limit=gas_limit,
contract_version=contract_version,
confirmations=confirmations)
return contract, deployment_receipt
def deploy(self,
transacting_power: TransactingPower,
deployment_mode=FULL,
gas_limit: int = None,
progress=None,
contract_version: str = "latest",
ignore_deployed: bool = False,
confirmations: int = 0,
emitter = None
) -> dict:
"""
Deploys a new StakingInterface contract, and a new StakingInterfaceRouter, targeting the former.
This is meant to be called only once per general deployment.
"""
if deployment_mode not in (BARE, FULL):
raise ValueError(f"Invalid deployment mode ({deployment_mode})")
self.check_deployment_readiness(deployer_address=transacting_power.account,
contract_version=contract_version,
ignore_deployed=ignore_deployed)
# 1 - StakingInterface
if emitter:
emitter.message(f"\nNext Transaction: {self.contract_name} Contract Creation", color='blue', bold=True)
staking_interface_contract, deployment_receipt = self._deploy_essential(transacting_power=transacting_power,
contract_version=contract_version,
gas_limit=gas_limit,
confirmations=confirmations)
# This is the end of bare deployment.
if deployment_mode is BARE:
self._contract = staking_interface_contract
return self._finish_bare_deployment(deployment_receipt=deployment_receipt, progress=progress)
if progress:
progress.update(1)
# 2 - StakingInterfaceRouter
if emitter:
emitter.message(f"\nNext Transaction: {self._proxy_deployer.contract_name} deployment for {self.contract_name}", color='blue', bold=True)
router_deployer = self._proxy_deployer(registry=self.registry, target_contract=staking_interface_contract)
router_deployment_receipts = router_deployer.deploy(transacting_power=transacting_power, gas_limit=gas_limit)
router_deployment_receipt = router_deployment_receipts[router_deployer.deployment_steps[0]]
if progress:
progress.update(1)
# Gather the transaction receipts
ordered_receipts = (deployment_receipt, router_deployment_receipt)
deployment_receipts = dict(zip(self.deployment_steps, ordered_receipts))
self._contract = staking_interface_contract
return deployment_receipts
class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, OwnableContractMixin):
agency = AdjudicatorAgent

View File

@ -1,916 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "zeppelin/token/ERC20/SafeERC20.sol";
import "zeppelin/math/SafeMath.sol";
import "zeppelin/math/Math.sol";
import "zeppelin/utils/Address.sol";
import "contracts/lib/AdditionalMath.sol";
import "contracts/lib/SignatureVerifier.sol";
import "contracts/NuCypherToken.sol";
import "contracts/proxy/Upgradeable.sol";
import "contracts/IStakingEscrow.sol";
/**
* @title PolicyManager
* @notice Contract holds policy data and locks accrued policy fees
* @dev |v6.3.1|
*/
contract PolicyManager is Upgradeable {
using SafeERC20 for NuCypherToken;
using SafeMath for uint256;
using AdditionalMath for uint256;
using AdditionalMath for int256;
using AdditionalMath for uint16;
using Address for address payable;
event PolicyCreated(
bytes16 indexed policyId,
address indexed sponsor,
address indexed owner,
uint256 feeRate,
uint64 startTimestamp,
uint64 endTimestamp,
uint256 numberOfNodes
);
event ArrangementRevoked(
bytes16 indexed policyId,
address indexed sender,
address indexed node,
uint256 value
);
event RefundForArrangement(
bytes16 indexed policyId,
address indexed sender,
address indexed node,
uint256 value
);
event PolicyRevoked(bytes16 indexed policyId, address indexed sender, uint256 value);
event RefundForPolicy(bytes16 indexed policyId, address indexed sender, uint256 value);
event MinFeeRateSet(address indexed node, uint256 value);
// TODO #1501
// Range range
event FeeRateRangeSet(address indexed sender, uint256 min, uint256 defaultValue, uint256 max);
event Withdrawn(address indexed node, address indexed recipient, uint256 value);
struct ArrangementInfo {
address node;
uint256 indexOfDowntimePeriods;
uint16 lastRefundedPeriod;
}
struct Policy {
bool disabled;
address payable sponsor;
address owner;
uint128 feeRate;
uint64 startTimestamp;
uint64 endTimestamp;
uint256 reservedSlot1;
uint256 reservedSlot2;
uint256 reservedSlot3;
uint256 reservedSlot4;
uint256 reservedSlot5;
ArrangementInfo[] arrangements;
}
struct NodeInfo {
uint128 fee;
uint16 previousFeePeriod;
uint256 feeRate;
uint256 minFeeRate;
mapping (uint16 => int256) stub; // former slot for feeDelta
mapping (uint16 => int256) feeDelta;
}
// TODO used only for `delegateGetNodeInfo`, probably will be removed after #1512
struct MemoryNodeInfo {
uint128 fee;
uint16 previousFeePeriod;
uint256 feeRate;
uint256 minFeeRate;
}
struct Range {
uint128 min;
uint128 defaultValue;
uint128 max;
}
bytes16 internal constant RESERVED_POLICY_ID = bytes16(0);
address internal constant RESERVED_NODE = address(0);
uint256 internal constant MAX_BALANCE = uint256(type(uint128).max);
// controlled overflow to get max int256
int256 public constant DEFAULT_FEE_DELTA = int256((type(uint256).max) >> 1);
IStakingEscrow public immutable escrow;
uint32 public immutable genesisSecondsPerPeriod;
uint32 public immutable secondsPerPeriod;
mapping (bytes16 => Policy) public policies;
mapping (address => NodeInfo) public nodes;
Range public feeRateRange;
uint64 public resetTimestamp;
/**
* @notice Constructor sets address of the escrow contract
* @dev Put same address in both inputs variables except when migration is happening
* @param _escrowDispatcher Address of escrow dispatcher
* @param _escrowImplementation Address of escrow implementation
*/
constructor(IStakingEscrow _escrowDispatcher, IStakingEscrow _escrowImplementation) {
escrow = _escrowDispatcher;
// if the input address is not the StakingEscrow then calling `secondsPerPeriod` will throw error
uint32 localSecondsPerPeriod = _escrowImplementation.secondsPerPeriod();
require(localSecondsPerPeriod > 0);
secondsPerPeriod = localSecondsPerPeriod;
uint32 localgenesisSecondsPerPeriod = _escrowImplementation.genesisSecondsPerPeriod();
require(localgenesisSecondsPerPeriod > 0);
genesisSecondsPerPeriod = localgenesisSecondsPerPeriod;
// handle case when we deployed new StakingEscrow but not yet upgraded
if (_escrowDispatcher != _escrowImplementation) {
require(_escrowDispatcher.secondsPerPeriod() == localSecondsPerPeriod ||
_escrowDispatcher.secondsPerPeriod() == localgenesisSecondsPerPeriod);
}
}
/**
* @dev Checks that sender is the StakingEscrow contract
*/
modifier onlyEscrowContract()
{
require(msg.sender == address(escrow));
_;
}
/**
* @return Number of current period
*/
function getCurrentPeriod() public view returns (uint16) {
return uint16(block.timestamp / secondsPerPeriod);
}
/**
* @return Recalculate period value using new basis
*/
function recalculatePeriod(uint16 _period) internal view returns (uint16) {
return uint16(uint256(_period) * genesisSecondsPerPeriod / secondsPerPeriod);
}
/**
* @notice Register a node
* @param _node Node address
* @param _period Initial period
*/
function register(address _node, uint16 _period) external onlyEscrowContract {
NodeInfo storage nodeInfo = nodes[_node];
require(nodeInfo.previousFeePeriod == 0 && _period < getCurrentPeriod());
nodeInfo.previousFeePeriod = _period;
}
/**
* @notice Migrate from the old period length to the new one
* @param _node Node address
*/
function migrate(address _node) external onlyEscrowContract {
NodeInfo storage nodeInfo = nodes[_node];
// with previous period length any previousFeePeriod will be greater than current period
// this is a sign of not migrated node
require(nodeInfo.previousFeePeriod >= getCurrentPeriod());
nodeInfo.previousFeePeriod = recalculatePeriod(nodeInfo.previousFeePeriod);
nodeInfo.feeRate = 0;
}
/**
* @notice Set minimum, default & maximum fee rate for all stakers and all policies ('global fee range')
*/
// TODO # 1501
// function setFeeRateRange(Range calldata _range) external onlyOwner {
function setFeeRateRange(uint128 _min, uint128 _default, uint128 _max) external onlyOwner {
require(_min <= _default && _default <= _max);
feeRateRange = Range(_min, _default, _max);
emit FeeRateRangeSet(msg.sender, _min, _default, _max);
}
/**
* @notice Set the minimum acceptable fee rate (set by staker for their associated worker)
* @dev Input value must fall within `feeRateRange` (global fee range)
*/
function setMinFeeRate(uint256 _minFeeRate) external {
require(_minFeeRate >= feeRateRange.min &&
_minFeeRate <= feeRateRange.max,
"The staker's min fee rate must fall within the global fee range");
NodeInfo storage nodeInfo = nodes[msg.sender];
if (nodeInfo.minFeeRate == _minFeeRate) {
return;
}
nodeInfo.minFeeRate = _minFeeRate;
emit MinFeeRateSet(msg.sender, _minFeeRate);
}
/**
* @notice Get the minimum acceptable fee rate (set by staker for their associated worker)
*/
function getMinFeeRate(NodeInfo storage _nodeInfo) internal view returns (uint256) {
// if minFeeRate has not been set or chosen value falls outside the global fee range
// a default value is returned instead
if (_nodeInfo.minFeeRate == 0 ||
_nodeInfo.minFeeRate < feeRateRange.min ||
_nodeInfo.minFeeRate > feeRateRange.max) {
return feeRateRange.defaultValue;
} else {
return _nodeInfo.minFeeRate;
}
}
/**
* @notice Get the minimum acceptable fee rate (set by staker for their associated worker)
*/
function getMinFeeRate(address _node) public view returns (uint256) {
NodeInfo storage nodeInfo = nodes[_node];
return getMinFeeRate(nodeInfo);
}
/**
* @notice Create policy
* @dev Generate policy id before creation
* @param _policyId Policy id
* @param _policyOwner Policy owner. Zero address means sender is owner
* @param _endTimestamp End timestamp of the policy in seconds
* @param _nodes Nodes that will handle policy
*/
function createPolicy(
bytes16 _policyId,
address _policyOwner,
uint64 _endTimestamp,
address[] calldata _nodes
)
external payable
{
require(
_endTimestamp > block.timestamp &&
msg.value > 0
);
require(address(this).balance <= MAX_BALANCE);
uint16 currentPeriod = getCurrentPeriod();
uint16 endPeriod = uint16(_endTimestamp / secondsPerPeriod) + 1;
uint256 numberOfPeriods = endPeriod - currentPeriod;
uint128 feeRate = uint128(msg.value.div(_nodes.length) / numberOfPeriods);
require(feeRate > 0 && feeRate * numberOfPeriods * _nodes.length == msg.value);
Policy storage policy = createPolicy(_policyId, _policyOwner, _endTimestamp, feeRate, _nodes.length);
for (uint256 i = 0; i < _nodes.length; i++) {
address node = _nodes[i];
addFeeToNode(currentPeriod, endPeriod, node, feeRate, int256(uint256(feeRate)));
policy.arrangements.push(ArrangementInfo(node, 0, 0));
}
}
/**
* @notice Create multiple policies with the same owner, nodes and length
* @dev Generate policy ids before creation
* @param _policyIds Policy ids
* @param _policyOwner Policy owner. Zero address means sender is owner
* @param _endTimestamp End timestamp of all policies in seconds
* @param _nodes Nodes that will handle all policies
*/
function createPolicies(
bytes16[] calldata _policyIds,
address _policyOwner,
uint64 _endTimestamp,
address[] calldata _nodes
)
external payable
{
require(
_endTimestamp > block.timestamp &&
msg.value > 0 &&
_policyIds.length > 1
);
require(address(this).balance <= MAX_BALANCE);
uint16 currentPeriod = getCurrentPeriod();
uint16 endPeriod = uint16(_endTimestamp / secondsPerPeriod) + 1;
uint256 numberOfPeriods = endPeriod - currentPeriod;
uint128 feeRate = uint128(msg.value.div(_nodes.length) / numberOfPeriods / _policyIds.length);
require(feeRate > 0 && feeRate * numberOfPeriods * _nodes.length * _policyIds.length == msg.value);
for (uint256 i = 0; i < _policyIds.length; i++) {
Policy storage policy = createPolicy(_policyIds[i], _policyOwner, _endTimestamp, feeRate, _nodes.length);
for (uint256 j = 0; j < _nodes.length; j++) {
policy.arrangements.push(ArrangementInfo(_nodes[j], 0, 0));
}
}
int256 fee = int256(_policyIds.length * feeRate);
for (uint256 i = 0; i < _nodes.length; i++) {
address node = _nodes[i];
addFeeToNode(currentPeriod, endPeriod, node, feeRate, fee);
}
}
/**
* @notice Create policy
* @param _policyId Policy id
* @param _policyOwner Policy owner. Zero address means sender is owner
* @param _endTimestamp End timestamp of the policy in seconds
* @param _feeRate Fee rate for policy
* @param _nodesLength Number of nodes that will handle policy
*/
function createPolicy(
bytes16 _policyId,
address _policyOwner,
uint64 _endTimestamp,
uint128 _feeRate,
uint256 _nodesLength
)
internal returns (Policy storage policy)
{
policy = policies[_policyId];
require(
_policyId != RESERVED_POLICY_ID &&
policy.feeRate == 0 &&
!policy.disabled
);
policy.sponsor = payable(msg.sender);
policy.startTimestamp = uint64(block.timestamp);
policy.endTimestamp = _endTimestamp;
policy.feeRate = _feeRate;
if (_policyOwner != msg.sender && _policyOwner != address(0)) {
policy.owner = _policyOwner;
}
emit PolicyCreated(
_policyId,
msg.sender,
_policyOwner == address(0) ? msg.sender : _policyOwner,
_feeRate,
policy.startTimestamp,
policy.endTimestamp,
_nodesLength
);
}
/**
* @notice Increase fee rate for specified node
* @param _currentPeriod Current period
* @param _endPeriod End period of policy
* @param _node Node that will handle policy
* @param _feeRate Fee rate for one policy
* @param _overallFeeRate Fee rate for all policies
*/
function addFeeToNode(
uint16 _currentPeriod,
uint16 _endPeriod,
address _node,
uint128 _feeRate,
int256 _overallFeeRate
)
internal
{
require(_node != RESERVED_NODE);
NodeInfo storage nodeInfo = nodes[_node];
require(nodeInfo.previousFeePeriod != 0 &&
nodeInfo.previousFeePeriod < _currentPeriod &&
_feeRate >= getMinFeeRate(nodeInfo));
// Check default value for feeDelta
if (nodeInfo.feeDelta[_currentPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[_currentPeriod] = _overallFeeRate;
} else {
// Overflow protection removed, because ETH total supply less than uint255/int256
nodeInfo.feeDelta[_currentPeriod] += _overallFeeRate;
}
if (nodeInfo.feeDelta[_endPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[_endPeriod] = -_overallFeeRate;
} else {
nodeInfo.feeDelta[_endPeriod] -= _overallFeeRate;
}
// Reset to default value if needed
if (nodeInfo.feeDelta[_currentPeriod] == 0) {
nodeInfo.feeDelta[_currentPeriod] = DEFAULT_FEE_DELTA;
}
if (nodeInfo.feeDelta[_endPeriod] == 0) {
nodeInfo.feeDelta[_endPeriod] = DEFAULT_FEE_DELTA;
}
}
/**
* @notice Get policy owner
*/
function getPolicyOwner(bytes16 _policyId) public view returns (address) {
Policy storage policy = policies[_policyId];
return policy.owner == address(0) ? policy.sponsor : policy.owner;
}
/**
* @notice Call from StakingEscrow to update node info once per period.
* Set default `feeDelta` value for specified period and update node fee
* @param _node Node address
* @param _processedPeriod1 Processed period
* @param _processedPeriod2 Processed period
* @param _periodToSetDefault Period to set
*/
function ping(
address _node,
uint16 _processedPeriod1,
uint16 _processedPeriod2,
uint16 _periodToSetDefault
)
external onlyEscrowContract
{
NodeInfo storage node = nodes[_node];
// protection from calling not migrated node, see migrate()
require(node.previousFeePeriod <= getCurrentPeriod());
if (_processedPeriod1 != 0) {
updateFee(node, _processedPeriod1);
}
if (_processedPeriod2 != 0) {
updateFee(node, _processedPeriod2);
}
// This code increases gas cost for node in trade of decreasing cost for policy sponsor
if (_periodToSetDefault != 0 && node.feeDelta[_periodToSetDefault] == 0) {
node.feeDelta[_periodToSetDefault] = DEFAULT_FEE_DELTA;
}
}
/**
* @notice Update node fee
* @param _info Node info structure
* @param _period Processed period
*/
function updateFee(NodeInfo storage _info, uint16 _period) internal {
if (_info.previousFeePeriod == 0 || _period <= _info.previousFeePeriod) {
return;
}
for (uint16 i = _info.previousFeePeriod + 1; i <= _period; i++) {
int256 delta = _info.feeDelta[i];
if (delta == DEFAULT_FEE_DELTA) {
// gas refund
_info.feeDelta[i] = 0;
continue;
}
_info.feeRate = _info.feeRate.addSigned(delta);
// gas refund
_info.feeDelta[i] = 0;
}
_info.previousFeePeriod = _period;
_info.fee += uint128(_info.feeRate);
}
/**
* @notice Withdraw fee by node
*/
function withdraw() external returns (uint256) {
return withdraw(payable(msg.sender));
}
/**
* @notice Withdraw fee by node
* @param _recipient Recipient of the fee
*/
function withdraw(address payable _recipient) public returns (uint256) {
NodeInfo storage node = nodes[msg.sender];
uint256 fee = node.fee;
require(fee != 0);
node.fee = 0;
_recipient.sendValue(fee);
emit Withdrawn(msg.sender, _recipient, fee);
return fee;
}
/**
* @notice Calculate amount of refund
* @param _policy Policy
* @param _arrangement Arrangement
*/
function calculateRefundValue(Policy storage _policy, ArrangementInfo storage _arrangement)
internal view returns (uint256 refundValue, uint256 indexOfDowntimePeriods, uint16 lastRefundedPeriod)
{
uint16 policyStartPeriod = uint16(_policy.startTimestamp / secondsPerPeriod);
uint16 maxPeriod = AdditionalMath.min16(getCurrentPeriod(), uint16(_policy.endTimestamp / secondsPerPeriod));
uint16 minPeriod = AdditionalMath.max16(policyStartPeriod, _arrangement.lastRefundedPeriod);
uint16 downtimePeriods = 0;
uint256 length = escrow.getPastDowntimeLength(_arrangement.node);
uint256 initialIndexOfDowntimePeriods;
if (_arrangement.lastRefundedPeriod == 0) {
initialIndexOfDowntimePeriods = escrow.findIndexOfPastDowntime(_arrangement.node, policyStartPeriod);
} else {
initialIndexOfDowntimePeriods = _arrangement.indexOfDowntimePeriods;
}
for (indexOfDowntimePeriods = initialIndexOfDowntimePeriods;
indexOfDowntimePeriods < length;
indexOfDowntimePeriods++)
{
(uint16 startPeriod, uint16 endPeriod) =
escrow.getPastDowntime(_arrangement.node, indexOfDowntimePeriods);
if (startPeriod > maxPeriod) {
break;
} else if (endPeriod < minPeriod) {
continue;
}
downtimePeriods += AdditionalMath.min16(maxPeriod, endPeriod)
.sub16(AdditionalMath.max16(minPeriod, startPeriod)) + 1;
if (maxPeriod <= endPeriod) {
break;
}
}
uint16 lastCommittedPeriod = escrow.getLastCommittedPeriod(_arrangement.node);
if (indexOfDowntimePeriods == length && lastCommittedPeriod < maxPeriod) {
// Overflow protection removed:
// lastCommittedPeriod < maxPeriod and minPeriod <= maxPeriod + 1
downtimePeriods += maxPeriod - AdditionalMath.max16(minPeriod - 1, lastCommittedPeriod);
}
refundValue = _policy.feeRate * downtimePeriods;
lastRefundedPeriod = maxPeriod + 1;
}
/**
* @notice Revoke/refund arrangement/policy by the sponsor
* @param _policyId Policy id
* @param _node Node that will be excluded or RESERVED_NODE if full policy should be used
( @param _forceRevoke Force revoke arrangement/policy
*/
function refundInternal(bytes16 _policyId, address _node, bool _forceRevoke)
internal returns (uint256 refundValue)
{
refundValue = 0;
Policy storage policy = policies[_policyId];
require(!policy.disabled && policy.startTimestamp >= resetTimestamp);
uint16 endPeriod = uint16(policy.endTimestamp / secondsPerPeriod) + 1;
uint256 numberOfActive = policy.arrangements.length;
uint256 i = 0;
for (; i < policy.arrangements.length; i++) {
ArrangementInfo storage arrangement = policy.arrangements[i];
address node = arrangement.node;
if (node == RESERVED_NODE || _node != RESERVED_NODE && _node != node) {
numberOfActive--;
continue;
}
uint256 nodeRefundValue;
(nodeRefundValue, arrangement.indexOfDowntimePeriods, arrangement.lastRefundedPeriod) =
calculateRefundValue(policy, arrangement);
if (_forceRevoke) {
NodeInfo storage nodeInfo = nodes[node];
// Check default value for feeDelta
uint16 lastRefundedPeriod = arrangement.lastRefundedPeriod;
if (nodeInfo.feeDelta[lastRefundedPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[lastRefundedPeriod] = -int256(uint256(policy.feeRate));
} else {
nodeInfo.feeDelta[lastRefundedPeriod] -= int256(uint256(policy.feeRate));
}
if (nodeInfo.feeDelta[endPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[endPeriod] = int256(uint256(policy.feeRate));
} else {
nodeInfo.feeDelta[endPeriod] += int256(uint256(policy.feeRate));
}
// Reset to default value if needed
if (nodeInfo.feeDelta[lastRefundedPeriod] == 0) {
nodeInfo.feeDelta[lastRefundedPeriod] = DEFAULT_FEE_DELTA;
}
if (nodeInfo.feeDelta[endPeriod] == 0) {
nodeInfo.feeDelta[endPeriod] = DEFAULT_FEE_DELTA;
}
nodeRefundValue += uint256(endPeriod - lastRefundedPeriod) * policy.feeRate;
}
if (_forceRevoke || arrangement.lastRefundedPeriod >= endPeriod) {
arrangement.node = RESERVED_NODE;
arrangement.indexOfDowntimePeriods = 0;
arrangement.lastRefundedPeriod = 0;
numberOfActive--;
emit ArrangementRevoked(_policyId, msg.sender, node, nodeRefundValue);
} else {
emit RefundForArrangement(_policyId, msg.sender, node, nodeRefundValue);
}
refundValue += nodeRefundValue;
if (_node != RESERVED_NODE) {
break;
}
}
address payable policySponsor = policy.sponsor;
if (_node == RESERVED_NODE) {
if (numberOfActive == 0) {
policy.disabled = true;
// gas refund
policy.sponsor = payable(address(0));
policy.owner = address(0);
policy.feeRate = 0;
policy.startTimestamp = 0;
policy.endTimestamp = 0;
emit PolicyRevoked(_policyId, msg.sender, refundValue);
} else {
emit RefundForPolicy(_policyId, msg.sender, refundValue);
}
} else {
// arrangement not found
require(i < policy.arrangements.length);
}
if (refundValue > 0) {
policySponsor.sendValue(refundValue);
}
}
/**
* @notice Calculate amount of refund
* @param _policyId Policy id
* @param _node Node or RESERVED_NODE if all nodes should be used
*/
function calculateRefundValueInternal(bytes16 _policyId, address _node)
internal view returns (uint256 refundValue)
{
refundValue = 0;
Policy storage policy = policies[_policyId];
require((policy.owner == msg.sender || policy.sponsor == msg.sender) && !policy.disabled);
uint256 i = 0;
for (; i < policy.arrangements.length; i++) {
ArrangementInfo storage arrangement = policy.arrangements[i];
if (arrangement.node == RESERVED_NODE || _node != RESERVED_NODE && _node != arrangement.node) {
continue;
}
(uint256 nodeRefundValue,,) = calculateRefundValue(policy, arrangement);
refundValue += nodeRefundValue;
if (_node != RESERVED_NODE) {
break;
}
}
if (_node != RESERVED_NODE) {
// arrangement not found
require(i < policy.arrangements.length);
}
}
/**
* @notice Revoke policy by the sponsor
* @param _policyId Policy id
*/
function revokePolicy(bytes16 _policyId) external returns (uint256 refundValue) {
require(getPolicyOwner(_policyId) == msg.sender);
return refundInternal(_policyId, RESERVED_NODE, true);
}
/**
* @notice Revoke arrangement by the sponsor
* @param _policyId Policy id
* @param _node Node that will be excluded
*/
function revokeArrangement(bytes16 _policyId, address _node)
external returns (uint256 refundValue)
{
require(_node != RESERVED_NODE);
require(getPolicyOwner(_policyId) == msg.sender);
return refundInternal(_policyId, _node, true);
}
/**
* @notice Get unsigned hash for revocation
* @param _policyId Policy id
* @param _node Node that will be excluded
* @return Revocation hash, EIP191 version 0x45 ('E')
*/
function getRevocationHash(bytes16 _policyId, address _node) public view returns (bytes32) {
return SignatureVerifier.hashEIP191(abi.encodePacked(_policyId, _node), bytes1(0x45));
}
/**
* @notice Check correctness of signature
* @param _policyId Policy id
* @param _node Node that will be excluded, zero address if whole policy will be revoked
* @param _signature Signature of owner
*/
function checkOwnerSignature(bytes16 _policyId, address _node, bytes memory _signature) internal view {
bytes32 hash = getRevocationHash(_policyId, _node);
address recovered = SignatureVerifier.recover(hash, _signature);
require(getPolicyOwner(_policyId) == recovered);
}
/**
* @notice Revoke policy or arrangement using owner's signature
* @param _policyId Policy id
* @param _node Node that will be excluded, zero address if whole policy will be revoked
* @param _signature Signature of owner, EIP191 version 0x45 ('E')
*/
function revoke(bytes16 _policyId, address _node, bytes calldata _signature)
external returns (uint256 refundValue)
{
checkOwnerSignature(_policyId, _node, _signature);
return refundInternal(_policyId, _node, true);
}
/**
* @notice Refund part of fee by the sponsor
* @param _policyId Policy id
*/
function refund(bytes16 _policyId) external {
Policy storage policy = policies[_policyId];
require(policy.owner == msg.sender || policy.sponsor == msg.sender);
refundInternal(_policyId, RESERVED_NODE, false);
}
/**
* @notice Refund part of one node's fee by the sponsor
* @param _policyId Policy id
* @param _node Node address
*/
function refund(bytes16 _policyId, address _node)
external returns (uint256 refundValue)
{
require(_node != RESERVED_NODE);
Policy storage policy = policies[_policyId];
require(policy.owner == msg.sender || policy.sponsor == msg.sender);
return refundInternal(_policyId, _node, false);
}
/**
* @notice Calculate amount of refund
* @param _policyId Policy id
*/
function calculateRefundValue(bytes16 _policyId)
external view returns (uint256 refundValue)
{
return calculateRefundValueInternal(_policyId, RESERVED_NODE);
}
/**
* @notice Calculate amount of refund
* @param _policyId Policy id
* @param _node Node
*/
function calculateRefundValue(bytes16 _policyId, address _node)
external view returns (uint256 refundValue)
{
require(_node != RESERVED_NODE);
return calculateRefundValueInternal(_policyId, _node);
}
/**
* @notice Get number of arrangements in the policy
* @param _policyId Policy id
*/
function getArrangementsLength(bytes16 _policyId) external view returns (uint256) {
return policies[_policyId].arrangements.length;
}
/**
* @notice Get information about staker's fee rate
* @param _node Address of staker
* @param _period Period to get fee delta
*/
function getNodeFeeDelta(address _node, uint16 _period)
// TODO "virtual" only for tests, probably will be removed after #1512
public view virtual returns (int256)
{
// TODO remove after upgrade #2579
if (_node == RESERVED_NODE && _period == 11) {
return 55;
}
return nodes[_node].feeDelta[_period];
}
/**
* @notice Return the information about arrangement
*/
function getArrangementInfo(bytes16 _policyId, uint256 _index)
// TODO change to structure when ABIEncoderV2 is released (#1501)
// public view returns (ArrangementInfo)
external view returns (address node, uint256 indexOfDowntimePeriods, uint16 lastRefundedPeriod)
{
ArrangementInfo storage info = policies[_policyId].arrangements[_index];
node = info.node;
indexOfDowntimePeriods = info.indexOfDowntimePeriods;
lastRefundedPeriod = info.lastRefundedPeriod;
}
/**
* @dev Get Policy structure by delegatecall
*/
function delegateGetPolicy(address _target, bytes16 _policyId)
internal returns (Policy memory result)
{
bytes32 memoryAddress = delegateGetData(_target, this.policies.selector, 1, bytes32(_policyId), 0);
assembly {
result := memoryAddress
}
}
/**
* @dev Get ArrangementInfo structure by delegatecall
*/
function delegateGetArrangementInfo(address _target, bytes16 _policyId, uint256 _index)
internal returns (ArrangementInfo memory result)
{
bytes32 memoryAddress = delegateGetData(
_target, this.getArrangementInfo.selector, 2, bytes32(_policyId), bytes32(_index));
assembly {
result := memoryAddress
}
}
/**
* @dev Get NodeInfo structure by delegatecall
*/
function delegateGetNodeInfo(address _target, address _node)
internal returns (MemoryNodeInfo memory result)
{
bytes32 memoryAddress = delegateGetData(_target, this.nodes.selector, 1, bytes32(uint256(uint160(_node))), 0);
assembly {
result := memoryAddress
}
}
/**
* @dev Get feeRateRange structure by delegatecall
*/
function delegateGetFeeRateRange(address _target) internal returns (Range memory result) {
bytes32 memoryAddress = delegateGetData(_target, this.feeRateRange.selector, 0, 0, 0);
assembly {
result := memoryAddress
}
}
/// @dev the `onlyWhileUpgrading` modifier works through a call to the parent `verifyState`
function verifyState(address _testTarget) public override virtual {
super.verifyState(_testTarget);
require(uint64(delegateGet(_testTarget, this.resetTimestamp.selector)) == resetTimestamp);
Range memory rangeToCheck = delegateGetFeeRateRange(_testTarget);
require(feeRateRange.min == rangeToCheck.min &&
feeRateRange.defaultValue == rangeToCheck.defaultValue &&
feeRateRange.max == rangeToCheck.max);
Policy storage policy = policies[RESERVED_POLICY_ID];
Policy memory policyToCheck = delegateGetPolicy(_testTarget, RESERVED_POLICY_ID);
require(policyToCheck.sponsor == policy.sponsor &&
policyToCheck.owner == policy.owner &&
policyToCheck.feeRate == policy.feeRate &&
policyToCheck.startTimestamp == policy.startTimestamp &&
policyToCheck.endTimestamp == policy.endTimestamp &&
policyToCheck.disabled == policy.disabled);
require(delegateGet(_testTarget, this.getArrangementsLength.selector, RESERVED_POLICY_ID) ==
policy.arrangements.length);
if (policy.arrangements.length > 0) {
ArrangementInfo storage arrangement = policy.arrangements[0];
ArrangementInfo memory arrangementToCheck = delegateGetArrangementInfo(
_testTarget, RESERVED_POLICY_ID, 0);
require(arrangementToCheck.node == arrangement.node &&
arrangementToCheck.indexOfDowntimePeriods == arrangement.indexOfDowntimePeriods &&
arrangementToCheck.lastRefundedPeriod == arrangement.lastRefundedPeriod);
}
NodeInfo storage nodeInfo = nodes[RESERVED_NODE];
MemoryNodeInfo memory nodeInfoToCheck = delegateGetNodeInfo(_testTarget, RESERVED_NODE);
require(nodeInfoToCheck.fee == nodeInfo.fee &&
nodeInfoToCheck.feeRate == nodeInfo.feeRate &&
nodeInfoToCheck.previousFeePeriod == nodeInfo.previousFeePeriod &&
nodeInfoToCheck.minFeeRate == nodeInfo.minFeeRate);
require(int256(delegateGet(_testTarget, this.getNodeFeeDelta.selector,
bytes32(bytes20(RESERVED_NODE)), bytes32(uint256(11)))) == getNodeFeeDelta(RESERVED_NODE, 11));
}
/// @dev the `onlyWhileUpgrading` modifier works through a call to the parent `finishUpgrade`
function finishUpgrade(address _target) public override virtual {
super.finishUpgrade(_target);
if (resetTimestamp == 0) {
resetTimestamp = uint64(block.timestamp);
}
// Create fake Policy and NodeInfo to use them in verifyState(address)
Policy storage policy = policies[RESERVED_POLICY_ID];
policy.sponsor = payable(msg.sender);
policy.owner = address(this);
policy.startTimestamp = 1;
policy.endTimestamp = 2;
policy.feeRate = 3;
policy.disabled = true;
policy.arrangements.push(ArrangementInfo(RESERVED_NODE, 11, 22));
NodeInfo storage nodeInfo = nodes[RESERVED_NODE];
nodeInfo.fee = 100;
nodeInfo.feeRate = 33;
nodeInfo.previousFeePeriod = 44;
nodeInfo.feeDelta[11] = 55;
nodeInfo.minFeeRate = 777;
}
}

View File

@ -1,147 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "zeppelin/ownership/Ownable.sol";
import "zeppelin/utils/Address.sol";
import "zeppelin/token/ERC20/SafeERC20.sol";
import "contracts/staking_contracts/StakingInterface.sol";
import "zeppelin/proxy/Initializable.sol";
/**
* @notice Router for accessing interface contract
*/
contract StakingInterfaceRouter is Ownable {
BaseStakingInterface public target;
/**
* @param _target Address of the interface contract
*/
constructor(BaseStakingInterface _target) {
require(address(_target.token()) != address(0));
target = _target;
}
/**
* @notice Upgrade interface
* @param _target New contract address
*/
function upgrade(BaseStakingInterface _target) external onlyOwner {
require(address(_target.token()) != address(0));
target = _target;
}
}
/**
* @notice Internal base class for AbstractStakingContract and InitializableStakingContract
*/
abstract contract RawStakingContract {
using Address for address;
/**
* @dev Returns address of StakingInterfaceRouter
*/
function router() public view virtual returns (StakingInterfaceRouter);
/**
* @dev Checks permission for calling fallback function
*/
function isFallbackAllowed() public virtual returns (bool);
/**
* @dev Withdraw tokens from staking contract
*/
function withdrawTokens(uint256 _value) public virtual;
/**
* @dev Withdraw ETH from staking contract
*/
function withdrawETH() public virtual;
receive() external payable {}
/**
* @dev Function sends all requests to the target contract
*/
fallback() external payable {
require(isFallbackAllowed());
address target = address(router().target());
require(target.isContract());
// execute requested function from target contract
(bool callSuccess, ) = target.delegatecall(msg.data);
if (callSuccess) {
// copy result of the request to the return data
// we can use the second return value from `delegatecall` (bytes memory)
// but it will consume a little more gas
assembly {
returndatacopy(0x0, 0x0, returndatasize())
return(0x0, returndatasize())
}
} else {
revert();
}
}
}
/**
* @notice Base class for any staking contract (not usable with openzeppelin proxy)
* @dev Implement `isFallbackAllowed()` or override fallback function
* Implement `withdrawTokens(uint256)` and `withdrawETH()` functions
*/
abstract contract AbstractStakingContract is RawStakingContract {
StakingInterfaceRouter immutable router_;
NuCypherToken public immutable token;
/**
* @param _router Interface router contract address
*/
constructor(StakingInterfaceRouter _router) {
router_ = _router;
NuCypherToken localToken = _router.target().token();
require(address(localToken) != address(0));
token = localToken;
}
/**
* @dev Returns address of StakingInterfaceRouter
*/
function router() public view override returns (StakingInterfaceRouter) {
return router_;
}
}
/**
* @notice Base class for any staking contract usable with openzeppelin proxy
* @dev Implement `isFallbackAllowed()` or override fallback function
* Implement `withdrawTokens(uint256)` and `withdrawETH()` functions
*/
abstract contract InitializableStakingContract is Initializable, RawStakingContract {
StakingInterfaceRouter router_;
NuCypherToken public token;
/**
* @param _router Interface router contract address
*/
function initialize(StakingInterfaceRouter _router) public initializer {
router_ = _router;
NuCypherToken localToken = _router.target().token();
require(address(localToken) != address(0));
token = localToken;
}
/**
* @dev Returns address of StakingInterfaceRouter
*/
function router() public view override returns (StakingInterfaceRouter) {
return router_;
}
}

View File

@ -1,323 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "zeppelin/ownership/Ownable.sol";
import "contracts/staking_contracts/AbstractStakingContract.sol";
/**
* @notice Contract acts as delegate for sub-stakers
**/
contract PoolingStakingContractV2 is InitializableStakingContract, Ownable {
using Address for address payable;
using SafeERC20 for NuCypherToken;
event TokensDeposited(
address indexed sender,
uint256 value,
uint256 depositedTokens
);
event TokensWithdrawn(
address indexed sender,
uint256 value,
uint256 depositedTokens
);
event ETHWithdrawn(address indexed sender, uint256 value);
event WorkerOwnerSet(address indexed sender, address indexed workerOwner);
struct Delegator {
uint256 depositedTokens;
uint256 withdrawnReward;
uint256 withdrawnETH;
}
/**
* Defines base fraction and precision of worker fraction.
* E.g., for a value of 10000, a worker fraction of 100 represents 1% of reward (100/10000)
*/
uint256 public constant BASIS_FRACTION = 10000;
IStakingEscrow public escrow;
address public workerOwner;
uint256 public totalDepositedTokens;
uint256 public totalWithdrawnReward;
uint256 public totalWithdrawnETH;
uint256 workerFraction;
uint256 public workerWithdrawnReward;
mapping(address => Delegator) public delegators;
/**
* @notice Initialize function for using with OpenZeppelin proxy
* @param _workerFraction Share of token reward that worker node owner will get.
* Use value up to BASIS_FRACTION (10000), if _workerFraction = BASIS_FRACTION -> means 100% reward as commission.
* For example, 100 worker fraction is 1% of reward
* @param _router StakingInterfaceRouter address
* @param _workerOwner Owner of worker node, only this address can withdraw worker commission
*/
function initialize(
uint256 _workerFraction,
StakingInterfaceRouter _router,
address _workerOwner
) external initializer {
require(_workerOwner != address(0) && _workerFraction <= BASIS_FRACTION);
InitializableStakingContract.initialize(_router);
_transferOwnership(msg.sender);
escrow = _router.target().escrow();
workerFraction = _workerFraction;
workerOwner = _workerOwner;
emit WorkerOwnerSet(msg.sender, _workerOwner);
}
/**
* @notice withdrawAll() is allowed
*/
function isWithdrawAllAllowed() public view returns (bool) {
// no tokens in StakingEscrow contract which belong to pool
return escrow.getAllTokens(address(this)) == 0;
}
/**
* @notice deposit() is allowed
*/
function isDepositAllowed() public view returns (bool) {
// tokens which directly belong to pool
uint256 freeTokens = token.balanceOf(address(this));
// no sub-stakes and no earned reward
return isWithdrawAllAllowed() && freeTokens == totalDepositedTokens;
}
/**
* @notice Set worker owner address
*/
function setWorkerOwner(address _workerOwner) external onlyOwner {
workerOwner = _workerOwner;
emit WorkerOwnerSet(msg.sender, _workerOwner);
}
/**
* @notice Calculate worker's fraction depending on deposited tokens
* Override to implement dynamic worker fraction.
*/
function getWorkerFraction() public view virtual returns (uint256) {
return workerFraction;
}
/**
* @notice Transfer tokens as delegator
* @param _value Amount of tokens to transfer
*/
function depositTokens(uint256 _value) external {
require(isDepositAllowed(), "Deposit must be enabled");
require(_value > 0, "Value must be not empty");
totalDepositedTokens += _value;
Delegator storage delegator = delegators[msg.sender];
delegator.depositedTokens += _value;
token.safeTransferFrom(msg.sender, address(this), _value);
emit TokensDeposited(msg.sender, _value, delegator.depositedTokens);
}
/**
* @notice Get available reward for all delegators and owner
*/
function getAvailableReward() public view returns (uint256) {
// locked + unlocked tokens in StakingEscrow contract which belong to pool
uint256 stakedTokens = escrow.getAllTokens(address(this));
// tokens which directly belong to pool
uint256 freeTokens = token.balanceOf(address(this));
// tokens in excess of the initially deposited
uint256 reward = stakedTokens + freeTokens - totalDepositedTokens;
// check how many of reward tokens belong directly to pool
if (reward > freeTokens) {
return freeTokens;
}
return reward;
}
/**
* @notice Get cumulative reward.
* Available and withdrawn reward together to use in delegator/owner reward calculations
*/
function getCumulativeReward() public view returns (uint256) {
return getAvailableReward() + totalWithdrawnReward;
}
/**
* @notice Get available reward in tokens for worker node owner
*/
function getAvailableWorkerReward() public view returns (uint256) {
// total current and historical reward
uint256 reward = getCumulativeReward();
// calculate total reward for worker including historical reward
uint256 maxAllowableReward;
// usual case
if (totalDepositedTokens != 0) {
uint256 fraction = getWorkerFraction();
maxAllowableReward = reward * fraction / BASIS_FRACTION;
// special case when there are no delegators
} else {
maxAllowableReward = reward;
}
// check that worker has any new reward
if (maxAllowableReward > workerWithdrawnReward) {
return maxAllowableReward - workerWithdrawnReward;
}
return 0;
}
/**
* @notice Get available reward in tokens for delegator
*/
function getAvailableDelegatorReward(address _delegator) public view returns (uint256) {
// special case when there are no delegators
if (totalDepositedTokens == 0) {
return 0;
}
// total current and historical reward
uint256 reward = getCumulativeReward();
Delegator storage delegator = delegators[_delegator];
uint256 fraction = getWorkerFraction();
// calculate total reward for delegator including historical reward
// excluding worker share
uint256 maxAllowableReward = reward * delegator.depositedTokens * (BASIS_FRACTION - fraction)
/ (totalDepositedTokens * BASIS_FRACTION);
// check that worker has any new reward
if (maxAllowableReward > delegator.withdrawnReward) {
return maxAllowableReward - delegator.withdrawnReward;
}
return 0;
}
/**
* @notice Withdraw reward in tokens to worker node owner
*/
function withdrawWorkerReward() external {
require(msg.sender == workerOwner);
uint256 balance = token.balanceOf(address(this));
uint256 availableReward = getAvailableWorkerReward();
if (availableReward > balance) {
availableReward = balance;
}
require(
availableReward > 0,
"There is no available reward to withdraw"
);
workerWithdrawnReward += availableReward;
totalWithdrawnReward += availableReward;
token.safeTransfer(msg.sender, availableReward);
emit TokensWithdrawn(msg.sender, availableReward, 0);
}
/**
* @notice Withdraw reward to delegator
* @param _value Amount of tokens to withdraw
*/
function withdrawTokens(uint256 _value) public override {
uint256 balance = token.balanceOf(address(this));
require(_value <= balance, "Not enough tokens in the contract");
Delegator storage delegator = delegators[msg.sender];
uint256 availableReward = getAvailableDelegatorReward(msg.sender);
require( _value <= availableReward, "Requested amount of tokens exceeded allowed portion");
delegator.withdrawnReward += _value;
totalWithdrawnReward += _value;
token.safeTransfer(msg.sender, _value);
emit TokensWithdrawn(msg.sender, _value, delegator.depositedTokens);
}
/**
* @notice Withdraw reward, deposit and fee to delegator
*/
function withdrawAll() public {
require(isWithdrawAllAllowed(), "Withdraw deposit and reward must be enabled");
uint256 balance = token.balanceOf(address(this));
Delegator storage delegator = delegators[msg.sender];
uint256 availableReward = getAvailableDelegatorReward(msg.sender);
uint256 value = availableReward + delegator.depositedTokens;
require(value <= balance, "Not enough tokens in the contract");
// TODO remove double reading: availableReward and availableWorkerReward use same calls to external contracts
uint256 availableWorkerReward = getAvailableWorkerReward();
// potentially could be less then due reward
uint256 availableETH = getAvailableDelegatorETH(msg.sender);
// prevent losing reward for worker after calculations
uint256 workerReward = availableWorkerReward * delegator.depositedTokens / totalDepositedTokens;
if (workerReward > 0) {
require(value + workerReward <= balance, "Not enough tokens in the contract");
token.safeTransfer(workerOwner, workerReward);
emit TokensWithdrawn(workerOwner, workerReward, 0);
}
uint256 withdrawnToDecrease = workerWithdrawnReward * delegator.depositedTokens / totalDepositedTokens;
workerWithdrawnReward -= withdrawnToDecrease;
totalWithdrawnReward -= withdrawnToDecrease + delegator.withdrawnReward;
totalDepositedTokens -= delegator.depositedTokens;
delegator.withdrawnReward = 0;
delegator.depositedTokens = 0;
token.safeTransfer(msg.sender, value);
emit TokensWithdrawn(msg.sender, value, 0);
totalWithdrawnETH -= delegator.withdrawnETH;
delegator.withdrawnETH = 0;
if (availableETH > 0) {
emit ETHWithdrawn(msg.sender, availableETH);
payable(msg.sender).sendValue(availableETH);
}
}
/**
* @notice Get available ether for delegator
*/
function getAvailableDelegatorETH(address _delegator) public view returns (uint256) {
Delegator storage delegator = delegators[_delegator];
uint256 balance = address(this).balance;
// ETH balance + already withdrawn
balance += totalWithdrawnETH;
uint256 maxAllowableETH = balance * delegator.depositedTokens / totalDepositedTokens;
uint256 availableETH = maxAllowableETH - delegator.withdrawnETH;
if (availableETH > balance) {
availableETH = balance;
}
return availableETH;
}
/**
* @notice Withdraw available amount of ETH to delegator
*/
function withdrawETH() public override {
Delegator storage delegator = delegators[msg.sender];
uint256 availableETH = getAvailableDelegatorETH(msg.sender);
require(availableETH > 0, "There is no available ETH to withdraw");
delegator.withdrawnETH += availableETH;
totalWithdrawnETH += availableETH;
emit ETHWithdrawn(msg.sender, availableETH);
payable(msg.sender).sendValue(availableETH);
}
/**
* @notice Calling fallback function is allowed only for the owner
*/
function isFallbackAllowed() public override view returns (bool) {
return msg.sender == owner();
}
}

View File

@ -1,203 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "contracts/staking_contracts/AbstractStakingContract.sol";
import "contracts/NuCypherToken.sol";
import "contracts/IStakingEscrow.sol";
import "contracts/PolicyManager.sol";
import "contracts/WorkLock.sol";
import "threshold/IStaking.sol";
/**
* @notice Base StakingInterface
*/
contract BaseStakingInterface {
address public immutable stakingInterfaceAddress;
NuCypherToken public immutable token;
IStakingEscrow public immutable escrow;
PolicyManager public immutable policyManager;
WorkLock public immutable workLock;
IStaking public immutable tStaking;
/**
* @notice Constructor sets addresses of the contracts
* @param _token Token contract
* @param _escrow Escrow contract
* @param _policyManager PolicyManager contract
* @param _workLock WorkLock contract
* @param _tStaking Threshold TokenStaking contract
*/
constructor(
NuCypherToken _token,
IStakingEscrow _escrow,
PolicyManager _policyManager,
WorkLock _workLock,
IStaking _tStaking
) {
require(_token.totalSupply() > 0 &&
_escrow.token() == _token &&
_policyManager.secondsPerPeriod() > 0 &&
_tStaking.stakedNu(address(0)) == 0 &&
// in case there is no worklock contract
(address(_workLock) == address(0) || _workLock.boostingRefund() > 0));
token = _token;
escrow = _escrow;
policyManager = _policyManager;
workLock = _workLock;
tStaking = _tStaking;
stakingInterfaceAddress = address(this);
}
/**
* @dev Checks executing through delegate call
*/
modifier onlyDelegateCall()
{
require(stakingInterfaceAddress != address(this));
_;
}
/**
* @dev Checks the existence of the worklock contract
*/
modifier workLockSet()
{
require(address(workLock) != address(0));
_;
}
}
/**
* @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, use immutable fields.
* @dev |v1.9.1|
*/
contract StakingInterface is BaseStakingInterface {
event WithdrawnAsStaker(address indexed sender, uint256 value);
event PolicyFeeWithdrawn(address indexed sender, uint256 value);
event MinFeeRateSet(address indexed sender, uint256 value);
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);
event ThresholdNUStaked(
address indexed sender,
address indexed operator,
address beneficiary,
address authorizer
);
event ThresholdNUUnstaked(address indexed sender, address indexed operator, uint96 amount);
/**
* @notice Constructor sets addresses of the contracts
* @param _token Token contract
* @param _escrow Escrow contract
* @param _policyManager PolicyManager contract
* @param _workLock WorkLock contract
* @param _tStaking Threshold TokenStaking contract
*/
constructor(
NuCypherToken _token,
IStakingEscrow _escrow,
PolicyManager _policyManager,
WorkLock _workLock,
IStaking _tStaking
)
BaseStakingInterface(_token, _escrow, _policyManager, _workLock, _tStaking)
{
}
/**
* @notice Withdraw available amount of tokens from the staking escrow to the staking contract
* @param _value Amount of token to withdraw
*/
function withdrawAsStaker(uint256 _value) public onlyDelegateCall {
escrow.withdraw(_value);
emit WithdrawnAsStaker(msg.sender, _value);
}
/**
* @notice Withdraw available policy fees from the policy manager to the staking contract
*/
function withdrawPolicyFee() public onlyDelegateCall {
uint256 value = policyManager.withdraw();
emit PolicyFeeWithdrawn(msg.sender, value);
}
/**
* @notice Set the minimum fee that the staker will accept in the policy manager contract
*/
function setMinFeeRate(uint256 _minFeeRate) public onlyDelegateCall {
policyManager.setMinFeeRate(_minFeeRate);
emit MinFeeRateSet(msg.sender, _minFeeRate);
}
/**
* @notice Bid for tokens by transferring ETH
*/
function bid(uint256 _value) public payable onlyDelegateCall workLockSet {
workLock.bid{value: _value}();
emit Bid(msg.sender, _value);
}
/**
* @notice Cancel bid and refund deposited ETH
*/
function cancelBid() public onlyDelegateCall workLockSet {
workLock.cancelBid();
emit BidCanceled(msg.sender);
}
/**
* @notice Withdraw compensation after force refund
*/
function withdrawCompensation() public onlyDelegateCall workLockSet {
workLock.withdrawCompensation();
emit CompensationWithdrawn(msg.sender);
}
/**
* @notice Claimed tokens will be deposited and locked as stake in the StakingEscrow contract
*/
function claim() public onlyDelegateCall workLockSet {
uint256 claimedTokens = workLock.claim();
emit Claimed(msg.sender, claimedTokens);
}
/**
* @notice Refund ETH for the completed work
*/
function refund() public onlyDelegateCall workLockSet {
uint256 refundETH = workLock.refund();
emit Refund(msg.sender, refundETH);
}
/**
* @notice Copies delegation from the legacy NU staking contract to T staking contract,
* additionally appointing beneficiary and authorizer roles.
*/
function stakeNu(
address operator,
address payable beneficiary,
address authorizer
) external onlyDelegateCall {
tStaking.stakeNu(operator, beneficiary, authorizer);
emit ThresholdNUStaked(msg.sender, operator, beneficiary, authorizer);
}
/**
* @notice Reduces cached legacy NU stake amount by the provided amount.
*/
function unstakeNu(address operator, uint96 amount) external onlyDelegateCall {
tStaking.unstakeNu(operator, amount);
emit ThresholdNUUnstaked(msg.sender, operator, amount);
}
}

View File

@ -23,7 +23,7 @@ from unittest.mock import patch
from eth_tester.exceptions import ValidationError
from nucypher_core import NodeMetadata
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent
from nucypher.blockchain.eth.agents import ContractAgency
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.characters.lawful import Alice, Ursula
from nucypher.config.constants import TEMPORARY_DOMAIN
@ -159,29 +159,3 @@ class Amonia(Alice):
"""
with patch("nucypher.policy.policies.Policy._publish", self.grant_without_paying):
return self.grant_without_paying(*args, **kwargs)
def grant_while_paying_the_wrong_nodes(self,
ursulas_to_trick_into_working_for_free,
ursulas_to_pay_instead,
*args, **kwargs):
"""
Instead of paying the nodes with whom I've made Arrangements,
I'll pay my flunkies instead. Since this is a valid transaction and creates
an on-chain Policy using PolicyManager, I'm hoping Ursula won't notice.
"""
def publish_wrong_payee_address_to_blockchain(policy, ursulas):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=self.registry)
receipt = policy_agent.create_policy(
policy_id=bytes(policy.hrac), # bytes16 _policyID
transacting_power=policy.publisher.transacting_power,
value=policy.value,
end_timestamp=policy.expiration, # uint16 _numberOfPeriods
node_addresses=[f.checksum_address for f in ursulas_to_pay_instead] # address[] memory _nodes
)
return receipt['transactionHash']
with patch("nucypher.policy.policies.BlockchainPolicy._publish",
publish_wrong_payee_address_to_blockchain):
return super().grant(ursulas=ursulas_to_trick_into_working_for_free, *args, **kwargs)

View File

@ -534,28 +534,3 @@ def transfer_ownership(general_config, actor_options, target_address, gas):
new_owner=target_address,
transaction_gas_limit=gas)
paint_receipt_summary(emitter=emitter, receipt=receipt)
@deploy.command("set-range")
@group_general_config
@group_actor_options
@click.option('--minimum', help="Minimum value for range (in wei)", type=WEI)
@click.option('--default', help="Default value for range (in wei)", type=WEI)
@click.option('--maximum', help="Maximum value for range (in wei)", type=WEI)
def set_range(general_config, actor_options, minimum, default, maximum):
"""
Set the minimum, default & maximum fee rate for all policies ('global fee range') in the policy manager contract.
The minimum acceptable fee rate (set by stakers) must fall within the global fee range.
"""
emitter = general_config.emitter
ADMINISTRATOR, _, _, _ = actor_options.create_actor(emitter)
if not minimum:
minimum = click.prompt("Enter new minimum value for range", type=click.IntRange(min=0))
if not default:
default = click.prompt("Enter new default value for range", type=click.IntRange(min=minimum))
if not maximum:
maximum = click.prompt("Enter new maximum value for range", type=click.IntRange(min=default))
ADMINISTRATOR.set_fee_rate_range(minimum=minimum, default=default, maximum=maximum)
emitter.echo(MINIMUM_POLICY_RATE_EXCEEDED_WARNING.format(minimum=minimum, maximum=maximum, default=default))

View File

@ -115,7 +115,6 @@ from nucypher.cli.options import (
option_gas_price
)
from nucypher.cli.painting.staking import (
paint_min_rate,
paint_staged_stake,
paint_staged_stake_division,
paint_stakes,
@ -1189,57 +1188,6 @@ def events(general_config, staker_options, config_file, event_name, from_block,
csv_output_file=csv_output_file)
@stake.command('set-min-rate')
@group_transacting_staker_options
@option_config_file
@option_force
@group_general_config
@click.option('--min-rate', help="Minimum acceptable fee rate (in GWEI), set by staker", type=GWEI)
def set_min_rate(general_config: GroupGeneralConfig,
transacting_staker_options: TransactingStakerOptions,
config_file, force, min_rate):
"""Staker sets the minimum acceptable fee rate for their associated worker."""
# Setup
emitter = setup_emitter(general_config)
STAKEHOLDER = transacting_staker_options.create_character(emitter, config_file)
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)
if not min_rate:
paint_min_rate(emitter, STAKEHOLDER)
minimum, _default, maximum = STAKEHOLDER.staker.policy_agent.get_fee_rate_range()
lower_bound_in_gwei = Web3.fromWei(minimum, 'gwei')
upper_bound_in_gwei = Web3.fromWei(maximum, 'gwei')
min_rate = click.prompt(PROMPT_STAKER_MIN_POLICY_RATE, type=DecimalRange(min=lower_bound_in_gwei,
max=upper_bound_in_gwei))
min_rate = int(Web3.toWei(Decimal(min_rate), 'gwei'))
if not force:
min_rate_in_gwei = Web3.fromWei(min_rate, 'gwei')
click.confirm(CONFIRM_NEW_MIN_POLICY_RATE.format(min_rate=min_rate_in_gwei), abort=True)
password = get_password(stakeholder=STAKEHOLDER,
blockchain=blockchain,
client_account=client_account,
hw_wallet=transacting_staker_options.hw_wallet)
STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
receipt = STAKEHOLDER.staker.set_min_fee_rate(min_rate=min_rate)
# Report Success
message = SUCCESSFUL_SET_MIN_POLICY_RATE.format(min_rate=min_rate, staking_address=staking_address)
emitter.echo(message, color='green')
paint_receipt_summary(emitter=emitter,
receipt=receipt,
chain_name=blockchain.client.chain_name,
transaction_type='set_min_rate')
@stake.command()
@group_transacting_staker_options
@option_config_file

View File

@ -21,7 +21,7 @@ from pathlib import Path
import click
from nucypher.blockchain.eth.actors import Staker
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent, StakingEscrowAgent
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
from nucypher.blockchain.eth.constants import (
POLICY_MANAGER_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME
@ -40,7 +40,7 @@ from nucypher.cli.options import (
option_registry_filepath,
option_staking_address,
)
from nucypher.cli.painting.staking import paint_fee_rate_range, paint_stakes
from nucypher.cli.painting.staking import paint_stakes
from nucypher.cli.painting.status import paint_contract_status, paint_locked_tokens_status, paint_stakers
from nucypher.cli.utils import (
connect_to_blockchain,
@ -236,13 +236,3 @@ def events(general_config, registry_options, contract_name, from_block, to_block
to_block=to_block,
argument_filters=argument_filters,
csv_output_file=csv_output_file)
@status.command(name='fee-range')
@group_registry_options
@group_general_config
def fee_range(general_config, registry_options):
"""Provide information on the global fee range the range into which the minimum fee rate must fall."""
emitter, registry, blockchain = registry_options.setup(general_config=general_config)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=registry)
paint_fee_rate_range(emitter=emitter, policy_agent=policy_agent)

View File

@ -17,23 +17,20 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import maya
import tabulate
import webbrowser
from web3.main import Web3
from nucypher.blockchain.eth.agents import (
ContractAgency,
NucypherTokenAgent,
PolicyManagerAgent,
)
from nucypher.blockchain.eth.constants import NUCYPHER_TOKEN_CONTRACT_NAME
from nucypher.blockchain.eth.deployers import DispatcherDeployer, PolicyManagerDeployer, StakingInterfaceRouterDeployer
from nucypher.blockchain.eth.deployers import DispatcherDeployer
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import etherscan_url
from nucypher.characters.banners import NU_BANNER
from nucypher.cli.painting.staking import paint_fee_rate_range
from nucypher.cli.painting.transactions import paint_receipt_summary
@ -163,14 +160,3 @@ Registry ................ {registry.filepath}
message = f"\n{contract_deployer_class.contract_name} is not enrolled in {registry.filepath}"
emitter.echo(message, color='yellow')
emitter.echo(sep, nl=False)
try:
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=registry)
paint_fee_rate_range(emitter, policy_agent)
emitter.echo(sep, nl=False)
except BaseContractRegistry.UnknownContract:
message = f"\n{PolicyManagerDeployer.contract_name} is not enrolled in {registry.filepath}"
emitter.echo(message, color='yellow')
emitter.echo(sep, nl=False)

View File

@ -255,29 +255,6 @@ def paint_staking_accounts(emitter, signer, registry, domain):
emitter.echo(tabulate.tabulate(rows, showindex=True, headers=headers, tablefmt="fancy_grid"))
def paint_fee_rate_range(emitter, policy_agent):
minimum, default, maximum = policy_agent.get_fee_rate_range()
range_payload = f"""
Global fee Range:
~ Minimum ............ {prettify_eth_amount(minimum)}
~ Default ............ {prettify_eth_amount(default)}
~ Maximum ............ {prettify_eth_amount(maximum)}"""
emitter.echo(range_payload)
def paint_min_rate(emitter, staker):
paint_fee_rate_range(emitter, staker.policy_agent)
minimum = staker.min_fee_rate
raw_minimum = staker.raw_min_fee_rate
rate_payload = f"""
Minimum acceptable fee rate (set by staker for their associated worker):
~ Previously set ....... {prettify_eth_amount(raw_minimum)}
~ Effective ............ {prettify_eth_amount(minimum)}"""
emitter.echo(rate_payload)
def paint_staking_rewards(stakeholder, blockchain, emitter, past_periods, staking_address, staking_agent):
if not past_periods:
reward_amount = stakeholder.staker.calculate_staking_reward()

View File

@ -28,7 +28,6 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
PolicyManagerAgent,
StakingEscrowAgent
)
from nucypher.blockchain.eth.constants import NULL_ADDRESS
@ -44,14 +43,12 @@ def paint_contract_status(registry, emitter):
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=registry)
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=registry)
contracts = f"""
| Contract Deployments |
{token_agent.contract_name} ............ {token_agent.contract_address}
{staking_agent.contract_name} ............ {staking_agent.contract_address}
{policy_agent.contract_name} ............ {policy_agent.contract_address}
{adjudicator_agent.contract_name} .............. {adjudicator_agent.contract_address}
"""

View File

@ -19,14 +19,11 @@ from abc import ABC, abstractmethod
from typing import Optional, NamedTuple, Dict
import maya
from hexbytes import HexBytes
from nucypher_core import ReencryptionRequest
from web3.types import Wei, ChecksumAddress, Timestamp, TxReceipt
from nucypher.blockchain.economics import EconomicsFactory
from nucypher.blockchain.eth.agents import PolicyManagerAgent, SubscriptionManagerAgent
from nucypher.blockchain.eth.agents import SubscriptionManagerAgent
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.blockchain.eth.utils import get_current_period, datetime_at_period, calculate_period_duration
from nucypher.policy.policies import BlockchainPolicy, Policy

View File

@ -1,168 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import collections
import os
import pytest
from eth_tester.exceptions import TransactionFailed
from eth_utils import is_checksum_address, to_wei
from nucypher.blockchain.eth.constants import POLICY_ID_LENGTH
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent, StakingEscrowAgent, NucypherTokenAgent
from tests.constants import FEE_RATE_RANGE
MockPolicyMetadata = collections.namedtuple('MockPolicyMetadata', 'policy_id author addresses')
@pytest.fixture(scope='function')
def policy_meta(testerchain, agency, application_economics, blockchain_ursulas, test_registry):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
_policy_id = os.urandom(POLICY_ID_LENGTH)
staker_addresses = list(staking_agent.get_stakers_reservoir(periods=1).draw(3))
number_of_periods = 10
now = testerchain.w3.eth.getBlock('latest').timestamp
tpower = TransactingPower(account=testerchain.alice_account, signer=Web3Signer(testerchain.client))
_txhash = policy_agent.create_policy(policy_id=_policy_id,
transacting_power=tpower,
value=to_wei(1, 'gwei') * len(staker_addresses) * number_of_periods,
end_timestamp=now + (number_of_periods - 1) * application_economics.hours_per_period * 60 * 60,
node_addresses=staker_addresses)
return MockPolicyMetadata(policy_id=_policy_id, author=tpower, addresses=staker_addresses)
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_create_policy(testerchain, agency, application_economics, test_registry):
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
policy_id = os.urandom(POLICY_ID_LENGTH)
node_addresses = list(staking_agent.get_stakers_reservoir(periods=1).draw(3))
now = testerchain.w3.eth.getBlock('latest').timestamp
tpower = TransactingPower(account=testerchain.alice_account, signer=Web3Signer(testerchain.client))
receipt = policy_agent.create_policy(policy_id=policy_id,
transacting_power=tpower,
value=application_economics.min_authorization,
end_timestamp=now + 10 * application_economics.hours_per_period * 60,
node_addresses=node_addresses)
assert receipt['status'] == 1, "Transaction Rejected"
assert receipt['logs'][0]['address'] == policy_agent.contract_address
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_fetch_policy_arrangements(agency, policy_meta, test_registry):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
arrangements = list(policy_agent.fetch_policy_arrangements(policy_id=policy_meta.policy_id))
assert arrangements
assert len(arrangements) == len(policy_meta.addresses)
assert is_checksum_address(arrangements[0][0])
assert list(record[0] for record in arrangements) == policy_meta.addresses
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_revoke_arrangement(agency, policy_meta, test_registry):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
receipt = policy_agent.revoke_arrangement(policy_id=policy_meta.policy_id,
transacting_power=policy_meta.author,
node_address=policy_meta.addresses[0])
assert receipt['status'] == 1, "Transaction Rejected"
assert receipt['logs'][0]['address'] == policy_agent.contract_address
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_revoke_policy(agency, policy_meta, test_registry):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
receipt = policy_agent.revoke_policy(policy_id=policy_meta.policy_id, transacting_power=policy_meta.author)
assert receipt['status'] == 1, "Transaction Rejected"
assert receipt['logs'][0]['address'] == policy_agent.contract_address
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_calculate_refund(testerchain, agency, policy_meta, test_registry):
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
staker = policy_meta.addresses[-1]
worker = staking_agent.get_worker_from_staker(staker)
testerchain.time_travel(hours=9)
worker_power = TransactingPower(account=worker, signer=Web3Signer(testerchain.client))
staking_agent.commit_to_next_period(transacting_power=worker_power)
receipt = policy_agent.calculate_refund(policy_id=policy_meta.policy_id, transacting_power=policy_meta.author)
assert receipt['status'] == 1, "Transaction Rejected"
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_collect_refund(testerchain, agency, policy_meta, test_registry):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
testerchain.time_travel(hours=9)
receipt = policy_agent.collect_refund(policy_id=policy_meta.policy_id, transacting_power=policy_meta.author)
assert receipt['status'] == 1, "Transaction Rejected"
assert receipt['logs'][0]['address'] == policy_agent.contract_address
@pytest.mark.skip()
def test_set_min_fee_rate(testerchain, test_registry, agency, policy_meta):
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
minimum, default, maximum = FEE_RATE_RANGE
staker = policy_meta.addresses[-1]
tpower = TransactingPower(account=staker, signer=Web3Signer(testerchain.client))
assert policy_agent.get_min_fee_rate(staker) == default
with pytest.raises((TransactionFailed, ValueError)):
policy_agent.set_min_fee_rate(transacting_power=tpower, min_rate=minimum - 1)
receipt = policy_agent.set_min_fee_rate(transacting_power=tpower, min_rate=minimum + 1)
assert receipt['status'] == 1
assert policy_agent.get_min_fee_rate(staker) == minimum + 1
@pytest.mark.skip()
@pytest.mark.usefixtures('blockchain_ursulas')
def test_collect_policy_fee(testerchain, agency, policy_meta, application_economics, test_registry):
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
staker = policy_meta.addresses[-1]
worker = staking_agent.get_worker_from_staker(staker)
worker_power = TransactingPower(account=worker, signer=Web3Signer(testerchain.client))
old_eth_balance = token_agent.blockchain.client.get_balance(staker)
for _ in range(application_economics.min_operator_seconds):
testerchain.time_travel(periods=1)
staking_agent.commit_to_next_period(transacting_power=worker_power)
staker_power = TransactingPower(account=staker, signer=Web3Signer(testerchain.client))
receipt = policy_agent.collect_policy_fee(collector_address=staker, transacting_power=staker_power)
assert receipt['status'] == 1, "Transaction Rejected"
assert receipt['logs'][0]['address'] == policy_agent.contract_address
new_eth_balance = token_agent.blockchain.client.get_balance(staker)
assert new_eth_balance > old_eth_balance

View File

@ -21,11 +21,8 @@ import pytest
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.deployers import (
AdjudicatorDeployer,
NucypherTokenDeployer,
PolicyManagerDeployer,
StakingEscrowDeployer,
StakingInterfaceDeployer
)
from constant_sorrow.constants import (FULL, INIT)
@ -62,13 +59,6 @@ def staking_escrow_deployer(testerchain,
return staking_escrow_deployer
@pytest.fixture(scope="module")
def staking_interface_deployer(staking_escrow_deployer, testerchain, test_registry, threshold_staking):
staking_interface_deployer = StakingInterfaceDeployer(staking_interface=threshold_staking.address,
registry=test_registry)
return staking_interface_deployer
@pytest.fixture(scope="function")
def deployment_progress():
class DeploymentProgress:

View File

@ -31,7 +31,6 @@ from nucypher.blockchain.eth.deployers import (
AdjudicatorDeployer,
BaseContractDeployer,
NucypherTokenDeployer,
PolicyManagerDeployer,
StakingEscrowDeployer
)

View File

@ -1,143 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent
from nucypher.blockchain.eth.constants import POLICY_MANAGER_CONTRACT_NAME
from nucypher.blockchain.eth.deployers import (DispatcherDeployer, PolicyManagerDeployer)
@pytest.mark.skip()
def test_policy_manager_deployment(policy_manager_deployer,
staking_escrow_stub_deployer,
deployment_progress,
transacting_power):
assert policy_manager_deployer.contract_name == POLICY_MANAGER_CONTRACT_NAME
deployment_receipts = policy_manager_deployer.deploy(progress=deployment_progress,
transacting_power=transacting_power)
# deployment steps must match expected number of steps
steps = policy_manager_deployer.deployment_steps
assert deployment_progress.num_steps == len(steps) == len(deployment_receipts) == 2
for step_title in steps:
assert deployment_receipts[step_title]['status'] == 1
staking_escrow_address = policy_manager_deployer.contract.functions.escrow().call()
assert staking_escrow_stub_deployer.contract_address == staking_escrow_address
@pytest.mark.skip()
def test_make_agent(policy_manager_deployer, test_registry):
# Create a PolicyManagerAgent
policy_agent = policy_manager_deployer.make_agent()
# Retrieve the PolicyManagerAgent singleton
some_policy_agent = PolicyManagerAgent(registry=test_registry)
assert policy_agent == some_policy_agent # __eq__
# Compare the contract address for equality
assert policy_agent.contract_address == some_policy_agent.contract_address
@pytest.mark.skip()
def test_policy_manager_has_dispatcher(policy_manager_deployer, testerchain, test_registry):
# Let's get the "bare" PolicyManager contract (i.e., unwrapped, no dispatcher)
existing_bare_contract = testerchain.get_contract_by_name(registry=test_registry,
contract_name=policy_manager_deployer.contract_name,
proxy_name=DispatcherDeployer.contract_name,
use_proxy_address=False)
# This contract shouldn't be accessible directly through the deployer or the agent
assert policy_manager_deployer.contract_address != existing_bare_contract.address
policy_manager_agent = PolicyManagerAgent(registry=test_registry)
assert policy_manager_agent.contract_address != existing_bare_contract
# The wrapped contract, on the other hand, points to the bare one.
target = policy_manager_deployer.contract.functions.target().call()
assert target == existing_bare_contract.address
@pytest.mark.skip()
def test_upgrade(testerchain, test_registry, transacting_power):
deployer = PolicyManagerDeployer(registry=test_registry)
bare_contract = testerchain.get_contract_by_name(registry=test_registry,
contract_name=PolicyManagerDeployer.contract_name,
proxy_name=DispatcherDeployer.contract_name,
use_proxy_address=False)
old_address = bare_contract.address
receipts = deployer.upgrade(ignore_deployed=True, confirmations=0, transacting_power=transacting_power)
bare_contract = testerchain.get_contract_by_name(registry=test_registry,
contract_name=PolicyManagerDeployer.contract_name,
proxy_name=DispatcherDeployer.contract_name,
use_proxy_address=False)
new_address = bare_contract.address
assert old_address != new_address
# TODO: Contract ABI is not updated in Agents when upgrade/rollback #1184
transactions = ('deploy', 'retarget')
assert len(receipts) == len(transactions)
for tx in transactions:
assert receipts[tx]['status'] == 1
@pytest.mark.skip()
def test_rollback(testerchain, test_registry, transacting_power):
deployer = PolicyManagerDeployer(registry=test_registry)
policy_manager_agent = PolicyManagerAgent(registry=test_registry)
current_target = policy_manager_agent.contract.functions.target().call()
# Let's do one more upgrade
receipts = deployer.upgrade(ignore_deployed=True, confirmations=0, transacting_power=transacting_power)
for title, receipt in receipts.items():
assert receipt['status'] == 1
old_target = current_target
current_target = policy_manager_agent.contract.functions.target().call()
assert current_target != old_target
# It's time to rollback.
receipt = deployer.rollback(transacting_power=transacting_power)
assert receipt['status'] == 1
new_target = policy_manager_agent.contract.functions.target().call()
assert new_target != current_target
assert new_target == old_target
@pytest.mark.skip()
def test_set_fee_range(policy_manager_deployer, test_registry, transacting_power):
policy_agent: PolicyManagerAgent = ContractAgency.get_agent(PolicyManagerAgent, registry=test_registry)
assert policy_agent.get_fee_rate_range() == (0, 0, 0)
minimum, default, maximum = 10, 20, 30
receipt = policy_manager_deployer.set_fee_rate_range(minimum=minimum,
default=default,
maximum=maximum,
transacting_power=transacting_power)
assert receipt['status'] == 1
assert policy_agent.get_fee_rate_range() == (minimum, default, maximum)

View File

@ -22,7 +22,6 @@ import pytest
from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
PolicyManagerAgent,
StakingEscrowAgent
)
from nucypher.blockchain.eth.constants import (
@ -34,7 +33,6 @@ from nucypher.blockchain.eth.constants import (
)
from nucypher.blockchain.eth.deployers import (
StakingEscrowDeployer,
StakingInterfaceDeployer,
SubscriptionManagerDeployer
)
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry
@ -61,36 +59,10 @@ def test_nucypher_deploy_inspect_no_deployments(click_runner, testerchain, new_l
assert 'not enrolled' in result.output
@pytest.mark.skip()
def test_set_range(click_runner, testerchain, agency_local_registry):
minimum, default, maximum = 10, 20, 30
status_command = ('set-range',
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--registry-infile', str(agency_local_registry.filepath.absolute()),
'--minimum', minimum,
'--default', default,
'--network', TEMPORARY_DOMAIN,
'--maximum', maximum)
account_index = '0\n'
yes = 'Y\n'
user_input = account_index + yes + yes
result = click_runner.invoke(deploy,
status_command,
input=user_input,
catch_exceptions=False)
assert result.exit_code == 0, result.output
assert f"range [{minimum}, {maximum}]" in result.output
assert f"default value {default}" in result.output
@pytest.mark.skip()
def test_nucypher_deploy_inspect_fully_deployed(click_runner, agency_local_registry):
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=agency_local_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=agency_local_registry)
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
status_command = ('inspect',
@ -103,7 +75,6 @@ def test_nucypher_deploy_inspect_fully_deployed(click_runner, agency_local_regis
catch_exceptions=False)
assert result.exit_code == 0
assert staking_agent.owner in result.output
assert policy_agent.owner in result.output
assert adjudicator_agent.owner in result.output
minimum, default, maximum = 10, 10, 10
@ -117,7 +88,6 @@ def test_nucypher_deploy_inspect_fully_deployed(click_runner, agency_local_regis
def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=agency_local_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=agency_local_registry)
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
assert staking_agent.owner == testerchain.etherbase_account
@ -167,41 +137,11 @@ def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
assert result.exit_code == 0
assert staking_agent.owner != maclane
assert staking_agent.owner == michwill
assert policy_agent.owner == testerchain.etherbase_account
assert adjudicator_agent.owner == testerchain.etherbase_account
# Test transfer ownersh
@pytest.mark.skip()
def test_transfer_ownership_staking_interface_router(click_runner, testerchain, agency_local_registry):
maclane = testerchain.unassigned_accounts[0]
ownership_command = ('transfer-ownership',
'--registry-infile', str(agency_local_registry.filepath.absolute()),
'--contract-name', StakingInterfaceDeployer.contract_name,
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--target-address', maclane,
'--debug')
account_index = '0\n'
yes = 'Y\n'
user_input = account_index + yes + yes
result = click_runner.invoke(deploy,
ownership_command,
input=user_input,
catch_exceptions=False)
assert result.exit_code == 0, result.output
# This owner is updated
interface_deployer = StakingInterfaceDeployer(registry=agency_local_registry)
assert interface_deployer.owner == maclane
def test_bare_contract_deployment_to_alternate_registry(click_runner, agency_local_registry):
if ALTERNATE_REGISTRY_FILEPATH.exists():

View File

@ -17,7 +17,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import csv
import random
import re
from pathlib import Path
import pytest
@ -27,7 +26,6 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
PolicyManagerAgent,
StakingEscrowAgent
)
from nucypher.blockchain.eth.token import NU
@ -49,10 +47,9 @@ def test_nucypher_status_network(click_runner, testerchain, agency_local_registr
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=agency_local_registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=agency_local_registry)
policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=agency_local_registry)
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
agents = (token_agent, staking_agent, policy_agent, adjudicator_agent)
agents = (token_agent, staking_agent, adjudicator_agent)
for agent in agents:
contract_regex = f"^{agent.contract_name} \\.+ {agent.contract_address}"
assert re.search(contract_regex, result.output, re.MULTILINE)

View File

@ -1,163 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "contracts/PolicyManager.sol";
/**
* @notice Upgrade to this contract must lead to fail
*/
contract PolicyManagerBad is PolicyManager {
constructor(IStakingEscrow _escrow) PolicyManager(_escrow, _escrow) {
}
function getNodeFeeDelta(address, uint16) public view override returns (int256) {}
}
/**
* @notice Contract for testing upgrading the PolicyManager contract
*/
contract PolicyManagerV2Mock is PolicyManager {
uint256 public valueToCheck;
constructor(IStakingEscrow _escrow) PolicyManager(_escrow, _escrow) {
}
function setValueToCheck(uint256 _valueToCheck) public {
valueToCheck = _valueToCheck;
}
function verifyState(address _testTarget) public override {
super.verifyState(_testTarget);
require(delegateGet(_testTarget, this.valueToCheck.selector) == valueToCheck);
}
}
/**
* @notice Contract for using in PolicyManager tests
*/
contract StakingEscrowForPolicyMock {
struct Downtime {
uint16 startPeriod;
uint16 endPeriod;
}
uint32 public immutable genesisSecondsPerPeriod;
uint32 public immutable secondsPerPeriod;
PolicyManager public policyManager;
uint16 public lastCommittedPeriod;
Downtime[] public downtime;
/**
* @param _genesisHoursPerPeriod Size of period in hours at genesis
* @param _hoursPerPeriod Size of period in hours
*/
constructor(uint16 _genesisHoursPerPeriod, uint16 _hoursPerPeriod) {
secondsPerPeriod = _hoursPerPeriod * uint32(1 hours);
genesisSecondsPerPeriod = _genesisHoursPerPeriod * uint32(1 hours);
}
/**
* @return Number of current period
*/
function getCurrentPeriod() public view returns (uint16) {
return uint16(block.timestamp / secondsPerPeriod);
}
/**
* @notice Set last committed period
*/
function setLastCommittedPeriod(uint16 _lastCommittedPeriod) external {
lastCommittedPeriod = _lastCommittedPeriod;
}
/**
* @notice Add downtime period
*/
function pushDowntimePeriod(uint16 _startPeriod, uint16 _endPeriod) external {
downtime.push(Downtime(_startPeriod, _endPeriod));
}
/**
* @notice Emulate ping method call
*/
function ping(
address _node,
uint16 _processedPeriod1,
uint16 _processedPeriod2,
uint16 _periodToSetDefault
) external {
policyManager.ping(_node, _processedPeriod1, _processedPeriod2, _periodToSetDefault);
}
/**
* @notice Emulate migrate method call
*/
function migrate(address _node) external {
policyManager.migrate(_node);
}
/**
* @notice Set policy manager address
*/
function setPolicyManager(PolicyManager _policyManager) external {
policyManager = _policyManager;
}
function getPastDowntimeLength(address) public view returns (uint256) {
return downtime.length;
}
function getPastDowntime(address, uint256 _index)
public view returns (uint16 startPeriod, uint16 endPeriod)
{
Downtime storage data = downtime[_index];
startPeriod = data.startPeriod;
endPeriod = data.endPeriod;
}
function getLastCommittedPeriod(address) public view returns (uint256) {
return lastCommittedPeriod;
}
function register(address _node, uint16 _period) public {
policyManager.register(_node, _period);
}
function register(address _node) external {
register(_node, getCurrentPeriod() - 1);
}
function findIndexOfPastDowntime(address, uint16 _period) external view returns (uint256 index) {
for (index = 0; index < downtime.length; index++) {
if (_period <= downtime[index].endPeriod) {
return index;
}
}
}
}
/**
* @notice Helper to prepare broken state
*/
contract ExtendedPolicyManager is PolicyManager {
constructor(IStakingEscrow _escrow) PolicyManager(_escrow, _escrow) {
}
function setNodeFeeDelta(address _node, uint16 _period, int256 _value) external {
NodeInfo storage node = nodes[_node];
node.feeDelta[_period] = _value;
}
}

View File

@ -1,299 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "contracts/NuCypherToken.sol";
import "contracts/staking_contracts/AbstractStakingContract.sol";
/**
* @notice Contract for using in staking contracts tests
*/
contract StakingEscrowForStakingContractMock {
NuCypherToken public immutable token;
address public node;
uint256 public value;
uint256 public lockedValue;
bool public snapshots;
constructor(NuCypherToken _token) {
token = _token;
}
function deposit(address _node, uint256 _value) external {
node = _node;
value += _value;
lockedValue += _value;
token.transferFrom(msg.sender, address(this), _value);
}
function withdraw(uint256 _value) public {
value -= _value;
token.transfer(msg.sender, _value);
}
function withdrawAll() external {
withdraw(value);
}
function getAllTokens(address) external view returns (uint256) {
return value;
}
function setSnapshots(bool _snapshotsEnabled) external {
snapshots = _snapshotsEnabled;
}
}
/**
* @notice Contract for staking contract tests
*/
contract PolicyManagerForStakingContractMock {
uint32 public immutable secondsPerPeriod = 1;
uint256 public minFeeRate;
function withdraw() public returns (uint256) {
uint256 value = address(this).balance;
require(value > 0);
payable(msg.sender).transfer(value);
return value;
}
function setMinFeeRate(uint256 _minFeeRate) public {
minFeeRate = _minFeeRate;
}
function additionalMethod(uint256 _minFeeRate) public {
minFeeRate = _minFeeRate;
}
receive() external payable {}
}
/**
* @notice Contract for staking contract tests
*/
contract WorkLockForStakingContractMock {
uint256 public immutable boostingRefund = 1;
uint256 public claimed;
uint256 public depositedETH;
uint256 public compensationValue;
uint256 public refundETH;
uint256 public futureClaim;
function bid() external payable {
depositedETH = msg.value;
}
function cancelBid() external {
uint256 value = depositedETH;
depositedETH = 0;
payable(msg.sender).transfer(value);
}
function sendCompensation() external payable {
compensationValue = msg.value;
}
function compensation(address) public view returns (uint256) {
return compensationValue;
}
function withdrawCompensation() external {
uint256 value = compensationValue;
compensationValue = 0;
payable(msg.sender).transfer(value);
}
function setClaimedTokens(uint256 _claimedTokens) external {
futureClaim = _claimedTokens;
}
function claim() external returns (uint256) {
if (futureClaim == 0) {
claimed += 1;
} else {
claimed += futureClaim;
}
return claimed;
}
function sendRefund() external payable {
refundETH = msg.value;
}
function refund() external returns (uint256) {
uint256 value = refundETH;
refundETH = 0;
payable(msg.sender).transfer(value);
return value;
}
}
/**
* @notice Contract for staking contract tests
*/
contract ThresholdStakingForStakingContractMock {
address public operator;
address payable public beneficiary;
address public authorizer;
uint96 public stakedNuInT;
function stakedNu(address) external view returns (uint256) {
return 0;
}
function stakeNu(
address _operator,
address payable _beneficiary,
address _authorizer
) external {
operator = _operator;
beneficiary = _beneficiary;
authorizer = _authorizer;
stakedNuInT = 1000;
}
function unstakeNu(address _operator, uint96 _amount) external {
require(operator == _operator);
stakedNuInT -= _amount;
}
}
/**
* @notice Contract for staking contract tests
*/
contract StakingInterfaceMockV1 {
address public immutable token = address(1);
address public immutable escrow = address(1);
function firstMethod() public pure {}
function secondMethod() public pure returns (uint256) {
return 20;
}
}
/**
* @notice Contract for staking contract tests
*/
contract StakingInterfaceMockV2 {
address public immutable token = address(1);
address public immutable escrow = address(1);
receive() external payable {}
function firstMethod(uint256) public pure {}
function secondMethod() public pure returns (uint256) {
return 15;
}
function thirdMethod() public pure {}
}
/**
* @dev Interface that could be destroyed by selfdestruct
*/
contract DestroyableStakingInterface {
address public immutable token = address(1);
address public immutable escrow = address(1);
function method() public pure returns (uint256) {
return 15;
}
function destroy() public {
selfdestruct(payable(msg.sender));
}
}
/**
* @notice Simple implementation of AbstractStakingContract
*/
contract SimpleStakingContract is AbstractStakingContract, Ownable {
using SafeERC20 for NuCypherToken;
using Address for address payable;
/**
* @param _router Address of the StakingInterfaceRouter contract
*/
constructor(StakingInterfaceRouter _router) AbstractStakingContract(_router) {}
/**
* @notice Withdraw available amount of tokens to owner
* @param _value Amount of token to withdraw
*/
function withdrawTokens(uint256 _value) public override onlyOwner {
token.safeTransfer(msg.sender, _value);
}
/**
* @notice Withdraw available ETH to the owner
*/
function withdrawETH() public override onlyOwner {
uint256 balance = address(this).balance;
require(balance != 0);
payable(msg.sender).sendValue(balance);
}
/**
* @notice Calling fallback function is allowed only for the owner
*/
function isFallbackAllowed() public view override returns (bool) {
return msg.sender == owner();
}
}
interface IExtendedStakingEscrow is IStakingEscrow {
function deposit(address, uint256) external;
function withdraw(uint256) external override;
}
/**
* @notice Contract for staking contract tests
*/
contract ExtendedStakingInterface is StakingInterface {
event DepositedAsStaker(address indexed sender, uint256 value);
constructor(
NuCypherToken _token,
IStakingEscrow _escrow,
PolicyManager _policyManager,
WorkLock _workLock,
IStaking _tStaking
)
StakingInterface(_token, _escrow, _policyManager, _workLock, _tStaking)
{
}
function depositAsStaker(uint256 _value) public onlyDelegateCall {
require(token.balanceOf(address(this)) >= _value);
token.approve(address(escrow), _value);
IExtendedStakingEscrow(address(escrow)).deposit(address(this), _value);
emit DepositedAsStaker(msg.sender, _value);
}
}

View File

@ -1,437 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import pytest
from eth_tester.exceptions import TransactionFailed
from eth_utils import to_canonical_address, to_wei
from web3.contract import Contract
from nucypher_core.umbral import SecretKey, Signer
from nucypher.blockchain.economics import Economics, Economics
from nucypher.blockchain.eth.constants import NULL_ADDRESS, POLICY_ID_LENGTH
from nucypher.blockchain.eth.token import NU
from nucypher.crypto.utils import sha256_digest
from nucypher.crypto.signing import SignatureStamp
from nucypher.utilities.ethereum import to_32byte_hex
TOTAL_SUPPLY = NU(10_000_000_000, 'NU').to_units()
MIN_ALLOWED_TOKENS = NU(15_000, 'NU').to_units()
MAX_ALLOWED_TOKENS = 10 * MIN_ALLOWED_TOKENS
MIN_LOCKED_PERIODS = 4
def pytest_namespace():
return {'escrow_supply': 0,
'staker1_tokens': 0,
'staker4_tokens': 0,
'staker1_completed_work': 0,
'staker2_completed_work': 0}
@pytest.fixture(scope='module')
def token(deploy_contract):
# Create an ERC20 token
contract, _ = deploy_contract('NuCypherToken', _totalSupplyOfTokens=TOTAL_SUPPLY)
return contract
@pytest.fixture(scope='module')
def escrow_dispatcher(testerchain, token, deploy_contract):
escrow_stub, _ = deploy_contract('StakingEscrowStub',
token.address,
MIN_ALLOWED_TOKENS,
MAX_ALLOWED_TOKENS)
dispatcher, _ = deploy_contract('Dispatcher', escrow_stub.address)
return dispatcher
@pytest.fixture(scope='module')
def worklock(testerchain, token, escrow_dispatcher, deploy_contract):
# Creator deploys the worklock using test values
now = testerchain.w3.eth.getBlock('latest').timestamp
start_bid_date = ((now + 3600) // 3600 + 1) * 3600 # beginning of the next hour plus 1 hour
end_bid_date = start_bid_date + 3600
end_cancellation_date = end_bid_date + 3600
boosting_refund = 100
staking_periods = MIN_LOCKED_PERIODS
min_allowed_bid = to_wei(1, 'ether')
contract, _ = deploy_contract(
contract_name='WorkLock',
_token=token.address,
_escrow=escrow_dispatcher.address,
_startBidDate=start_bid_date,
_endBidDate=end_bid_date,
_endCancellationDate=end_cancellation_date,
_boostingRefund=boosting_refund,
_stakingPeriods=staking_periods,
_minAllowedBid=min_allowed_bid
)
return contract
@pytest.fixture(scope='module')
def threshold_staking(deploy_contract):
threshold_staking, _ = deploy_contract('ThresholdStakingForStakingEscrowMock')
return threshold_staking
@pytest.fixture(scope='module')
def escrow_bare(testerchain,
token,
worklock,
threshold_staking,
escrow_dispatcher,
deploy_contract):
# Creator deploys the escrow
contract, _ = deploy_contract(
'EnhancedStakingEscrow',
token.address,
worklock.address,
threshold_staking.address
)
tx = escrow_dispatcher.functions.upgrade(contract.address).transact()
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture(scope='module')
def escrow(testerchain, escrow_bare, escrow_dispatcher):
# Wrap dispatcher contract
contract = testerchain.client.get_contract(
abi=escrow_bare.abi,
address=escrow_dispatcher.address,
ContractFactoryClass=Contract)
return contract
def mock_ursula(testerchain, account, mocker):
ursula_privkey = SecretKey.random()
ursula_stamp = SignatureStamp(verifying_key=ursula_privkey.public_key(),
signer=Signer(ursula_privkey))
signed_stamp = testerchain.client.sign_message(account=account,
message=bytes(ursula_stamp))
ursula = mocker.Mock(stamp=ursula_stamp, decentralized_identity_evidence=signed_stamp)
return ursula
@pytest.fixture(scope='module')
def staking_interface(testerchain, token, escrow, worklock, threshold_staking, deploy_contract):
policy_manager, _ = deploy_contract('PolicyManagerForStakingContractMock')
# Creator deploys the staking interface
staking_interface, _ = deploy_contract(
'StakingInterface',
token.address,
escrow.address,
policy_manager.address,
worklock.address,
threshold_staking.address
)
return staking_interface
@pytest.fixture(scope='module')
def staking_interface_router(testerchain, staking_interface, deploy_contract):
router, _ = deploy_contract('StakingInterfaceRouter', staking_interface.address)
return router
@pytest.fixture(scope='module')
def simple_staking_contract(testerchain, staking_interface, staking_interface_router, deploy_contract):
creator = testerchain.w3.eth.accounts[0]
staker3 = testerchain.client.accounts[3]
# Create the first preallocation escrow
contract, _ = deploy_contract('SimpleStakingContract', staking_interface_router.address)
tx = contract.functions.transferOwnership(staker3).transact({'from': creator})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture(scope='module')
def simple_staking_contract_interface(testerchain, staking_interface, simple_staking_contract):
contract = testerchain.client.get_contract(
abi=staking_interface.abi,
address=simple_staking_contract.address,
ContractFactoryClass=Contract)
return contract
def test_worklock_phases(testerchain,
token,
escrow,
simple_staking_contract,
simple_staking_contract_interface,
worklock):
creator, staker1, staker2, staker3, staker4, alice1, alice2, *contracts_owners =\
testerchain.client.accounts
# Initialize worklock
worklock_supply = 3 * MIN_ALLOWED_TOKENS + MAX_ALLOWED_TOKENS
tx = token.functions.approve(worklock.address, worklock_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = worklock.functions.tokenDeposit(worklock_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Give staker some ether
tx = testerchain.w3.eth.sendTransaction(
{'from': testerchain.w3.eth.coinbase, 'to': staker2, 'value': 10 ** 10})
testerchain.wait_for_receipt(tx)
# Can't do anything before start date
deposited_eth_1 = to_wei(18, 'ether')
deposited_eth_2 = to_wei(1, 'ether')
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.bid().transact({'from': staker2, 'value': deposited_eth_1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Wait for the start of the bidding
testerchain.time_travel(hours=2)
# Staker does bid
min_stake = MIN_ALLOWED_TOKENS
bonus_worklock_supply = worklock_supply - min_stake
assert worklock.functions.workInfo(staker2).call()[0] == 0
assert testerchain.w3.eth.getBalance(worklock.address) == 0
tx = worklock.functions.bid().transact({'from': staker2, 'value': deposited_eth_1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker2).call()[0] == deposited_eth_1
worklock_balance = deposited_eth_1
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance
assert worklock.functions.ethToTokens(deposited_eth_1).call() == min_stake + bonus_worklock_supply
# Can't claim while bidding phase
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.claim().transact({'from': staker2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Other stakers do bid
assert worklock.functions.workInfo(simple_staking_contract.address).call()[0] == 0
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': simple_staking_contract.address, 'value': deposited_eth_2})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(simple_staking_contract.address) == deposited_eth_2
tx = simple_staking_contract_interface.functions.bid(deposited_eth_2).transact({'from': staker3, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(simple_staking_contract.address) == 0
assert worklock.functions.workInfo(simple_staking_contract.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
assert worklock.functions.ethToTokens(deposited_eth_2).call() == min_stake
assert worklock.functions.ethToTokens(deposited_eth_1).call() == min_stake + bonus_worklock_supply
assert worklock.functions.workInfo(staker1).call()[0] == 0
tx = worklock.functions.bid().transact({'from': staker1, 'value': 2 * deposited_eth_2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker1).call()[0] == 2 * deposited_eth_2
worklock_balance += 2 * deposited_eth_2
bonus_worklock_supply -= min_stake
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
# Wait for the end of the bidding
testerchain.time_travel(hours=1)
# Can't bid after the end of bidding
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.bid().transact({'from': staker2, 'value': 1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# One of stakers cancels bid
assert worklock.functions.getBiddersLength().call() == 3
tx = simple_staking_contract_interface.functions.cancelBid().transact({'from': staker3, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(simple_staking_contract.address).call()[0] == 0
worklock_balance -= deposited_eth_2
bonus_worklock_supply += min_stake
assert testerchain.w3.eth.getBalance(simple_staking_contract.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
assert worklock.functions.getBiddersLength().call() == 2
assert worklock.functions.bidders(1).call() == staker1
assert worklock.functions.workInfo(staker1).call()[3] == 1
# Wait for the end of the cancellation window
testerchain.time_travel(hours=1)
# Can't cancel after the end of cancellation window
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.cancelBid().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Can't claim before check
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.claim().transact({'from': staker2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Do force refund to whale
assert worklock.functions.ethToTokens(deposited_eth_1).call() > MAX_ALLOWED_TOKENS
staker2_balance = testerchain.w3.eth.getBalance(staker2)
tx = worklock.functions.forceRefund([staker2]).transact()
testerchain.wait_for_receipt(tx)
staker2_bid = worklock.functions.workInfo(staker2).call()[0]
refund = deposited_eth_1 - staker2_bid
assert refund > 0
staker2_tokens = worklock.functions.ethToTokens(staker2_bid).call()
assert staker2_tokens <= MAX_ALLOWED_TOKENS
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance
assert testerchain.w3.eth.getBalance(staker2) == staker2_balance
assert worklock.functions.compensation(staker2).call() == refund
tx = worklock.functions.withdrawCompensation().transact({'from': staker2})
testerchain.wait_for_receipt(tx)
worklock_balance -= refund
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance
assert testerchain.w3.eth.getBalance(staker2) == staker2_balance + refund
assert worklock.functions.compensation(staker2).call() == 0
# 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
# Stakers claim tokens
assert not worklock.functions.workInfo(staker2).call()[2]
tx = worklock.functions.claim().transact({'from': staker2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker2).call()[2]
assert token.functions.balanceOf(staker2).call() == 0
staker2_remaining_work = staker2_tokens
assert worklock.functions.ethToWork(staker2_bid).call() == staker2_remaining_work
assert worklock.functions.workToETH(staker2_remaining_work, staker2_bid).call() == staker2_bid
assert worklock.functions.getRemainingWork(staker2).call() == 0
assert token.functions.balanceOf(worklock.address).call() == worklock_supply - staker2_tokens
pytest.escrow_supply = staker2_tokens
assert escrow.functions.getAllTokens(staker2).call() == staker2_tokens
assert escrow.functions.getCompletedWork(staker2).call() == TOTAL_SUPPLY
tx = worklock.functions.claim().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker1).call()[2]
staker1_claims = worklock.functions.ethToTokens(2 * deposited_eth_2).call()
pytest.staker1_tokens = staker1_claims
assert escrow.functions.getAllTokens(staker1).call() == pytest.staker1_tokens
pytest.escrow_supply += staker1_claims
# Can't claim more than once
with pytest.raises((TransactionFailed, ValueError)):
tx = worklock.functions.claim().transact({'from': staker2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
def test_upgrading_and_rollback(testerchain,
token,
escrow,
escrow_dispatcher,
staking_interface_router,
worklock,
threshold_staking,
deploy_contract):
creator, staker1, staker2, staker3, staker4, alice1, alice2, *others =\
testerchain.client.accounts
# Upgrade main contracts
escrow_v1 = escrow.functions.target().call()
# Creator deploys the contracts as the second versions
escrow_v2, _ = deploy_contract(
'StakingEscrow',
token.address,
worklock.address,
threshold_staking.address
)
# Staker and Alice can't upgrade contracts, only owner can
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow_dispatcher.functions.upgrade(escrow_v2.address).transact({'from': alice1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow_dispatcher.functions.upgrade(escrow_v2.address).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
# Prepare transactions to upgrade contracts
tx = escrow_dispatcher.functions.upgrade(escrow_v2.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert escrow_v2.address == escrow.functions.target().call()
# Staker and Alice can't rollback contracts, only owner can
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow_dispatcher.functions.rollback().transact({'from': alice1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow_dispatcher.functions.rollback().transact({'from': staker1})
testerchain.wait_for_receipt(tx)
# Prepare transactions to rollback contracts
tx = escrow_dispatcher.functions.rollback().transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert escrow_v1 == escrow.functions.target().call()
# Upgrade the staking interface
# Deploy the same contract as the second version
policy_manager, _ = deploy_contract('PolicyManagerForStakingContractMock')
staking_interface_v2, _ = deploy_contract(
'StakingInterface',
token.address,
escrow.address,
policy_manager.address,
worklock.address,
threshold_staking.address
)
# Staker and Alice can't upgrade library, only owner can
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface_router.functions.upgrade(staking_interface_v2.address).transact({'from': alice1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface_router.functions.upgrade(staking_interface_v2.address).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
# Prepare transactions to upgrade library
tx = staking_interface_router.functions.upgrade(staking_interface_v2.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert staking_interface_v2.address == staking_interface_router.functions.target().call()
def test_refund(testerchain, escrow, worklock):
staker1, staker2, staker3, staker4 = testerchain.client.accounts[1:5]
deposited_eth_2 = to_wei(1, 'ether')
worklock_balance = testerchain.w3.eth.getBalance(worklock.address)
# Full refund for staker
assert escrow.functions.getCompletedWork(staker1).call() == TOTAL_SUPPLY
remaining_work = worklock.functions.getRemainingWork(staker1).call()
assert remaining_work == 0
assert worklock.functions.workInfo(staker1).call()[0] == 2 * deposited_eth_2
staker1_balance = testerchain.w3.eth.getBalance(staker1)
tx = worklock.functions.refund().transact({'from': staker1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.workInfo(staker1).call()[0] == 0
assert testerchain.w3.eth.getBalance(staker1) == staker1_balance + 2 * deposited_eth_2
worklock_balance -= 2 * deposited_eth_2
assert testerchain.w3.eth.getBalance(worklock.address) == worklock_balance

View File

@ -1,37 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
MAX_NUNIT_ERROR = 10 * 1e-18 # 2 decimal places
def prepare_staker(origin_tpower, staking_agent, token_agent, token_economics, ursula, ursula_tpower, amount, lock_periods=None):
if not lock_periods:
lock_periods = 100 * token_economics.maximum_rewarded_periods
# Prepare one staker
_txhash = token_agent.transfer(amount=amount,
target_address=ursula,
transacting_power=origin_tpower)
_txhash = token_agent.approve_transfer(amount=amount,
spender_address=staking_agent.contract_address,
transacting_power=ursula_tpower)
_txhash = staking_agent.deposit_tokens(amount=amount,
lock_periods=lock_periods,
transacting_power=ursula_tpower,
staker_address=ursula)
_txhash = staking_agent.bond_worker(transacting_power=ursula_tpower, worker_address=ursula)
_txhash = staking_agent.set_restaking(transacting_power=ursula_tpower, value=False)

View File

@ -1,65 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from web3.contract import Contract
@pytest.fixture()
def escrow(testerchain, deploy_contract):
# Creator deploys the escrow
escrow, _ = deploy_contract('StakingEscrowForPolicyMock', 1, 1)
return escrow
@pytest.fixture(params=[False, True])
def policy_manager(testerchain, escrow, request, deploy_contract):
creator, client, bad_node, node1, node2, node3, *everyone_else = testerchain.client.accounts
# Creator deploys the policy manager
contract, _ = deploy_contract('ExtendedPolicyManager', escrow.address)
# Give client some ether
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': client, 'value': 10000})
testerchain.wait_for_receipt(tx)
if request.param:
dispatcher, _ = deploy_contract('Dispatcher', contract.address)
# Deploy second version of the government contract
contract = testerchain.client.get_contract(
abi=contract.abi,
address=dispatcher.address,
ContractFactoryClass=Contract)
tx = escrow.functions.setPolicyManager(contract.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Travel to the start of the next period to prevent problems with unexpected overflow first period
testerchain.time_travel(hours=1)
# Register nodes
tx = escrow.functions.register(node1).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.register(node2).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.register(node3).transact()
testerchain.wait_for_receipt(tx)
return contract

View File

@ -1,883 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import pytest
from eth_tester.exceptions import TransactionFailed
from eth_utils import to_canonical_address
from web3.contract import Contract
from nucypher.blockchain.eth.constants import NULL_ADDRESS, POLICY_ID_LENGTH
DISABLED_FIELD = 0
SPONSOR_FIELD = 1
OWNER_FIELD = 2
RATE_FIELD = 3
START_TIMESTAMP_FIELD = 4
END_TIMESTAMP_FIELD = 5
FEE_FIELD = 0
PREVIOUS_FEE_PERIOD_FIELD = 1
FEE_RATE_FIELD = 2
MIN_FEE_RATE_FIELD = 3
def test_create_revoke(testerchain, escrow, policy_manager):
creator, policy_sponsor, bad_node, node1, node2, node3, policy_owner, *everyone_else = testerchain.client.accounts
rate = 20
one_period = 60 * 60
number_of_periods = 10
value = rate * number_of_periods
policy_sponsor_balance = testerchain.client.get_balance(policy_sponsor)
policy_owner_balance = testerchain.client.get_balance(policy_owner)
policy_created_log = policy_manager.events.PolicyCreated.createFilter(fromBlock='latest')
arrangement_revoked_log = policy_manager.events.ArrangementRevoked.createFilter(fromBlock='latest')
policy_revoked_log = policy_manager.events.PolicyRevoked.createFilter(fromBlock='latest')
arrangement_refund_log = policy_manager.events.RefundForArrangement.createFilter(fromBlock='latest')
policy_refund_log = policy_manager.events.RefundForPolicy.createFilter(fromBlock='latest')
min_fee_log = policy_manager.events.MinFeeRateSet.createFilter(fromBlock='latest')
fee_range_log = policy_manager.events.FeeRateRangeSet.createFilter(fromBlock='latest')
# Only past periods is allowed in register method
current_period = policy_manager.functions.getCurrentPeriod().call()
node_for_registering = everyone_else[0]
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.register(node_for_registering, current_period).transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.register(node_for_registering, current_period + 1).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.register(node_for_registering, current_period - 1).transact()
testerchain.wait_for_receipt(tx)
assert 0 < policy_manager.functions.nodes(node_for_registering).call()[PREVIOUS_FEE_PERIOD_FIELD]
# Can't register twice
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.register(node_for_registering, current_period - 2).transact()
testerchain.wait_for_receipt(tx)
# Check registered nodes
assert 0 < policy_manager.functions.nodes(node1).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node2).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node3).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 == policy_manager.functions.nodes(bad_node).call()[PREVIOUS_FEE_PERIOD_FIELD]
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id = os.urandom(POLICY_ID_LENGTH)
# Try to create policy for bad (unregistered) node
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [bad_node])\
.transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1, bad_node])\
.transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx)
# Try to create policy with no ETH
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't create policy using timestamp from the past
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, current_timestamp -1, [node1])\
.transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx)
# Create policy
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
# Check balances and policy info
assert value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 200 == testerchain.client.get_balance(policy_sponsor)
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert 1 == policy_manager.functions.getArrangementsLength(policy_id).call()
assert node1 == policy_manager.functions.getArrangementInfo(policy_id, 0).call()[0]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id).call()
events = policy_created_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 1 == event_args['numberOfNodes']
# Can't create policy with the same id
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx)
# Only policy owner can revoke policy
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokePolicy(policy_id).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id).call()[DISABLED_FIELD]
events = policy_revoked_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sender']
assert value == event_args['value']
events = arrangement_revoked_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sender']
assert node1 == event_args['node']
assert value == event_args['value']
# Can't revoke again because policy and all arrangements are disabled
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokePolicy(policy_id).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id, node1).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't create policy with the same id even after revoking
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx)
# Create new policy
period = escrow.functions.getCurrentPeriod().call()
for period_to_set_default in range(period, period + number_of_periods + 1):
tx = escrow.functions.ping(node1, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
for period_to_set_default in range(period, period + number_of_periods + 1):
tx = escrow.functions.ping(node2, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
tx = policy_manager.functions.createPolicy(policy_id_2, policy_owner, end_timestamp, [node1, node2, node3])\
.transact({'from': policy_sponsor, 'value': 6 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 6 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 6 * value == testerchain.client.get_balance(policy_sponsor)
policy = policy_manager.functions.policies(policy_id_2).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert policy_owner == policy[OWNER_FIELD]
assert 2 * rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_owner == policy_manager.functions.getPolicyOwner(policy_id_2).call()
events = policy_created_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_owner == event_args['owner']
assert 2 * rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 3 == event_args['numberOfNodes']
# Can't revoke nonexistent arrangement
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, testerchain.client.accounts[6])\
.transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't revoke null arrangement (also it's nonexistent)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, NULL_ADDRESS).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Policy sponsor can't revoke policy, only owner can
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, node1).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Revoke only one arrangement
tx = policy_manager.functions.revokeArrangement(policy_id_2, node1).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 4 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 4 * value == testerchain.client.get_balance(policy_sponsor)
assert not policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
assert policy_owner_balance == testerchain.client.get_balance(policy_owner)
events = arrangement_revoked_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_owner == event_args['sender']
assert node1 == event_args['node']
assert 2 * value == event_args['value']
# Can't revoke again because arrangement is disabled
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, node1).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't revoke null arrangement (it's nonexistent)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, NULL_ADDRESS).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Revoke policy with remaining arrangements
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 0 == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance == testerchain.client.get_balance(policy_sponsor)
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
events = arrangement_revoked_log.get_all_entries()
assert 4 == len(events)
event_args = events[2]['args']
assert policy_id_2 == event_args['policyId']
assert policy_owner == event_args['sender']
assert node2 == event_args['node']
assert 2 * value == event_args['value']
event_args = events[3]['args']
assert policy_id_2 == event_args['policyId']
assert policy_owner == event_args['sender']
assert node3 == event_args['node']
assert 2 * value == event_args['value']
events = policy_revoked_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_owner == event_args['sender']
assert 4 * value == event_args['value']
# Can't revoke policy again because policy and all arrangements are disabled
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revokeArrangement(policy_id_2, node1).transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't create policy with wrong ETH value - when fee is not calculated by formula:
# numberOfNodes * feeRate * numberOfPeriods
policy_id_3 = os.urandom(POLICY_ID_LENGTH)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id_3, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 11})
testerchain.wait_for_receipt(tx)
# Can't set minimum fee because range is [0, 0]
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setMinFeeRate(10).transact({'from': node1})
testerchain.wait_for_receipt(tx)
# Only owner can change range
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setFeeRateRange(10, 20, 30).transact({'from': node1})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.feeRateRange().call() == [0, 0, 0]
tx = policy_manager.functions.setMinFeeRate(0).transact({'from': node1})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.getMinFeeRate(node1).call() == 0
assert len(min_fee_log.get_all_entries()) == 0
tx = policy_manager.functions.setFeeRateRange(0, 0, 0).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.feeRateRange().call() == [0, 0, 0]
events = fee_range_log.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == creator
assert event_args['min'] == 0
assert event_args['defaultValue'] == 0
assert event_args['max'] == 0
# Can't set range with inconsistent values
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setFeeRateRange(10, 5, 11).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setFeeRateRange(10, 15, 11).transact({'from': creator})
testerchain.wait_for_receipt(tx)
min_rate, default_rate, max_rate = 10, 20, 30
tx = policy_manager.functions.setFeeRateRange(min_rate, default_rate, max_rate).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.feeRateRange().call() == [min_rate, default_rate, max_rate]
assert policy_manager.functions.nodes(node1).call()[MIN_FEE_RATE_FIELD] == 0
assert policy_manager.functions.nodes(node2).call()[MIN_FEE_RATE_FIELD] == 0
assert policy_manager.functions.getMinFeeRate(node1).call() == default_rate
assert policy_manager.functions.getMinFeeRate(node2).call() == default_rate
events = fee_range_log.get_all_entries()
assert len(events) == 2
event_args = events[1]['args']
assert event_args['sender'] == creator
assert event_args['min'] == min_rate
assert event_args['defaultValue'] == default_rate
assert event_args['max'] == max_rate
# Can't set min fee let out of range
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setMinFeeRate(5).transact({'from': node1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.setMinFeeRate(35).transact({'from': node1})
testerchain.wait_for_receipt(tx)
# Set minimum fee rate for nodes
tx = policy_manager.functions.setMinFeeRate(10).transact({'from': node1})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.setMinFeeRate(20).transact({'from': node2})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.nodes(node1).call()[MIN_FEE_RATE_FIELD] == 10
assert policy_manager.functions.nodes(node2).call()[MIN_FEE_RATE_FIELD] == 20
assert policy_manager.functions.getMinFeeRate(node1).call() == 10
assert policy_manager.functions.getMinFeeRate(node2).call() == 20
events = min_fee_log.get_all_entries()
assert len(events) == 2
event_args = events[0]['args']
assert event_args['node'] == node1
assert event_args['value'] == 10
event_args = events[1]['args']
assert event_args['node'] == node2
assert event_args['value'] == 20
# Try to create policy with low rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + 10
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id_3, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 5})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id_3, policy_sponsor, end_timestamp, [node1, node2])\
.transact({'from': policy_sponsor, 'value': 30})
testerchain.wait_for_receipt(tx)
# Create new policy
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(
policy_id_3, NULL_ADDRESS, end_timestamp, [node1, node2]) \
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 2 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 2 * value == testerchain.client.get_balance(policy_sponsor)
policy = policy_manager.functions.policies(policy_id_3).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id_3).call()
events = policy_created_log.get_all_entries()
assert 3 == len(events)
event_args = events[2]['args']
assert policy_id_3 == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 2 == event_args['numberOfNodes']
# Revocation using signature
data = policy_id_3 + to_canonical_address(node1)
wrong_signature = testerchain.client.sign_message(account=creator, message=data)
# Only owner's signature can be used
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revoke(policy_id_3, node1, wrong_signature)\
.transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
signature = testerchain.client.sign_message(account=policy_sponsor, message=data)
tx = policy_manager.functions.revoke(policy_id_3, node1, signature)\
.transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - value == testerchain.client.get_balance(policy_sponsor)
assert not policy_manager.functions.policies(policy_id_3).call()[DISABLED_FIELD]
assert NULL_ADDRESS == policy_manager.functions.getArrangementInfo(policy_id_3, 0).call()[0]
assert node2 == policy_manager.functions.getArrangementInfo(policy_id_3, 1).call()[0]
data = policy_id_3 + to_canonical_address(NULL_ADDRESS)
signature = testerchain.client.sign_message(account=policy_sponsor, message=data)
tx = policy_manager.functions.revoke(policy_id_3, NULL_ADDRESS, signature)\
.transact({'from': creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_3).call()[DISABLED_FIELD]
# Create new policy
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_4 = os.urandom(POLICY_ID_LENGTH)
tx = policy_manager.functions.createPolicy(policy_id_4, policy_owner, end_timestamp, [node1, node2, node3]) \
.transact({'from': policy_sponsor, 'value': 3 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
data = policy_id_4 + to_canonical_address(NULL_ADDRESS)
wrong_signature = testerchain.client.sign_message(account=policy_sponsor, message=data)
# Only owner's signature can be used
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.revoke(policy_id_4, NULL_ADDRESS, wrong_signature)\
.transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
signature = testerchain.client.sign_message(account=policy_owner, message=data)
tx = policy_manager.functions.revoke(policy_id_4, NULL_ADDRESS, signature)\
.transact({'from': creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_4).call()[DISABLED_FIELD]
events = policy_revoked_log.get_all_entries()
assert 4 == len(events)
events = arrangement_revoked_log.get_all_entries()
assert 9 == len(events)
events = arrangement_refund_log.get_all_entries()
assert 0 == len(events)
events = policy_refund_log.get_all_entries()
assert 0 == len(events)
# If min fee rate is outside of the range after changing it - then default value must be returned
min_rate, default_rate, max_rate = 11, 15, 19
tx = policy_manager.functions.setFeeRateRange(min_rate, default_rate, max_rate).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.feeRateRange().call() == [min_rate, default_rate, max_rate]
assert policy_manager.functions.nodes(node1).call()[MIN_FEE_RATE_FIELD] == 10
assert policy_manager.functions.nodes(node2).call()[MIN_FEE_RATE_FIELD] == 20
assert policy_manager.functions.getMinFeeRate(node1).call() == default_rate
assert policy_manager.functions.getMinFeeRate(node2).call() == default_rate
events = fee_range_log.get_all_entries()
assert len(events) == 3
event_args = events[2]['args']
assert event_args['sender'] == creator
assert event_args['min'] == min_rate
assert event_args['defaultValue'] == default_rate
assert event_args['max'] == max_rate
# Try to create policy with low rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + 10
policy_id_5 = os.urandom(POLICY_ID_LENGTH)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions\
.createPolicy(policy_id_5, NULL_ADDRESS, end_timestamp, [node1]) \
.transact({'from': policy_sponsor, 'value': default_rate - 1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions\
.createPolicy(policy_id_5, NULL_ADDRESS, end_timestamp, [node2]) \
.transact({'from': policy_sponsor, 'value': default_rate - 1})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions \
.createPolicy(policy_id_5, NULL_ADDRESS, end_timestamp, [node1, node2]) \
.transact({'from': policy_sponsor, 'value': 2 * default_rate})
testerchain.wait_for_receipt(tx)
def test_create_multiple_policies(testerchain, escrow, policy_manager):
creator, policy_sponsor, bad_node, node1, node2, node3, policy_owner, *everyone_else = testerchain.client.accounts
rate = 20
one_period = 60 * 60
number_of_periods = 10
value = rate * number_of_periods
default_fee_delta = policy_manager.functions.DEFAULT_FEE_DELTA().call()
policy_sponsor_balance = testerchain.client.get_balance(policy_sponsor)
policy_created_log = policy_manager.events.PolicyCreated.createFilter(fromBlock='latest')
# Check registered nodes
assert 0 < policy_manager.functions.nodes(node1).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node2).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node3).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 == policy_manager.functions.nodes(bad_node).call()[PREVIOUS_FEE_PERIOD_FIELD]
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
# Try to create policy for bad (unregistered) node
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [bad_node])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1, bad_node])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Try to create policy with no ETH
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't create policy using timestamp from the past
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, current_timestamp - 1, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Can't create two policies with the same id
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_1, policy_id_1], policy_sponsor, end_timestamp, [node1]) \
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Can't use createPolicies() method for only one policy
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_1], policy_sponsor, end_timestamp, [node1]) \
.transact({'from': policy_sponsor, 'value': value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Create policy
current_period = escrow.functions.getCurrentPeriod().call()
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
# Check balances and policy info
assert 2 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 2 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 2
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert 1 == policy_manager.functions.getArrangementsLength(policy_id).call()
assert node1 == policy_manager.functions.getArrangementInfo(policy_id, 0).call()[0]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id).call()
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node1, current_period + number_of_periods).call() == -2 * rate
event_args = events[i]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 1 == event_args['numberOfNodes']
# Can't create policy with the same id
policy_id_3 = os.urandom(POLICY_ID_LENGTH)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_3, policy_id_1], policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Revoke policies
tx = policy_manager.functions.revokePolicy(policy_id_1).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_1).call()[DISABLED_FIELD]
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
# Create new policy
testerchain.time_travel(hours=1)
current_period = escrow.functions.getCurrentPeriod().call()
for period_to_set_default in range(current_period, current_period + number_of_periods + 1):
tx = escrow.functions.ping(node1, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.ping(node2, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
tx = policy_manager.functions.createPolicies(policies, policy_owner, end_timestamp, [node1, node2, node3])\
.transact({'from': policy_sponsor, 'value': 6 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 6 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 6 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 4
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert policy_owner == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_owner == policy_manager.functions.getPolicyOwner(policy_id).call()
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == default_fee_delta
assert policy_manager.functions.getNodeFeeDelta(node1, current_period + number_of_periods).call() == -2 * rate
assert policy_manager.functions.getNodeFeeDelta(node2, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node2, current_period + number_of_periods).call() == -2 * rate
assert policy_manager.functions.getNodeFeeDelta(node3, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node3, current_period + number_of_periods).call() == -2 * rate
event_args = events[i + 2]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_owner == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 3 == event_args['numberOfNodes']
# Revoke policies
tx = policy_manager.functions.revokePolicy(policy_id_1).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_1).call()[DISABLED_FIELD]
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
# Can't create policy with wrong ETH value - when fee is not calculated by formula:
# numberOfNodes * feeRate * numberOfPeriods * numberOfPolicies
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': value - 1})
testerchain.wait_for_receipt(tx)
min_rate, default_rate, max_rate = 10, 20, 30
tx = policy_manager.functions.setFeeRateRange(min_rate, default_rate, max_rate).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Set minimum fee rate for nodes
tx = policy_manager.functions.setMinFeeRate(10).transact({'from': node1})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.setMinFeeRate(20).transact({'from': node2})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.nodes(node1).call()[MIN_FEE_RATE_FIELD] == 10
assert policy_manager.functions.nodes(node2).call()[MIN_FEE_RATE_FIELD] == 20
assert policy_manager.functions.getMinFeeRate(node1).call() == 10
assert policy_manager.functions.getMinFeeRate(node2).call() == 20
# Try to create policy with low rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + 10
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * (min_rate - 1)})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1, node2])\
.transact({'from': policy_sponsor, 'value': 2 * 2 * (min_rate + 1)})
testerchain.wait_for_receipt(tx)
# Create new policy
value = 2 * default_rate * number_of_periods
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicies(
policies, NULL_ADDRESS, end_timestamp, [node1, node2]) \
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 2 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 2 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 6
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert default_rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id).call()
event_args = events[i + 4]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 2 == event_args['numberOfNodes']
def test_upgrading(testerchain, deploy_contract):
creator = testerchain.client.accounts[0]
# Deploy contracts
escrow1, _ = deploy_contract('StakingEscrowForPolicyMock', 1, 1)
escrow2, _ = deploy_contract('StakingEscrowForPolicyMock', 1, 1)
address1 = escrow1.address
address2 = escrow2.address
# Only escrow contract is allowed in PolicyManager constructor
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('PolicyManager', creator, address1)
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('PolicyManager', address1, creator)
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('PolicyManager', creator, creator)
contract_library_v1, _ = deploy_contract('ExtendedPolicyManager', address1)
dispatcher, _ = deploy_contract('Dispatcher', contract_library_v1.address)
# Deploy second version of the contract
contract_library_v2, _ = deploy_contract('PolicyManagerV2Mock', address2)
contract = testerchain.client.get_contract(
abi=contract_library_v2.abi,
address=dispatcher.address,
ContractFactoryClass=Contract)
tx = contract.functions.setFeeRateRange(10, 15, 20).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Can't call `finishUpgrade` and `verifyState` methods outside upgrade lifecycle
with pytest.raises((TransactionFailed, ValueError)):
tx = contract_library_v1.functions.finishUpgrade(contract.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = contract_library_v1.functions.verifyState(contract.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Upgrade to the second version
assert address1 == contract.functions.escrow().call()
tx = dispatcher.functions.upgrade(contract_library_v2.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Check constructor and storage values
assert contract_library_v2.address == dispatcher.functions.target().call()
assert address2 == contract.functions.escrow().call()
# Check new ABI
tx = contract.functions.setValueToCheck(3).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert 3 == contract.functions.valueToCheck().call()
# Can't upgrade to the previous version or to the bad version
contract_library_bad, _ = deploy_contract('PolicyManagerBad', address2)
with pytest.raises((TransactionFailed, ValueError)):
tx = dispatcher.functions.upgrade(contract_library_v1.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = dispatcher.functions.upgrade(contract_library_bad.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# But can rollback
tx = dispatcher.functions.rollback().transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert contract_library_v1.address == dispatcher.functions.target().call()
assert address1 == contract.functions.escrow().call()
# After rollback new ABI is unavailable
with pytest.raises((TransactionFailed, ValueError)):
tx = contract.functions.setValueToCheck(2).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Try to upgrade to the bad version
with pytest.raises((TransactionFailed, ValueError)):
tx = dispatcher.functions.upgrade(contract_library_bad.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
events = dispatcher.events.StateVerified.createFilter(fromBlock=0).get_all_entries()
assert 4 == len(events)
event_args = events[0]['args']
assert contract_library_v1.address == event_args['testTarget']
assert creator == event_args['sender']
event_args = events[1]['args']
assert contract_library_v2.address == event_args['testTarget']
assert creator == event_args['sender']
assert event_args == events[2]['args']
event_args = events[3]['args']
assert contract_library_v2.address == event_args['testTarget']
assert creator == event_args['sender']
events = dispatcher.events.UpgradeFinished.createFilter(fromBlock=0).get_all_entries()
assert 3 == len(events)
event_args = events[0]['args']
assert contract_library_v1.address == event_args['target']
assert creator == event_args['sender']
event_args = events[1]['args']
assert contract_library_v2.address == event_args['target']
assert creator == event_args['sender']
event_args = events[2]['args']
assert contract_library_v1.address == event_args['target']
assert creator == event_args['sender']
def test_handling_wrong_state(testerchain, deploy_contract):
creator, node1, node2, *everyone_else = testerchain.client.accounts
# Prepare enhanced version of contract
escrow, _ = deploy_contract('StakingEscrowForPolicyMock', 1, 1)
policy_manager, _ = deploy_contract('ExtendedPolicyManager', escrow.address)
tx = escrow.functions.setPolicyManager(policy_manager.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
current_period = policy_manager.functions.getCurrentPeriod().call()
initial_period = current_period - 1
tx = escrow.functions.register(node1, initial_period).transact()
testerchain.wait_for_receipt(tx)
# Prepare broken state, emulates creating policy in the same period as node was registered
number_of_periods = 2
tx = policy_manager.functions.setNodeFeeDelta(node1, initial_period, 1).transact()
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.setNodeFeeDelta(node1, initial_period + number_of_periods, -1).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.ping(node1, 0, 0, current_period).transact()
testerchain.wait_for_receipt(tx)
# Emulate making a commitments
testerchain.time_travel(hours=1)
current_period = policy_manager.functions.getCurrentPeriod().call()
tx = escrow.functions.ping(node1, 0, current_period - 1, current_period + 1).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
current_period = policy_manager.functions.getCurrentPeriod().call()
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.ping(node1, 0, current_period - 1, current_period + 1).transact()
testerchain.wait_for_receipt(tx)

View File

@ -1,670 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import pytest
from eth_tester.exceptions import TransactionFailed
from nucypher.blockchain.eth.constants import NULL_ADDRESS, POLICY_ID_LENGTH
DISABLED_FIELD = 0
SPONSOR_FIELD = 1
FEE_FIELD = 0
policy_id = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policy_id_3 = os.urandom(POLICY_ID_LENGTH)
rate = 20
one_period = 60 * 60
number_of_periods = 10
value = rate * number_of_periods
def test_fee(testerchain, escrow, policy_manager):
creator, policy_sponsor, bad_node, node1, node2, node3, *everyone_else = testerchain.client.accounts
node_balance = testerchain.client.get_balance(node1)
withdraw_log = policy_manager.events.Withdrawn.createFilter(fromBlock='latest')
# Emulate minting period without policies
period = escrow.functions.getCurrentPeriod().call()
tx = escrow.functions.ping(node1, period - 1, 0, period - 1).transact()
testerchain.wait_for_receipt(tx)
assert 0 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
for period_to_set_default in range(period, period + number_of_periods + 1):
tx = escrow.functions.ping(node1, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
# Create policy
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1, node3])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Nothing to withdraw
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.withdraw().transact({'from': node1})
testerchain.wait_for_receipt(tx)
# Can't ping directly (only through mint/commitToNextPeriod methods in the escrow contract)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.ping(node1, period - 1, 0, period).transact({'from': node1})
testerchain.wait_for_receipt(tx)
# Can't register directly (only through deposit method in the escrow contract)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.register(bad_node, period).transact({'from': bad_node})
testerchain.wait_for_receipt(tx)
# Mint some periods for calling updateFee method
for minting_period in range(period - 1, period + 4):
tx = escrow.functions.ping(node1, minting_period, 0, 0).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
period += 4
assert 80 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
# Withdraw some ETH
tx = policy_manager.functions.withdraw().transact({'from': node1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert node_balance + 80 == testerchain.client.get_balance(node1)
assert 120 + value == testerchain.client.get_balance(policy_manager.address)
events = withdraw_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert node1 == event_args['node']
assert node1 == event_args['recipient']
assert 80 == event_args['value']
# Mint more periods
tx = escrow.functions.ping(node1, 0, 0, period).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.ping(node1, 0, 0, period + 1).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.ping(node1, 0, 0, period + 2).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.ping(node1, period, period + 1, period + 3).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, period + 2, 0, period + 4).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, 0, period + 3, period + 5).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.ping(node1, period + 4, period + 5, 0).transact()
testerchain.wait_for_receipt(tx)
period += 6
assert 120 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, period, 0, 0).transact()
testerchain.wait_for_receipt(tx)
assert 120 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
# Withdraw some ETH
tx = policy_manager.functions.withdraw(node1).transact({'from': node1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert node_balance + value == testerchain.client.get_balance(node1)
assert value == testerchain.client.get_balance(policy_manager.address)
events = withdraw_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert node1 == event_args['node']
assert node1 == event_args['recipient']
assert 120 == event_args['value']
# Create policy
period = escrow.functions.getCurrentPeriod().call()
tx = escrow.functions.ping(node1, 0, 0, period).transact()
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id_2, policy_sponsor, end_timestamp, [node2, node3]) \
.transact({'from': policy_sponsor, 'value': int(2 * value)})
testerchain.wait_for_receipt(tx)
# Mint some periods
for minting_period in range(period, period + 5):
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node2, 0, minting_period, 0).transact()
testerchain.wait_for_receipt(tx)
period += 5
assert 100 == policy_manager.functions.nodes(node2).call()[FEE_FIELD]
# Mint more periods
for minting_period in range(period, period + 6):
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node2, 0, minting_period, 0).transact()
testerchain.wait_for_receipt(tx)
period += 6
assert 200 == policy_manager.functions.nodes(node2).call()[FEE_FIELD]
# Withdraw the second node fee to the first node
node_balance = testerchain.client.get_balance(node1)
node_2_balance = testerchain.client.get_balance(node2)
tx = policy_manager.functions.withdraw(node1).transact({'from': node2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert node_balance + 200 == testerchain.client.get_balance(node1)
assert node_2_balance == testerchain.client.get_balance(node2)
assert value + 200 == testerchain.client.get_balance(policy_manager.address)
events = withdraw_log.get_all_entries()
assert 3 == len(events)
event_args = events[2]['args']
assert node2 == event_args['node']
assert node1 == event_args['recipient']
assert 200 == event_args['value']
def test_refund(testerchain, escrow, policy_manager):
creator, policy_creator, bad_node, node1, node2, node3, policy_owner, *everyone_else = testerchain.client.accounts
creator_balance = testerchain.client.get_balance(policy_creator)
policy_created_log = policy_manager.events.PolicyCreated.createFilter(fromBlock='latest')
arrangement_revoked_log = policy_manager.events.ArrangementRevoked.createFilter(fromBlock='latest')
policy_revoked_log = policy_manager.events.PolicyRevoked.createFilter(fromBlock='latest')
arrangement_refund_log = policy_manager.events.RefundForArrangement.createFilter(fromBlock='latest')
policy_refund_log = policy_manager.events.RefundForPolicy.createFilter(fromBlock='latest')
# Create policy
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id, policy_owner, end_timestamp, [node1]) \
.transact({'from': policy_creator, 'value': value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
period = escrow.functions.getCurrentPeriod().call()
tx = escrow.functions.setLastCommittedPeriod(period - 1).transact({'from': creator})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=8)
# Check that methods only calculate value
tx = policy_manager.functions.calculateRefundValue(policy_id).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.calculateRefundValue(policy_id, node1).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 200 == testerchain.client.get_balance(policy_manager.address)
assert creator_balance - 200 == testerchain.client.get_balance(policy_creator)
assert 180 == policy_manager.functions.calculateRefundValue(policy_id, node1).call({'from': policy_creator})
assert 180 == policy_manager.functions.calculateRefundValue(policy_id).call({'from': policy_owner})
# Call refund, the result must be almost all ETH without payment for one period
tx = policy_manager.functions.refund(policy_id).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 20 == testerchain.client.get_balance(policy_manager.address)
assert creator_balance - 20 == testerchain.client.get_balance(policy_creator)
assert policy_creator == policy_manager.functions.policies(policy_id).call()[SPONSOR_FIELD]
events = arrangement_refund_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert 180 == event_args['value']
events = policy_refund_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_creator == event_args['sender']
assert 180 == event_args['value']
testerchain.time_travel(hours=1)
assert 20 == policy_manager.functions.calculateRefundValue(policy_id).call({'from': policy_creator})
assert 20 == policy_manager.functions.calculateRefundValue(policy_id, node1).call({'from': policy_creator})
# Call refund, last period must be refunded
tx = policy_manager.functions.refund(policy_id).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 0 == testerchain.client.get_balance(policy_manager.address)
assert creator_balance == testerchain.client.get_balance(policy_creator)
assert policy_manager.functions.policies(policy_id).call()[DISABLED_FIELD]
events = arrangement_refund_log.get_all_entries()
assert 1 == len(events)
events = arrangement_revoked_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_owner == event_args['sender']
assert node1 == event_args['node']
assert 20 == event_args['value']
events = policy_refund_log.get_all_entries()
assert 1 == len(events)
events = policy_revoked_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert policy_id == event_args['policyId']
assert policy_owner == event_args['sender']
assert 20 == event_args['value']
# Can't refund again because policy and all arrangements are disabled
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id).transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id, node1).transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id, NULL_ADDRESS).transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id).call({'from': policy_creator})
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id, node1).call({'from': policy_creator})
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id, NULL_ADDRESS).call({'from': policy_creator})
# Create new policy
testerchain.time_travel(hours=1)
period = escrow.functions.getCurrentPeriod().call()
tx = escrow.functions.setLastCommittedPeriod(period).transact()
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id_2, policy_creator, end_timestamp, [node1, node2, node3]) \
.transact({'from': policy_creator, 'value': 3 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Nothing to refund because nodes are active in the current period
assert 0 == policy_manager.functions.calculateRefundValue(policy_id_2).call({'from': policy_creator})
assert 0 == policy_manager.functions.calculateRefundValue(policy_id_2, node1).call({'from': policy_creator})
tx = policy_manager.functions.refund(policy_id_2).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.refund(policy_id_2, node1).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 3 * value == testerchain.client.get_balance(policy_manager.address)
assert creator_balance - 3 * value == testerchain.client.get_balance(policy_creator)
events = arrangement_refund_log.get_all_entries()
assert 5 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert 0 == event_args['value']
event_args = events[2]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node2 == event_args['node']
assert 0 == event_args['value']
event_args = events[3]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node3 == event_args['node']
assert 0 == event_args['value']
event_args = events[4]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert 0 == event_args['value']
events = policy_refund_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert 0 == event_args['value']
# Try to refund nonexistent policy
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id_3).transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id_3).call({'from': policy_creator})
# Only policy creator or owner can call refund
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id_2).transact({'from': node1})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id_2).call({'from': node1})
# Mint some periods and mark others as downtime periods
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, 0, period, 0).transact()
testerchain.wait_for_receipt(tx)
period += 1
testerchain.time_travel(hours=2)
tx = escrow.functions.ping(node1, period, period + 1, 0).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.pushDowntimePeriod(period + 2, period + 3).transact({'from': creator})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, period + 4, 0, 0).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.pushDowntimePeriod(period + 5, period + 7).transact({'from': creator})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, period + 8, 0, 0).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.setLastCommittedPeriod(period + 8).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert 100 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
testerchain.time_travel(hours=1)
assert 300 == policy_manager.functions.calculateRefundValue(policy_id_2).call({'from': policy_creator})
assert 100 == policy_manager.functions.calculateRefundValue(policy_id_2, node1).call({'from': policy_creator})
# Refund for only inactive periods
tx = policy_manager.functions.refund(policy_id_2, node1).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 2 * value + 100 == testerchain.client.get_balance(policy_manager.address)
assert creator_balance - (2 * value + 100) == testerchain.client.get_balance(policy_creator)
assert not policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
events = arrangement_refund_log.get_all_entries()
assert 5 == len(events)
events = arrangement_revoked_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert 100 == event_args['value']
events = policy_refund_log.get_all_entries()
assert 2 == len(events)
# Can't refund arrangement again because it's disabled
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id_2, node1).transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.refund(policy_id_2, NULL_ADDRESS)\
.transact({'from': policy_creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id_2, node1).call({'from': policy_creator})
with pytest.raises((TransactionFailed, ValueError)):
policy_manager.functions.calculateRefundValue(policy_id_2, NULL_ADDRESS)\
.call({'from': policy_creator})
# But can refund others arrangements
assert 200 == policy_manager.functions.calculateRefundValue(policy_id_2).call({'from': policy_creator})
assert 100 == policy_manager.functions.calculateRefundValue(policy_id_2, node2).call({'from': policy_creator})
assert 100 == policy_manager.functions.calculateRefundValue(policy_id_2, node3).call({'from': policy_creator})
tx = policy_manager.functions.refund(policy_id_2).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 3 * 100 == testerchain.client.get_balance(policy_manager.address)
assert creator_balance - 3 * 100 == testerchain.client.get_balance(policy_creator)
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
events = arrangement_refund_log.get_all_entries()
assert 5 == len(events)
events = arrangement_revoked_log.get_all_entries()
assert 4 == len(events)
event_args = events[2]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node2 == event_args['node']
assert 100 == event_args['value']
event_args = events[3]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node3 == event_args['node']
assert 100 == event_args['value']
events = policy_refund_log.get_all_entries()
assert 2 == len(events)
events = policy_revoked_log.get_all_entries()
assert 2 == len(events)
event_args = events[1]['args']
assert policy_id_2 == event_args['policyId']
assert policy_creator == event_args['sender']
assert 2 * 100 == event_args['value']
# Create new policy
period = escrow.functions.getCurrentPeriod().call()
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id_3, policy_creator, end_timestamp, [node1])\
.transact({'from': policy_creator, 'value': value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Mint some periods
period += 1
tx = escrow.functions.pushDowntimePeriod(period - 1, period).transact({'from': creator})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.ping(node1, period + 1, period + 2, 0).transact()
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=2)
tx = escrow.functions.ping(node1, period + 3, 0, 0).transact()
testerchain.wait_for_receipt(tx)
period += 3
tx = escrow.functions.setLastCommittedPeriod(period).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert 160 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
# Policy owner revokes policy
assert 40 == policy_manager.functions.calculateRefundValue(policy_id_3).call({'from': policy_creator})
assert 40 == policy_manager.functions.calculateRefundValue(policy_id_3, node1).call({'from': policy_creator})
policy_manager_balance = testerchain.client.get_balance(policy_manager.address)
creator_balance = testerchain.client.get_balance(policy_creator)
testerchain.time_travel(hours=1)
period = escrow.functions.getCurrentPeriod().call()
for period_to_set_default in range(period + 1, period + number_of_periods):
tx = escrow.functions.ping(node1, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id_3).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
returned = 40 + 5 * rate
assert policy_manager_balance - returned == testerchain.client.get_balance(policy_manager.address)
assert creator_balance + returned == testerchain.client.get_balance(policy_creator)
assert policy_manager.functions.policies(policy_id_3).call()[DISABLED_FIELD]
events = arrangement_refund_log.get_all_entries()
assert 5 == len(events)
events = policy_refund_log.get_all_entries()
assert 2 == len(events)
events = arrangement_revoked_log.get_all_entries()
assert 5 == len(events)
event_args = events[4]['args']
assert policy_id_3 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert returned == event_args['value']
events = policy_revoked_log.get_all_entries()
assert 3 == len(events)
event_args = events[2]['args']
assert policy_id_3 == event_args['policyId']
assert policy_creator == event_args['sender']
assert returned == event_args['value']
# Minting is useless after policy is revoked
for minting_period in range(period + 1, period + number_of_periods + 1):
testerchain.time_travel(hours=1)
tx = escrow.functions.ping(node1, 0, minting_period, 0).transact()
testerchain.wait_for_receipt(tx)
period += 20
assert 160 == policy_manager.functions.nodes(node1).call()[FEE_FIELD]
# Create policy again to test double call of `refund` with specific conditions
testerchain.time_travel(hours=number_of_periods + 2)
policy_id_4 = os.urandom(POLICY_ID_LENGTH)
number_of_periods_4 = 3
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods_4 - 1) * one_period
tx = policy_manager.functions.createPolicy(policy_id_4, policy_creator, end_timestamp, [node1]) \
.transact({'from': policy_creator, 'value': number_of_periods_4 * rate, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=number_of_periods_4 - 2)
period = escrow.functions.getCurrentPeriod().call()
creator_balance = testerchain.client.get_balance(policy_creator)
tx = escrow.functions.pushDowntimePeriod(0, period).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.setLastCommittedPeriod(period + 1).transact({'from': creator})
testerchain.wait_for_receipt(tx)
refund_value = (number_of_periods_4 - 1) * rate
assert refund_value == policy_manager.functions.calculateRefundValue(policy_id_4).call({'from': policy_creator})
# Call refund, the result must be almost all ETH without payment for one period
tx = policy_manager.functions.refund(policy_id_4).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert creator_balance + refund_value == testerchain.client.get_balance(policy_creator)
events = arrangement_refund_log.get_all_entries()
assert 6 == len(events)
event_args = events[5]['args']
assert policy_id_4 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert refund_value == event_args['value']
events = policy_refund_log.get_all_entries()
assert 3 == len(events)
event_args = events[2]['args']
assert policy_id_4 == event_args['policyId']
assert policy_creator == event_args['sender']
assert refund_value == event_args['value']
# Call refund again, the client must not get anything from the second call
creator_balance = testerchain.client.get_balance(policy_creator)
assert 0 == policy_manager.functions.calculateRefundValue(policy_id_4).call({'from': policy_creator})
tx = policy_manager.functions.refund(policy_id_4).transact({'from': policy_creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert creator_balance == testerchain.client.get_balance(policy_creator)
events = arrangement_refund_log.get_all_entries()
assert 7 == len(events)
event_args = events[6]['args']
assert policy_id_4 == event_args['policyId']
assert policy_creator == event_args['sender']
assert node1 == event_args['node']
assert 0 == event_args['value']
events = policy_created_log.get_all_entries()
assert 4 == len(events)
def test_reentrancy(testerchain, escrow, policy_manager, deploy_contract):
withdraw_log = policy_manager.events.Withdrawn.createFilter(fromBlock='latest')
arrangement_revoked_log = policy_manager.events.ArrangementRevoked.createFilter(fromBlock='latest')
policy_revoked_log = policy_manager.events.PolicyRevoked.createFilter(fromBlock='latest')
arrangement_refund_log = policy_manager.events.RefundForArrangement.createFilter(fromBlock='latest')
policy_refund_log = policy_manager.events.RefundForPolicy.createFilter(fromBlock='latest')
reentrancy_contract, _ = deploy_contract('ReentrancyTest')
contract_address = reentrancy_contract.address
tx = escrow.functions.register(contract_address).transact()
testerchain.wait_for_receipt(tx)
# Create policy and mint one period
periods = 3
policy_value = int(periods * rate)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (periods - 1) * one_period
transaction = policy_manager.functions.createPolicy(policy_id, contract_address, end_timestamp, [contract_address])\
.buildTransaction({'gas': 0})
tx = reentrancy_contract.functions.setData(1, transaction['to'], policy_value, transaction['data']).transact()
testerchain.wait_for_receipt(tx)
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': contract_address, 'value': 10000})
testerchain.wait_for_receipt(tx)
assert policy_value == testerchain.client.get_balance(policy_manager.address)
tx = policy_manager.functions.createPolicy(
policy_id_2, NULL_ADDRESS, end_timestamp, [contract_address])\
.transact({'value': policy_value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
period = escrow.functions.getCurrentPeriod().call()
tx = escrow.functions.ping(contract_address, 0, period, 0).transact()
testerchain.wait_for_receipt(tx)
assert 2 * rate == policy_manager.functions.nodes(contract_address).call()[FEE_FIELD]
# Check protection from reentrancy in withdrawal method
balance = testerchain.client.get_balance(contract_address)
transaction = policy_manager.functions.withdraw(contract_address).buildTransaction({'gas': 0})
# Depth for reentrancy is 2: first initial call and then attempt to call again
tx = reentrancy_contract.functions.setData(2, transaction['to'], 0, transaction['data']).transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = testerchain.client.send_transaction({'to': contract_address})
testerchain.wait_for_receipt(tx)
assert balance == testerchain.client.get_balance(contract_address)
assert 2 * rate == policy_manager.functions.nodes(contract_address).call()[FEE_FIELD]
assert 0 == len(withdraw_log.get_all_entries())
# Prepare for refund and check reentrancy protection
tx = escrow.functions.setLastCommittedPeriod(period).transact()
testerchain.wait_for_receipt(tx)
transaction = policy_manager.functions.revokePolicy(policy_id).buildTransaction({'gas': 0})
tx = reentrancy_contract.functions.setData(2, transaction['to'], 0, transaction['data']).transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = testerchain.client.send_transaction({'to': contract_address})
testerchain.wait_for_receipt(tx)
assert balance == testerchain.client.get_balance(contract_address)
assert not policy_manager.functions.policies(policy_id).call()[DISABLED_FIELD]
assert 2 * rate == policy_manager.functions.nodes(contract_address).call()[FEE_FIELD]
assert 0 == len(arrangement_revoked_log.get_all_entries())
assert 0 == len(policy_revoked_log.get_all_entries())
assert 0 == len(arrangement_refund_log.get_all_entries())
assert 0 == len(policy_refund_log.get_all_entries())
def test_revoke_and_default_state(testerchain, escrow, policy_manager):
creator, policy_sponsor, bad_node, node1, node2, node3, *everyone_else = testerchain.client.accounts
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + one_period
current_period = policy_manager.functions.getCurrentPeriod().call()
target_period = current_period + 2
# Create policy
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * rate, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.getNodeFeeDelta(node1, target_period).call() == -rate
testerchain.time_travel(hours=2)
current_period = policy_manager.functions.getCurrentPeriod().call()
assert current_period == target_period
# Create new policy where start is the target period (current)
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == -rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + one_period
tx = policy_manager.functions.createPolicy(policy_id_2, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 4 * rate, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == rate
# Revoke first policy
tx = policy_manager.functions.revokePolicy(policy_id).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == rate

View File

@ -1,87 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from nucypher.blockchain.eth.token import NU
TOTAL_SUPPLY = NU(1_000_000_000, 'NU').to_units()
@pytest.fixture()
def token(testerchain, deploy_contract):
# Create an ERC20 token
token, _ = deploy_contract('NuCypherToken', _totalSupplyOfTokens=TOTAL_SUPPLY)
return token
@pytest.fixture()
def escrow(testerchain, token, deploy_contract):
creator = testerchain.client.accounts[0]
# Creator deploys the escrow
contract, _ = deploy_contract('StakingEscrowForStakingContractMock', token.address)
# Give some coins to the escrow
tx = token.functions.transfer(contract.address, 10000).transact({'from': creator})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture()
def policy_manager(testerchain, deploy_contract):
contract, _ = deploy_contract('PolicyManagerForStakingContractMock')
return contract
@pytest.fixture()
def worklock(testerchain, deploy_contract):
contract, _ = deploy_contract('WorkLockForStakingContractMock')
return contract
@pytest.fixture()
def threshold_staking(testerchain, deploy_contract):
contract, _ = deploy_contract('ThresholdStakingForStakingContractMock')
return contract
@pytest.fixture()
def staking_interface(testerchain,
token,
escrow,
policy_manager,
worklock,
threshold_staking,
deploy_contract):
# Creator deploys the staking interface
contract, _ = deploy_contract(
'ExtendedStakingInterface',
token.address,
escrow.address,
policy_manager.address,
worklock.address,
threshold_staking.address
)
return contract
@pytest.fixture()
def router(testerchain, staking_interface, deploy_contract):
contract, _ = deploy_contract('StakingInterfaceRouter', staking_interface.address)
return contract

View File

@ -1,596 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from eth_tester.exceptions import TransactionFailed
from web3 import Web3
from web3.contract import Contract
from nucypher.blockchain.eth.token import NU
WORKER_FRACTION = 1000
BASIS_FRACTION = 10000
@pytest.fixture()
def pooling_contract(testerchain, router, deploy_contract):
owner = testerchain.client.accounts[1]
worker_owner = testerchain.client.accounts[2]
contract, _ = deploy_contract('PoolingStakingContractV2')
# Initialize
tx = contract.functions.initialize(WORKER_FRACTION, router.address, worker_owner).transact({'from': owner})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture()
def pooling_contract_interface(testerchain, staking_interface, pooling_contract):
return testerchain.client.get_contract(
abi=staking_interface.abi,
address=pooling_contract.address,
ContractFactoryClass=Contract)
def test_staking(testerchain, application_economics, token, escrow, pooling_contract, pooling_contract_interface):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
worker_owner = testerchain.client.accounts[2]
delegators = testerchain.client.accounts[3:6]
deposit_log = pooling_contract.events.TokensDeposited.createFilter(fromBlock='latest')
withdraw_log = pooling_contract.events.TokensWithdrawn.createFilter(fromBlock='latest')
workers_log = pooling_contract.events.WorkerOwnerSet.createFilter(fromBlock=0)
assert pooling_contract.functions.owner().call() == owner
assert pooling_contract.functions.workerOwner().call() == worker_owner
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == 0
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() == 0
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
assert pooling_contract.functions.getAvailableReward().call() == 0
assert pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Give some tokens to delegators
for index, delegator in enumerate(delegators):
tokens = application_economics.min_authorization // (index + 1) * 2
tx = token.functions.transfer(delegator, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Delegators deposit tokens to the pooling contract
total_deposited_tokens = 0
tokens_supply = 0
assert pooling_contract.functions.getAvailableReward().call() == 0
for index, delegator in enumerate(delegators):
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == 0
tokens = token.functions.balanceOf(delegator).call() // 2
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [tokens, 0, 0]
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == 0
total_deposited_tokens += tokens
tokens_supply += tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
events = deposit_log.get_all_entries()
assert len(events) == index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == tokens
assert event_args['depositedTokens'] == tokens
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == 0
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
assert pooling_contract.functions.getAvailableReward().call() == 0
assert pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Delegator can cancel deposit before stake will be created
delegator = delegators[0]
tokens = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert token.functions.balanceOf(delegator).call() == 2 * tokens
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
# Return back
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
# Delegators deposit tokens to the pooling contract again
for index, delegator in enumerate(delegators):
tokens = token.functions.balanceOf(delegator).call()
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [2 * tokens, 0, 0]
total_deposited_tokens += tokens
tokens_supply += tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
events = deposit_log.get_all_entries()
assert len(events) == len(delegators) + index + 2
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == tokens
assert event_args['depositedTokens'] == 2 * tokens
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
# Only owner can deposit tokens to the staking escrow
stake = tokens_supply
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.depositAsStaker(stake).transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
tx = pooling_contract_interface.functions.depositAsStaker(stake).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == 0
assert not pooling_contract.functions.isDepositAllowed().call()
assert not pooling_contract.functions.isWithdrawAllAllowed().call()
# Can't deposit after stake was created
tokens = application_economics.min_authorization
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, 0).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Give some tokens as a reward
assert pooling_contract.functions.getAvailableReward().call() == 0
reward = application_economics.min_authorization
tx = token.functions.approve(escrow.address, reward).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.deposit(pooling_contract.address, reward).transact()
testerchain.wait_for_receipt(tx)
assert not pooling_contract.functions.isDepositAllowed().call()
assert not pooling_contract.functions.isWithdrawAllAllowed().call()
# Only owner can withdraw tokens from the staking escrow
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.withdrawAsStaker(reward).transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.getAvailableReward().call() == 0
tx = pooling_contract_interface.functions.withdrawAsStaker(reward).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.getAvailableReward().call() == reward
worker_reward = reward * WORKER_FRACTION // BASIS_FRACTION
assert pooling_contract.functions.getAvailableWorkerReward().call() == worker_reward
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == reward
tokens_supply = reward
total_withdrawn_tokens = 0
assert not pooling_contract.functions.isDepositAllowed().call()
assert not pooling_contract.functions.isWithdrawAllAllowed().call()
# Can't withdraw initial deposit before stake will be unlocked
for delegator in delegators:
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
# Each delegator can withdraw some portion of tokens
available_reward = reward
for index, delegator in enumerate(delegators):
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(total_deposited_tokens * BASIS_FRACTION)
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawTokens(max_portion + 1).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
portion = max_portion // 2
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == max_portion
tx = pooling_contract.functions.withdrawTokens(portion).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [deposited_tokens, portion, 0]
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == max_portion - portion
tokens_supply -= portion
total_withdrawn_tokens += portion
available_reward -= portion
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == portion
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
events = withdraw_log.get_all_entries()
assert len(events) == index + 2
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == portion
assert event_args['depositedTokens'] == deposited_tokens
# Node owner withdraws tokens
assert pooling_contract.functions.getAvailableWorkerReward().call() == worker_reward
assert pooling_contract.functions.getAvailableReward().call() == available_reward
# Only node owner can call this method
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == worker_reward
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
tokens_supply -= worker_reward
total_withdrawn_tokens += worker_reward
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(owner).call() == 0
assert token.functions.balanceOf(worker_owner).call() == worker_reward
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert pooling_contract.functions.getAvailableReward().call() == available_reward - worker_reward
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 2
event_args = events[-1]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == worker_reward
assert event_args['depositedTokens'] == 0
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
# Withdraw stake
tx = pooling_contract_interface.functions.withdrawAsStaker(stake - 1).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert not pooling_contract.functions.isWithdrawAllAllowed().call()
tx = pooling_contract_interface.functions.withdrawAsStaker(1).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tokens_supply += stake
assert not pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Each delegator can withdraw rest of reward and deposit
previous_total_deposited_tokens = total_deposited_tokens
withdrawn_worker_reward = worker_reward
# Withdraw everything from one delegator and check others rewards
delegator = delegators[0]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
withdrawn_tokens = pooling_contract.functions.delegators(delegator).call()[1]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(previous_total_deposited_tokens * BASIS_FRACTION)
supposed_portion = max_portion // 2
reward_portion = pooling_contract.functions.getAvailableDelegatorReward(delegator).call()
# could be some rounding errors
assert abs(supposed_portion - reward_portion) <= 10
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
tokens_supply -= new_portion
total_deposited_tokens -= deposited_tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == 0
withdraw_to_decrease = withdrawn_worker_reward * deposited_tokens // previous_total_deposited_tokens
total_withdrawn_tokens -= withdraw_to_decrease
total_withdrawn_tokens -= withdrawn_tokens
withdrawn_worker_reward -= withdraw_to_decrease
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert abs(pooling_contract.functions.workerWithdrawnReward().call() - withdrawn_worker_reward) <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 3
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
assert not pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Check worker's reward, still zero
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
# Check others rewards
for delegator in delegators[1:3]:
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(previous_total_deposited_tokens * BASIS_FRACTION)
supposed_portion = max_portion // 2
reward_portion = pooling_contract.functions.getAvailableDelegatorReward(delegator).call()
# could be some rounding errors
assert abs(supposed_portion - reward_portion) <= 10
# Increase reward for delegators and worker
new_reward = NU(15_000, 'NU').to_units() // 2
tx = token.functions.transfer(pooling_contract.address, new_reward).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tokens_supply += new_reward
new_worker_reward = new_reward * WORKER_FRACTION // BASIS_FRACTION
assert new_worker_reward > 0
assert abs(pooling_contract.functions.getAvailableWorkerReward().call() - new_worker_reward) <= 1
new_worker_reward = pooling_contract.functions.getAvailableWorkerReward().call()
# Withdraw everything from one delegator and check others rewards
previous_total_deposited_tokens = total_deposited_tokens
delegator = delegators[1]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
withdrawn_tokens = pooling_contract.functions.delegators(delegator).call()[1]
reward_portion = pooling_contract.functions.getAvailableDelegatorReward(delegator).call()
other_reward_portion = pooling_contract.functions.getAvailableDelegatorReward(delegators[2]).call()
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
tokens_supply -= new_portion
total_deposited_tokens -= deposited_tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
new_worker_transfer = new_worker_reward * deposited_tokens // previous_total_deposited_tokens
tokens_supply -= new_worker_transfer
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward + new_worker_transfer
assert pooling_contract.functions.getAvailableDelegatorReward(delegator).call() == 0
withdraw_to_decrease = withdrawn_worker_reward * deposited_tokens // previous_total_deposited_tokens
total_withdrawn_tokens -= withdraw_to_decrease
total_withdrawn_tokens -= withdrawn_tokens
withdrawn_worker_reward -= withdraw_to_decrease
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert abs(pooling_contract.functions.workerWithdrawnReward().call() - withdrawn_worker_reward) <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 5
event_args = events[-2]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == new_worker_transfer
assert event_args['depositedTokens'] == 0
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
# Check worker's reward
new_worker_reward = new_worker_reward * (previous_total_deposited_tokens - deposited_tokens) // previous_total_deposited_tokens
assert pooling_contract.functions.getAvailableWorkerReward().call() == new_worker_reward
# Check others rewards
assert abs(pooling_contract.functions.getAvailableDelegatorReward(delegators[2]).call() - other_reward_portion) <= 10
assert not pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Withdraw last portion for last delegator
delegator = delegators[2]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
reward_portion = pooling_contract.functions.getAvailableDelegatorReward(delegator).call()
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() <= 1
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward + new_worker_transfer + new_worker_reward
assert pooling_contract.functions.getAvailableReward().call() <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 7
event_args = events[-2]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == new_worker_reward
assert event_args['depositedTokens'] == 0
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
assert not pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
#
# Change worker owner
#
# Prepare reward
tx = token.functions.transfer(pooling_contract.address, new_reward).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert not pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
# Only pool owner can change value
new_worker_owner = delegators[0]
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.setWorkerOwner(new_worker_owner).transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.setWorkerOwner(new_worker_owner).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Now old worker owner can't withdraw reward
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
# But new can
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': new_worker_owner})
testerchain.wait_for_receipt(tx)
events = workers_log.get_all_entries()
assert len(events) == 2
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['workerOwner'] == worker_owner
event_args = events[1]['args']
assert event_args['sender'] == owner
assert event_args['workerOwner'] == new_worker_owner
# There are no reward or locked tokens, deposit is allowed again
assert pooling_contract.functions.isDepositAllowed().call()
assert pooling_contract.functions.isWithdrawAllAllowed().call()
delegator = delegators[0]
tokens = application_economics.min_authorization
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
def test_fee(testerchain, application_economics, token, policy_manager, pooling_contract, pooling_contract_interface):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
delegators = testerchain.client.accounts[2:5]
withdraw_log = pooling_contract.events.ETHWithdrawn.createFilter(fromBlock='latest')
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert pooling_contract.functions.totalWithdrawnETH().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() == 0
# Give some tokens to delegators and deposit them
for index, delegator in enumerate(delegators):
tokens = application_economics.min_authorization // (index + 1)
tx = token.functions.transfer(delegator, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
total_deposited_tokens = pooling_contract.functions.totalDepositedTokens().call()
assert pooling_contract.functions.totalWithdrawnETH().call() == 0
assert testerchain.client.get_balance(pooling_contract.address) == 0
# Give some fees
value = Web3.toWei(1, 'ether')
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': policy_manager.address, 'value': value})
testerchain.wait_for_receipt(tx)
# Only owner can withdraw fees from the policy manager
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.withdrawPolicyFee().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
balance = testerchain.client.get_balance(owner)
tx = pooling_contract_interface.functions.withdrawPolicyFee().transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert testerchain.client.get_balance(pooling_contract.address) == value
assert testerchain.client.get_balance(owner) == balance
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
withdrawn_eth = 0
eth_supply = value
# Each delegator can withdraw portion of eth
for index, delegator in enumerate(delegators):
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = value * deposited_tokens // total_deposited_tokens
balance = testerchain.client.get_balance(delegator)
assert pooling_contract.functions.getAvailableDelegatorETH(delegator).call() == max_portion
tx = pooling_contract.functions.withdrawETH().transact({'from': delegator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [deposited_tokens, 0, max_portion]
eth_supply -= max_portion
withdrawn_eth += max_portion
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert testerchain.client.get_balance(pooling_contract.address) == eth_supply
assert testerchain.client.get_balance(delegator) == balance + max_portion
assert pooling_contract.functions.totalWithdrawnETH().call() == withdrawn_eth
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawETH().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
events = withdraw_log.get_all_entries()
assert len(events) == index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == max_portion
def test_reentrancy(testerchain, pooling_contract, token, deploy_contract):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
# Prepare contracts
reentrancy_contract, _ = deploy_contract('ReentrancyTest')
contract_address = reentrancy_contract.address
tx = pooling_contract.functions.transferOwnership(contract_address).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Transfer ETH to the contract
value = Web3.toWei(1, 'ether')
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': pooling_contract.address, 'value': value})
testerchain.wait_for_receipt(tx)
assert testerchain.client.get_balance(pooling_contract.address) == value
# Change eth distribution, owner will be able to withdraw only half
tokens = WORKER_FRACTION
tx = token.functions.transfer(owner, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Try to withdraw ETH twice
balance = testerchain.w3.eth.getBalance(contract_address)
transaction = pooling_contract.functions.withdrawETH().buildTransaction({'gas': 0})
tx = reentrancy_contract.functions.setData(1, transaction['to'], 0, transaction['data']).transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = testerchain.client.send_transaction({'to': contract_address})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(contract_address) == balance
# TODO same test for withdrawAll()

View File

@ -1,386 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from eth_tester.exceptions import TransactionFailed
from web3.contract import Contract
from nucypher.blockchain.eth.constants import NULL_ADDRESS
@pytest.fixture()
def staking_contract(testerchain, router, deploy_contract):
creator = testerchain.client.accounts[0]
user = testerchain.client.accounts[1]
contract, _ = deploy_contract('SimpleStakingContract', router.address)
# Transfer ownership
tx = contract.functions.transferOwnership(user).transact({'from': creator})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture()
def staking_contract_interface(testerchain, staking_interface, staking_contract):
return testerchain.client.get_contract(
abi=staking_interface.abi,
address=staking_contract.address,
ContractFactoryClass=Contract)
def test_nonexistent_method(testerchain, policy_manager, staking_contract):
"""
Test that interface executes only predefined methods
"""
owner = testerchain.client.accounts[1]
# Create fake instance of the user escrow contract
fake_preallocation_escrow = testerchain.client.get_contract(
abi=policy_manager.abi,
address=staking_contract.address,
ContractFactoryClass=Contract)
# Can't execute method that not in the interface
with pytest.raises((TransactionFailed, ValueError)):
tx = fake_preallocation_escrow.functions.additionalMethod(1).transact({'from': owner})
testerchain.wait_for_receipt(tx)
def test_staker(testerchain, token, escrow, staking_contract, staking_contract_interface, staking_interface):
"""
Test staker functions in the staking interface
"""
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
# Owner can't use the staking interface directly
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.withdrawAsStaker(100).transact({'from': owner})
testerchain.wait_for_receipt(tx)
staker_withdraws = staking_contract_interface.events.WithdrawnAsStaker.createFilter(fromBlock='latest')
# Use stakers methods through the staking contract
value = 1600
escrow_balance = token.functions.balanceOf(escrow.address).call()
tx = token.functions.transfer(staking_contract.address, 10 * value).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.depositAsStaker(2 * value).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.withdrawAsStaker(value).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert escrow.functions.value().call() == value
assert token.functions.balanceOf(escrow.address).call() == escrow_balance + value
assert token.functions.balanceOf(staking_contract.address).call() == 9 * value
events = staker_withdraws.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['value'] == value
def test_policy(testerchain, policy_manager, staking_contract, staking_contract_interface):
"""
Test policy manager functions in the staking interface
"""
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
owner_balance = testerchain.client.get_balance(owner)
# Nothing to withdraw
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.withdrawPolicyFee().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert owner_balance == testerchain.client.get_balance(owner)
assert 0 == testerchain.client.get_balance(staking_contract.address)
# Send ETH to the policy manager as a fee for the owner
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': policy_manager.address, 'value': 10000})
testerchain.wait_for_receipt(tx)
staker_fee = staking_contract_interface.events.PolicyFeeWithdrawn.createFilter(fromBlock='latest')
# Only owner can withdraw fee
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.withdrawPolicyFee().transact({'from': creator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Owner withdraws fee
tx = staking_contract_interface.functions.withdrawPolicyFee().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 10000 == testerchain.client.get_balance(staking_contract.address)
assert owner_balance == testerchain.client.get_balance(owner)
assert 0 == testerchain.client.get_balance(policy_manager.address)
assert 10000 == testerchain.client.get_balance(staking_contract.address)
events = staker_fee.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert owner == event_args['sender']
assert 10000 == event_args['value']
# Only owner can set min fee rate
min_fee_sets = staking_contract_interface.events.MinFeeRateSet.createFilter(fromBlock='latest')
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.setMinFeeRate(333).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.setMinFeeRate(222).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert 222 == policy_manager.functions.minFeeRate().call()
events = min_fee_sets.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert owner == event_args['sender']
assert 222 == event_args['value']
def test_worklock(testerchain, worklock, staking_contract, staking_contract_interface, staking_interface):
"""
Test worklock functions in the staking interface
"""
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
bids = staking_contract_interface.events.Bid.createFilter(fromBlock='latest')
claims = staking_contract_interface.events.Claimed.createFilter(fromBlock='latest')
refunds = staking_contract_interface.events.Refund.createFilter(fromBlock='latest')
cancellations = staking_contract_interface.events.BidCanceled.createFilter(fromBlock='latest')
compensations = staking_contract_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': staking_contract.address, 'value': 2 * bid})
testerchain.wait_for_receipt(tx)
# Bid
assert worklock.functions.depositedETH().call() == 0
assert testerchain.client.get_balance(staking_contract.address) == 2 * bid
tx = staking_contract_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(staking_contract.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 = staking_contract_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(staking_contract.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.compensationValue().call() == compensation
tx = staking_contract_interface.functions.withdrawCompensation().transact({'from': owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.compensationValue().call() == 0
assert testerchain.client.get_balance(staking_contract.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 = staking_contract_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 = staking_contract_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(staking_contract.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
def test_interface_without_worklock(testerchain,
deploy_contract,
token,
escrow,
policy_manager,
worklock,
threshold_staking):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
staking_interface, _ = deploy_contract(
'StakingInterface',
token.address,
escrow.address,
policy_manager.address,
worklock.address,
threshold_staking.address
)
router, _ = deploy_contract('StakingInterfaceRouter', staking_interface.address)
staking_contract, _ = deploy_contract('SimpleStakingContract', router.address)
# Transfer ownership
tx = staking_contract.functions.transferOwnership(owner).transact({'from': creator})
testerchain.wait_for_receipt(tx)
staking_contract_interface = testerchain.client.get_contract(
abi=staking_interface.abi,
address=staking_contract.address,
ContractFactoryClass=Contract)
# All worklock methods work
tx = staking_contract_interface.functions.bid(0).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.cancelBid().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.withdrawCompensation().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.claim().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface.functions.refund().transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Test interface without worklock
staking_interface, _ = deploy_contract(
'StakingInterface',
token.address,
escrow.address,
policy_manager.address,
NULL_ADDRESS,
threshold_staking.address
)
tx = router.functions.upgrade(staking_interface.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Current version of interface doesn't have worklock contract
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.bid(0).transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.cancelBid().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.withdrawCompensation().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.claim().transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface.functions.refund().transact({'from': owner})
testerchain.wait_for_receipt(tx)
def test_threshold_staking(testerchain,
threshold_staking,
staking_contract,
staking_contract_interface,
staking_interface):
"""
Test Threshold staking functions in the staking interface
"""
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
operator = testerchain.client.accounts[2]
beneficiary = testerchain.client.accounts[3]
authorizer = testerchain.client.accounts[4]
stakes = staking_contract_interface.events.ThresholdNUStaked.createFilter(fromBlock='latest')
unstakes = staking_contract_interface.events.ThresholdNUUnstaked.createFilter(fromBlock='latest')
# Owner can't use the staking interface directly
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.stakeNu(operator, beneficiary, authorizer).transact({'from': owner})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_interface.functions.unstakeNu(operator, 1).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Stake NU
assert threshold_staking.functions.stakedNuInT().call() == 0
tx = staking_contract_interface.functions.stakeNu(operator, beneficiary, authorizer).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert threshold_staking.functions.operator().call() == operator
assert threshold_staking.functions.beneficiary().call() == beneficiary
assert threshold_staking.functions.authorizer().call() == authorizer
assert threshold_staking.functions.stakedNuInT().call() != 0
events = stakes.get_all_entries()
assert len(events) == 1
event_args = events[-1]['args']
assert event_args['sender'] == owner
assert event_args['operator'] == operator
assert event_args['beneficiary'] == beneficiary
assert event_args['authorizer'] == authorizer
# Unstake NU
staked = threshold_staking.functions.stakedNuInT().call()
unstaked = staked // 3
tx = staking_contract_interface.functions.unstakeNu(operator, unstaked).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert threshold_staking.functions.stakedNuInT().call() == staked - unstaked
events = unstakes.get_all_entries()
assert len(events) == 1
event_args = events[0]['args']
assert event_args['sender'] == owner
assert event_args['operator'] == operator
assert event_args['amount'] == unstaked

View File

@ -1,166 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from eth_tester.exceptions import TransactionFailed
from web3.contract import Contract
from web3.exceptions import BadFunctionCallOutput
from nucypher.blockchain.eth.constants import NULL_ADDRESS
def test_upgrading(testerchain, token, deploy_contract, escrow):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': owner, 'value': 1})
testerchain.wait_for_receipt(tx)
interface_v1, _ = deploy_contract('StakingInterfaceMockV1')
interface_v2, _ = deploy_contract('StakingInterfaceMockV2')
router_contract, _ = deploy_contract('StakingInterfaceRouter', interface_v1.address)
staking_contract_contract, _ = deploy_contract('SimpleStakingContract', router_contract.address)
# Transfer ownership
tx = staking_contract_contract.functions.transferOwnership(owner).transact({'from': creator})
testerchain.wait_for_receipt(tx)
staking_contract_interface_v1 = testerchain.client.get_contract(
abi=interface_v1.abi,
address=staking_contract_contract.address,
ContractFactoryClass=Contract)
staking_contract_interface_v2 = testerchain.client.get_contract(
abi=interface_v2.abi,
address=staking_contract_contract.address,
ContractFactoryClass=Contract)
# Check existed methods and that only owner can call them
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface_v1.functions.firstMethod().transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface_v1.functions.firstMethod().transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert 20 == staking_contract_interface_v1.functions.secondMethod().call({'from': owner})
# Nonexistent methods can't be called
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface_v2.functions.thirdMethod().transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Anyone can send ETH
tx = testerchain.client.send_transaction(
{'from': creator, 'to': staking_contract_contract.address, 'value': 1, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert 1 == testerchain.client.get_balance(staking_contract_contract.address)
# Only creator can update a library
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(interface_v2.address).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert interface_v1.address == router_contract.functions.target().call()
tx = router_contract.functions.upgrade(interface_v2.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert interface_v2.address == router_contract.functions.target().call()
# Method with old signature is not working
with pytest.raises((TransactionFailed, ValueError)):
tx = staking_contract_interface_v1.functions.firstMethod().transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Method with old signature that available in new ABI is working
assert 15 == staking_contract_interface_v1.functions.secondMethod().call({'from': owner})
# New ABI is working
assert 15 == staking_contract_interface_v2.functions.secondMethod().call({'from': owner})
tx = staking_contract_interface_v2.functions.firstMethod(10).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = staking_contract_interface_v2.functions.thirdMethod().transact({'from': owner})
testerchain.wait_for_receipt(tx)
def test_interface_selfdestruct(testerchain, token, deploy_contract):
creator = testerchain.client.accounts[0]
account = testerchain.client.accounts[1]
# Deploy interface and destroy it
interface1, _ = deploy_contract('DestroyableStakingInterface')
assert 15 == interface1.functions.method().call()
tx = interface1.functions.destroy().transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((BadFunctionCallOutput, ValueError)):
interface1.functions.method().call()
# Can't create router using address without contract
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('StakingInterfaceRouter', NULL_ADDRESS)
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('StakingInterfaceRouter', account)
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('StakingInterfaceRouter', interface1.address)
# Deploy contract again with a router targeting it
interface2, _ = deploy_contract('DestroyableStakingInterface')
router_contract, _ = deploy_contract('StakingInterfaceRouter', interface2.address)
assert interface2.address == router_contract.functions.target().call()
# Can't create contracts using wrong addresses
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract(
'BaseStakingInterface', token.address, token.address, token.address, token.address, token.address)
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('StakingInterfaceRouter', token.address)
# Deploy staking contract
staking_contract, _ = deploy_contract('SimpleStakingContract', router_contract.address)
staking_contract_interface = testerchain.client.get_contract(
abi=interface1.abi,
address=staking_contract.address,
ContractFactoryClass=Contract)
assert 15 == staking_contract_interface.functions.method().call()
# Can't upgrade to an address without contract
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(NULL_ADDRESS).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(account).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(interface1.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Destroy library
tx = interface2.functions.destroy().transact()
testerchain.wait_for_receipt(tx)
# Staking contract must determine that there is no contract
with pytest.raises((TransactionFailed, ValueError)):
staking_contract_interface.functions.method().call()
# Can't upgrade to an address without contract
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(NULL_ADDRESS).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(account).transact({'from': creator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = router_contract.functions.upgrade(interface1.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Deploy the same contract again and upgrade to this contract
contract3_lib, _ = deploy_contract('DestroyableStakingInterface')
tx = router_contract.functions.upgrade(contract3_lib.address).transact({'from': creator})
testerchain.wait_for_receipt(tx)
assert 15 == staking_contract_interface.functions.method().call()

View File

@ -31,7 +31,6 @@ from nucypher.blockchain.eth.deployers import (
AdjudicatorDeployer,
BaseContractDeployer,
NucypherTokenDeployer,
PolicyManagerDeployer,
StakingEscrowDeployer,
WorklockDeployer
)
@ -126,7 +125,6 @@ CONSTRUCTOR_OVERRIDES = {
FORCE_SKIP = {
StakingEscrowDeployer.contract_name: ["v5.6.1", "v6.2.1"],
PolicyManagerDeployer.contract_name: ["v6.2.1"]
}

View File

@ -24,7 +24,6 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
PolicyManagerAgent,
StakingEscrowAgent,
WorkLockAgent
)
@ -83,13 +82,6 @@ def mock_adjudicator_agent(mock_testerchain, application_economics, mock_contrac
mock_agent.reset()
@pytest.fixture(scope='function', autouse=True)
def mock_policy_manager_agent(mock_testerchain, application_economics, mock_contract_agency):
mock_agent = mock_contract_agency.get_agent(PolicyManagerAgent)
yield mock_agent
mock_agent.reset()
@pytest.fixture(scope='function')
def mock_stdin(mocker):