mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2866 from vzotova/cleaning-contracts-pt1
Removes `PolicyManager` and `StakingInterface` contractspull/2868/head
commit
19cb0486dd
|
@ -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.
|
|
@ -28,12 +28,3 @@ Upgradeability and proxies
|
|||
:glob:
|
||||
|
||||
proxy/*
|
||||
|
||||
Staking and Pooling Contracts
|
||||
=============================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
staking/*
|
||||
|
|
|
@ -137,7 +137,6 @@ Whitepapers
|
|||
architecture/sub_stakes
|
||||
architecture/slashing
|
||||
architecture/periods
|
||||
architecture/staking_contracts
|
||||
.. TODO perhaps categorize architecture section
|
||||
|
||||
.. toctree::
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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_;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
"""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -31,7 +31,6 @@ from nucypher.blockchain.eth.deployers import (
|
|||
AdjudicatorDeployer,
|
||||
BaseContractDeployer,
|
||||
NucypherTokenDeployer,
|
||||
PolicyManagerDeployer,
|
||||
StakingEscrowDeployer
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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"]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
Loading…
Reference in New Issue