Merge pull request #2868 from vzotova/cleaning-contracts-pt2

Removes `WorkLock`
pull/2869/head
Derek Pierre 2022-02-10 12:44:20 -05:00 committed by GitHub
commit 788992381b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 22 additions and 5093 deletions

View File

@ -20,16 +20,13 @@ import json
import time
from decimal import Decimal
from typing import Callable, Union
from typing import Dict, Iterable, List, Optional, Tuple
from typing import Iterable, List, Optional, Tuple
import maya
from constant_sorrow.constants import FULL
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
from eth_typing import ChecksumAddress
from eth_utils import to_canonical_address
from hexbytes import HexBytes
from web3 import Web3
from web3.exceptions import ValidationError
from web3.types import TxReceipt
from nucypher.acumen.nicknames import Nickname
@ -42,14 +39,10 @@ from nucypher.blockchain.eth.agents import (
ContractAgency,
NucypherTokenAgent,
StakingEscrowAgent,
WorkLockAgent,
PREApplicationAgent
)
from nucypher.blockchain.eth.constants import (
NULL_ADDRESS,
POLICY_MANAGER_CONTRACT_NAME,
DISPATCHER_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME,
)
from nucypher.blockchain.eth.decorators import (
only_me,
@ -61,7 +54,6 @@ from nucypher.blockchain.eth.deployers import (
BaseContractDeployer,
NucypherTokenDeployer,
StakingEscrowDeployer,
WorklockDeployer,
PREApplicationDeployer,
SubscriptionManagerDeployer
)
@ -80,8 +72,7 @@ from nucypher.blockchain.eth.token import (
)
from nucypher.blockchain.eth.utils import (
calculate_period_duration,
datetime_to_period,
prettify_eth_amount
datetime_to_period
)
from nucypher.characters.banners import STAKEHOLDER_BANNER
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
@ -201,7 +192,7 @@ class ContractAdministrator(BaseActor):
)
aux_deployer_classes = (
WorklockDeployer,
# Add more deployer classes here
)
# For ownership transfers.
@ -1088,273 +1079,3 @@ class StakeHolder:
stake = sum(staking_agent.owned_tokens(staker_address=account) for account in self.signer.accounts)
nu_stake = NU.from_units(stake)
return nu_stake
class Bidder(NucypherTokenActor):
"""WorkLock participant"""
class BidderError(NucypherTokenActor.ActorError):
pass
class BiddingIsOpen(BidderError):
pass
class BiddingIsClosed(BidderError):
pass
class CancellationWindowIsOpen(BidderError):
pass
class CancellationWindowIsClosed(BidderError):
pass
class ClaimError(BidderError):
pass
class WhaleError(BidderError):
pass
@validate_checksum_address
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log = Logger(f"WorkLockBidder")
self.worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=self.registry) # type: WorkLockAgent
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry) # type: StakingEscrowAgent
self.economics = EconomicsFactory.get_economics(registry=self.registry)
self._all_bonus_bidders = None
def ensure_bidding_is_open(self, message: str = None) -> None:
now = self.worklock_agent.blockchain.get_blocktime()
start = self.worklock_agent.start_bidding_date
end = self.worklock_agent.end_bidding_date
if now < start:
message = message or f'Bidding does not open until {maya.MayaDT(start).slang_date()}'
raise self.BiddingIsClosed(message)
if now >= end:
message = message or f'Bidding closed at {maya.MayaDT(end).slang_date()}'
raise self.BiddingIsClosed(message)
def _ensure_bidding_is_closed(self, message: str = None) -> None:
now = self.worklock_agent.blockchain.get_blocktime()
end = self.worklock_agent.end_bidding_date
if now < end:
message = message or f"Bidding does not close until {maya.MayaDT(end).slang_date()}"
raise self.BiddingIsOpen(message)
def _ensure_cancellation_window(self, ensure_closed: bool = True, message: str = None) -> None:
now = self.worklock_agent.blockchain.get_blocktime()
end = self.worklock_agent.end_cancellation_date
if ensure_closed and now < end:
message = message or f"Operation cannot be performed while the cancellation window is still open " \
f"(closes at {maya.MayaDT(end).slang_date()})."
raise self.CancellationWindowIsOpen(message)
elif not ensure_closed and now >= end:
message = message or f"Operation is allowed only while the cancellation window is open " \
f"(closed at {maya.MayaDT(end).slang_date()})."
raise self.CancellationWindowIsClosed(message)
#
# Transactions
#
def place_bid(self, value: int) -> TxReceipt:
self.ensure_bidding_is_open()
minimum = self.worklock_agent.minimum_allowed_bid
if not self.get_deposited_eth and value < minimum:
raise self.BidderError(f"{prettify_eth_amount(value)} is too small a value for bidding; "
f"bid must be at least {prettify_eth_amount(minimum)}")
receipt = self.worklock_agent.bid(transacting_power=self.transacting_power, value=value)
return receipt
def claim(self) -> TxReceipt:
# Require the cancellation window is closed
self._ensure_cancellation_window(ensure_closed=True)
if not self.worklock_agent.is_claiming_available():
raise self.ClaimError(f"Claiming is not available yet")
# Ensure the claim was not already placed
if self.has_claimed:
raise self.ClaimError(f"Bidder {self.checksum_address} already placed a claim.")
# Require an active bid
if not self.get_deposited_eth:
raise self.ClaimError(f"No bids available for {self.checksum_address}")
receipt = self.worklock_agent.claim(transacting_power=self.transacting_power)
return receipt
def cancel_bid(self) -> TxReceipt:
self._ensure_cancellation_window(ensure_closed=False)
# Require an active bid
if not self.get_deposited_eth:
self.BidderError(f"No bids available for {self.checksum_address}")
receipt = self.worklock_agent.cancel_bid(transacting_power=self.transacting_power)
return receipt
def _get_max_bonus_bid_from_max_stake(self) -> int:
"""Returns maximum allowed bid calculated from maximum allowed locked tokens"""
max_bonus_tokens = self.economics.maximum_allowed_locked - self.economics.min_authorization
bonus_eth_supply = sum(
self._all_bonus_bidders.values()) if self._all_bonus_bidders else self.worklock_agent.get_bonus_eth_supply()
bonus_worklock_supply = self.worklock_agent.get_bonus_lot_value()
max_bonus_bid = max_bonus_tokens * bonus_eth_supply // bonus_worklock_supply
return max_bonus_bid
def get_whales(self, force_read: bool = False) -> Dict[str, int]:
"""Returns all worklock bidders over the whale threshold as a dictionary of addresses and bonus bid values."""
max_bonus_bid_from_max_stake = self._get_max_bonus_bid_from_max_stake()
bidders = dict()
for bidder, bid in self._get_all_bonus_bidders(force_read).items():
if bid > max_bonus_bid_from_max_stake:
bidders[bidder] = bid
return bidders
def _get_all_bonus_bidders(self, force_read: bool = False) -> dict:
if not force_read and self._all_bonus_bidders:
return self._all_bonus_bidders
bidders = self.worklock_agent.get_bidders()
min_bid = self.economics.worklock_min_allowed_bid
self._all_bonus_bidders = dict()
for bidder in bidders:
bid = self.worklock_agent.get_deposited_eth(bidder)
if bid > min_bid:
self._all_bonus_bidders[bidder] = bid - min_bid
return self._all_bonus_bidders
def _reduce_bids(self, whales: dict):
min_whale_bonus_bid = min(whales.values())
max_whale_bonus_bid = max(whales.values())
# first step - align at a minimum bid
if min_whale_bonus_bid != max_whale_bonus_bid:
whales = dict.fromkeys(whales.keys(), min_whale_bonus_bid)
self._all_bonus_bidders.update(whales)
bonus_eth_supply = sum(self._all_bonus_bidders.values())
bonus_worklock_supply = self.worklock_agent.get_bonus_lot_value()
max_bonus_tokens = self.economics.maximum_allowed_locked - self.economics.min_authorization
if (min_whale_bonus_bid * bonus_worklock_supply) // bonus_eth_supply <= max_bonus_tokens:
raise self.WhaleError(f"At least one of bidders {whales} has allowable bid")
a = min_whale_bonus_bid * bonus_worklock_supply - max_bonus_tokens * bonus_eth_supply
b = bonus_worklock_supply - max_bonus_tokens * len(whales)
refund = -(-a // b) # div ceil
min_whale_bonus_bid -= refund
whales = dict.fromkeys(whales.keys(), min_whale_bonus_bid)
self._all_bonus_bidders.update(whales)
return whales
def force_refund(self) -> TxReceipt:
self._ensure_cancellation_window(ensure_closed=True)
whales = self.get_whales()
if not whales:
raise self.WhaleError(f"Force refund aborted: No whales detected and all bids qualify for claims.")
new_whales = whales.copy()
while new_whales:
whales.update(new_whales)
whales = self._reduce_bids(whales)
new_whales = self.get_whales()
receipt = self.worklock_agent.force_refund(transacting_power=self.transacting_power,
addresses=list(whales.keys()))
if self.get_whales(force_read=True):
raise RuntimeError(f"Internal error: offline simulation differs from transaction results")
return receipt
# TODO better control: max iterations, interactive mode
def verify_bidding_correctness(self, gas_limit: int) -> dict:
self._ensure_cancellation_window(ensure_closed=True)
if self.worklock_agent.bidders_checked():
raise self.BidderError(f"Check was already done")
whales = self.get_whales()
if whales:
raise self.WhaleError(f"Some bidders have bids that are too high: {whales}")
self.log.debug(f"Starting bidding verification. Next bidder to check: {self.worklock_agent.next_bidder_to_check()}")
receipts = dict()
iteration = 1
while not self.worklock_agent.bidders_checked():
receipt = self.worklock_agent.verify_bidding_correctness(transacting_power=self.transacting_power,
gas_limit=gas_limit)
self.log.debug(f"Iteration {iteration}. Next bidder to check: {self.worklock_agent.next_bidder_to_check()}")
receipts[iteration] = receipt
iteration += 1
return receipts
def refund_deposit(self) -> dict:
"""Refund ethers for completed work"""
if not self.available_refund:
raise self.BidderError(f'There is no refund available for {self.checksum_address}')
receipt = self.worklock_agent.refund(transacting_power=self.transacting_power)
return receipt
def withdraw_compensation(self) -> TxReceipt:
"""Withdraw compensation after force refund"""
if not self.available_compensation:
raise self.BidderError(f'There is no compensation available for {self.checksum_address}; '
f'Did you mean to call "refund"?')
receipt = self.worklock_agent.withdraw_compensation(transacting_power=self.transacting_power)
return receipt
#
# Calls
#
@property
def get_deposited_eth(self) -> int:
bid = self.worklock_agent.get_deposited_eth(checksum_address=self.checksum_address)
return bid
@property
def has_claimed(self) -> bool:
has_claimed = self.worklock_agent.check_claim(self.checksum_address)
return has_claimed
@property
def completed_work(self) -> int:
work = self.staking_agent.get_completed_work(bidder_address=self.checksum_address)
completed_work = work - self.refunded_work
return completed_work
@property
def remaining_work(self) -> int:
try:
work = self.worklock_agent.get_remaining_work(checksum_address=self.checksum_address)
except (TestTransactionFailed, ValidationError, ValueError): # TODO: 1950
work = 0
return work
@property
def refunded_work(self) -> int:
work = self.worklock_agent.get_refunded_work(checksum_address=self.checksum_address)
return work
@property
def available_refund(self) -> int:
refund_eth = self.worklock_agent.get_available_refund(checksum_address=self.checksum_address)
return refund_eth
@property
def available_compensation(self) -> int:
compensation_eth = self.worklock_agent.get_available_compensation(checksum_address=self.checksum_address)
return compensation_eth
@property
def available_claim(self) -> int:
tokens = self.worklock_agent.eth_to_tokens(self.get_deposited_eth)
return tokens

View File

@ -26,12 +26,11 @@ from constant_sorrow.constants import ( # type: ignore
TRANSACTION,
CONTRACT_ATTRIBUTE
)
from eth_typing.encoding import HexStr
from eth_typing.evm import ChecksumAddress
from eth_utils.address import to_checksum_address
from hexbytes.main import HexBytes
from web3.contract import Contract, ContractFunction
from web3.types import Wei, Timestamp, TxReceipt, TxParams, Nonce
from web3.types import Wei, Timestamp, TxReceipt, TxParams
from nucypher.blockchain.eth.constants import (
ADJUDICATOR_CONTRACT_NAME,
@ -39,10 +38,8 @@ from nucypher.blockchain.eth.constants import (
ETH_ADDRESS_BYTE_LENGTH,
NUCYPHER_TOKEN_CONTRACT_NAME,
NULL_ADDRESS,
POLICY_MANAGER_CONTRACT_NAME,
SUBSCRIPTION_MANAGER_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME,
WORKLOCK_CONTRACT_NAME,
PRE_APPLICATION_CONTRACT_NAME
)
from nucypher.blockchain.eth.decorators import contract_api
@ -62,7 +59,6 @@ from nucypher.types import (
RawSubStakeInfo,
Period,
Work,
WorklockParameters,
StakerFlags,
StakerInfo,
StakingProviderInfo,
@ -1140,284 +1136,6 @@ class PREApplicationAgent(EthereumContractAgent):
return receipt
class WorkLockAgent(EthereumContractAgent):
contract_name: str = WORKLOCK_CONTRACT_NAME
_excluded_interfaces = ('shutdown', 'tokenDeposit')
#
# Transactions
#
@contract_api(TRANSACTION)
def bid(self, value: Wei, transacting_power: TransactingPower) -> TxReceipt:
"""Bid for NU tokens with ETH."""
contract_function: ContractFunction = self.contract.functions.bid()
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power,
payload={'value': value})
return receipt
@contract_api(TRANSACTION)
def cancel_bid(self, transacting_power: TransactingPower) -> TxReceipt:
"""Cancel bid and refund deposited ETH."""
contract_function: ContractFunction = self.contract.functions.cancelBid()
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def force_refund(self, transacting_power: TransactingPower, addresses: List[ChecksumAddress]) -> TxReceipt:
"""Force refund to bidders who can get tokens more than maximum allowed."""
addresses = sorted(addresses, key=str.casefold)
contract_function: ContractFunction = self.contract.functions.forceRefund(addresses)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def verify_bidding_correctness(self,
transacting_power: TransactingPower,
gas_limit: Wei, # TODO - #842: Gas Management
gas_to_save_state: Wei = Wei(30000)) -> TxReceipt:
"""Verify all bids are less than max allowed bid"""
contract_function: ContractFunction = self.contract.functions.verifyBiddingCorrectness(gas_to_save_state)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power,
transaction_gas_limit=gas_limit)
return receipt
@contract_api(TRANSACTION)
def claim(self, transacting_power: TransactingPower) -> TxReceipt:
"""
Claim tokens - will be deposited and locked as stake in the StakingEscrow contract.
"""
contract_function: ContractFunction = self.contract.functions.claim()
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power,
gas_estimation_multiplier=1.5) # FIXME
return receipt
@contract_api(TRANSACTION)
def refund(self, transacting_power: TransactingPower) -> TxReceipt:
"""Refund ETH for completed work."""
contract_function: ContractFunction = self.contract.functions.refund()
receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt
@contract_api(TRANSACTION)
def withdraw_compensation(self, transacting_power: TransactingPower) -> TxReceipt:
"""Withdraw compensation after force refund."""
contract_function: ContractFunction = self.contract.functions.withdrawCompensation()
receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt
@contract_api(CONTRACT_CALL)
def check_claim(self, checksum_address: ChecksumAddress) -> bool:
has_claimed: bool = bool(self.contract.functions.workInfo(checksum_address).call()[2])
return has_claimed
#
# Internal
#
@contract_api(CONTRACT_CALL)
def get_refunded_work(self, checksum_address: ChecksumAddress) -> Work:
work = self.contract.functions.workInfo(checksum_address).call()[1]
return Work(work)
#
# Calls
#
@contract_api(CONTRACT_CALL)
def get_available_refund(self, checksum_address: ChecksumAddress) -> Wei:
refund_eth: int = self.contract.functions.getAvailableRefund(checksum_address).call()
return Wei(refund_eth)
@contract_api(CONTRACT_CALL)
def get_available_compensation(self, checksum_address: ChecksumAddress) -> Wei:
compensation_eth: int = self.contract.functions.compensation(checksum_address).call()
return Wei(compensation_eth)
@contract_api(CONTRACT_CALL)
def get_deposited_eth(self, checksum_address: ChecksumAddress) -> Wei:
current_bid: int = self.contract.functions.workInfo(checksum_address).call()[0]
return Wei(current_bid)
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def lot_value(self) -> NuNits:
"""
Total number of tokens than can be bid for and awarded in or the number of NU
tokens deposited before the bidding windows begins via tokenDeposit().
"""
supply: int = self.contract.functions.tokenSupply().call()
return NuNits(supply)
@contract_api(CONTRACT_CALL)
def get_bonus_lot_value(self) -> NuNits:
"""
Total number of tokens than can be awarded for bonus part of bid.
"""
num_bidders: int = self.get_bidders_population()
supply: int = self.lot_value - num_bidders * self.contract.functions.minAllowableLockedTokens().call()
return NuNits(supply)
@contract_api(CONTRACT_CALL)
def get_remaining_work(self, checksum_address: str) -> Work:
"""Get remaining work periods until full refund for the target address."""
result = self.contract.functions.getRemainingWork(checksum_address).call()
return Work(result)
@contract_api(CONTRACT_CALL)
def get_bonus_eth_supply(self) -> Wei:
supply = self.contract.functions.bonusETHSupply().call()
return Wei(supply)
@contract_api(CONTRACT_CALL)
def get_eth_supply(self) -> Wei:
num_bidders: int = self.get_bidders_population()
min_bid: int = self.minimum_allowed_bid
supply: int = num_bidders * min_bid + self.get_bonus_eth_supply()
return Wei(supply)
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def boosting_refund(self) -> int:
refund = self.contract.functions.boostingRefund().call()
return refund
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def slowing_refund(self) -> int:
refund: int = self.contract.functions.SLOWING_REFUND().call()
return refund
@contract_api(CONTRACT_CALL)
def get_bonus_refund_rate(self) -> float:
f = self.contract.functions
slowing_refund: int = f.SLOWING_REFUND().call()
boosting_refund: int = f.boostingRefund().call()
refund_rate: float = self.get_bonus_deposit_rate() * slowing_refund / boosting_refund
return refund_rate
@contract_api(CONTRACT_CALL)
def get_base_refund_rate(self) -> int:
f = self.contract.functions
slowing_refund = f.SLOWING_REFUND().call()
boosting_refund = f.boostingRefund().call()
refund_rate = self.get_base_deposit_rate() * slowing_refund / boosting_refund
return refund_rate
@contract_api(CONTRACT_CALL)
def get_base_deposit_rate(self) -> int:
min_allowed_locked_tokens: NuNits = self.contract.functions.minAllowableLockedTokens().call()
deposit_rate: int = min_allowed_locked_tokens // self.minimum_allowed_bid # should never divide by 0
return deposit_rate
@contract_api(CONTRACT_CALL)
def get_bonus_deposit_rate(self) -> int:
try:
deposit_rate: int = self.get_bonus_lot_value() // self.get_bonus_eth_supply()
except ZeroDivisionError:
return 0
return deposit_rate
@contract_api(CONTRACT_CALL)
def eth_to_tokens(self, value: Wei) -> NuNits:
tokens: int = self.contract.functions.ethToTokens(value).call()
return NuNits(tokens)
@contract_api(CONTRACT_CALL)
def eth_to_work(self, value: Wei) -> Work:
tokens: int = self.contract.functions.ethToWork(value).call()
return Work(tokens)
@contract_api(CONTRACT_CALL)
def work_to_eth(self, value: Work) -> Wei:
wei: Wei = self.contract.functions.workToETH(value).call()
return Wei(wei)
@contract_api(CONTRACT_CALL)
def get_bidders_population(self) -> int:
"""Returns the number of bidders on the blockchain"""
return self.contract.functions.getBiddersLength().call()
@contract_api(CONTRACT_CALL)
def get_bidders(self) -> List[ChecksumAddress]:
"""Returns a list of bidders"""
num_bidders: int = self.get_bidders_population()
bidders: List[ChecksumAddress] = [self.contract.functions.bidders(i).call() for i in range(num_bidders)]
return bidders
@contract_api(CONTRACT_CALL)
def is_claiming_available(self) -> bool:
"""Returns True if claiming is available"""
result: bool = self.contract.functions.isClaimingAvailable().call()
return result
@contract_api(CONTRACT_CALL)
def estimate_verifying_correctness(self, gas_limit: Wei, gas_to_save_state: Wei = Wei(30000)) -> int: # TODO - #842: Gas Management
"""Returns how many bidders will be verified using specified gas limit"""
return self.contract.functions.verifyBiddingCorrectness(gas_to_save_state).call({'gas': gas_limit})
@contract_api(CONTRACT_CALL)
def next_bidder_to_check(self) -> int:
"""Returns the index of the next bidder to check as part of the bids verification process"""
return self.contract.functions.nextBidderToCheck().call()
@contract_api(CONTRACT_CALL)
def bidders_checked(self) -> bool:
"""Returns True if bidders have been checked"""
bidders_population: int = self.get_bidders_population()
return self.next_bidder_to_check() == bidders_population
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def minimum_allowed_bid(self) -> Wei:
min_bid: Wei = self.contract.functions.minAllowedBid().call()
return min_bid
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def start_bidding_date(self) -> Timestamp:
date: int = self.contract.functions.startBidDate().call()
return Timestamp(date)
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def end_bidding_date(self) -> Timestamp:
date: int = self.contract.functions.endBidDate().call()
return Timestamp(date)
@property # type: ignore
@contract_api(CONTRACT_ATTRIBUTE)
def end_cancellation_date(self) -> Timestamp:
date: int = self.contract.functions.endCancellationDate().call()
return Timestamp(date)
@contract_api(CONTRACT_CALL)
def worklock_parameters(self) -> WorklockParameters:
parameter_signatures = (
'tokenSupply',
'startBidDate',
'endBidDate',
'endCancellationDate',
'boostingRefund',
'stakingPeriods',
'minAllowedBid',
)
def _call_function_by_name(name: str) -> int:
return getattr(self.contract.functions, name)().call()
parameters = WorklockParameters(map(_call_function_by_name, parameter_signatures))
return parameters
class ContractAgency:
"""Where agents live and die."""

View File

@ -20,26 +20,18 @@
#
DISPATCHER_CONTRACT_NAME = 'Dispatcher'
STAKING_INTERFACE_ROUTER_CONTRACT_NAME = "StakingInterfaceRouter"
NUCYPHER_TOKEN_CONTRACT_NAME = 'NuCypherToken'
STAKING_ESCROW_CONTRACT_NAME = 'StakingEscrow'
STAKING_ESCROW_STUB_CONTRACT_NAME = 'StakingEscrowStub'
POLICY_MANAGER_CONTRACT_NAME = 'PolicyManager'
STAKING_INTERFACE_CONTRACT_NAME = 'StakingInterface'
ADJUDICATOR_CONTRACT_NAME = 'Adjudicator'
WORKLOCK_CONTRACT_NAME = 'WorkLock'
PRE_APPLICATION_CONTRACT_NAME = 'SimplePREApplication' # TODO: Use the real PREApplication
SUBSCRIPTION_MANAGER_CONTRACT_NAME = 'SubscriptionManager'
NUCYPHER_CONTRACT_NAMES = (
NUCYPHER_TOKEN_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME,
POLICY_MANAGER_CONTRACT_NAME,
ADJUDICATOR_CONTRACT_NAME,
DISPATCHER_CONTRACT_NAME,
STAKING_INTERFACE_CONTRACT_NAME,
STAKING_INTERFACE_ROUTER_CONTRACT_NAME,
WORKLOCK_CONTRACT_NAME,
PRE_APPLICATION_CONTRACT_NAME,
SUBSCRIPTION_MANAGER_CONTRACT_NAME
)

View File

@ -32,11 +32,9 @@ from web3.contract import Contract
from nucypher.blockchain.economics import Economics
from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
EthereumContractAgent,
NucypherTokenAgent,
StakingEscrowAgent,
WorkLockAgent,
PREApplicationAgent,
SubscriptionManagerAgent
)
@ -545,7 +543,10 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
STUB_MIN_ALLOWED_TOKENS = NU(15_000, 'NU').to_units()
STUB_MAX_ALLOWED_TOKENS = NU(30_000_000, 'NU').to_units()
def __init__(self, staking_interface: ChecksumAddress = None, *args, **kwargs):
def __init__(self,
staking_interface: ChecksumAddress = None,
worklock_address: ChecksumAddress = None,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.__dispatcher_contract = None
@ -553,20 +554,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
self.token_contract = self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=token_contract_name)
self.threshold_staking_address = staking_interface
self.worklock = self._get_contract(deployer_class=WorklockDeployer)
def _get_contract(self, deployer_class) -> VersionedContract:
contract_name = deployer_class.contract_name
try:
proxy_name = deployer_class._proxy_deployer.contract_name
except AttributeError:
proxy_name = None
try:
return self.blockchain.get_contract_by_name(registry=self.registry,
contract_name=contract_name,
proxy_name=proxy_name)
except self.registry.UnknownContract:
return None
self.worklock_address = worklock_address
def _deploy_stub(self,
transacting_power: TransactingPower,
@ -600,7 +588,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
**overrides):
constructor_kwargs = {}
constructor_kwargs.update({"_token": self.token_contract.address,
"_workLock": self.worklock.address if self.worklock is not None else NULL_ADDRESS,
"_workLock": self.worklock_address if self.worklock_address is not None else NULL_ADDRESS,
"_tStaking": self.threshold_staking_address})
constructor_kwargs.update(overrides)
constructor_kwargs = {k: v for k, v in constructor_kwargs.items() if v is not None}
@ -947,119 +935,3 @@ class PREApplicationDeployer(BaseContractDeployer):
self._contract = contract
self.deployment_receipts = dict(zip(self.deployment_steps, (receipt, )))
return self.deployment_receipts
# TODO: delete me
class WorklockDeployer(BaseContractDeployer):
agency = WorkLockAgent
contract_name = agency.contract_name
deployment_steps = ('contract_deployment', 'approve_funding', 'fund_worklock')
_upgradeable = False
def __init__(self, *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
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=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=proxy_name)
def _deploy_essential(self, transacting_power: TransactingPower, gas_limit: int = None, confirmations: int = 0):
# Deploy
constructor_args = (self.token_contract.address,
self.staking_contract.address,
*self.economics.worklock_deployment_parameters)
worklock_contract, receipt = self.blockchain.deploy_contract(transacting_power,
self.registry,
self.contract_name,
*constructor_args,
gas_limit=gas_limit,
confirmations=confirmations)
self._contract = worklock_contract
return worklock_contract, receipt
def deploy(self,
transacting_power: TransactingPower,
gas_limit: int = None,
progress=None,
confirmations: int = 0,
deployment_mode=FULL,
ignore_deployed: bool = False,
emitter=None,
) -> Dict[str, dict]:
if deployment_mode != FULL:
raise self.ContractDeploymentError(f"{self.contract_name} cannot be deployed in {deployment_mode} mode")
self.check_deployment_readiness(deployer_address=transacting_power.account,
ignore_deployed=ignore_deployed)
# Essential
if emitter:
emitter.message(f"\nNext Transaction: {self.contract_name} Contract Creation", color='blue', bold=True)
worklock_contract, deployment_receipt = self._deploy_essential(transacting_power=transacting_power,
gas_limit=gas_limit,
confirmations=confirmations)
if progress:
progress.update(1)
# Funding
approve_receipt, funding_receipt = self.fund(transacting_power=transacting_power,
progress=progress,
confirmations=confirmations,
emitter=emitter)
# Gather the transaction hashes
self.deployment_receipts = dict(zip(self.deployment_steps, (deployment_receipt,
approve_receipt,
funding_receipt)))
return self.deployment_receipts
def fund(self,
transacting_power: TransactingPower,
progress=None,
confirmations: int = 0,
emitter=None
) -> Tuple[dict, dict]:
"""
Convenience method for funding the contract and establishing the
total worklock lot value to be auctioned.
"""
supply = int(self.economics.worklock_supply)
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=self.registry)
if emitter:
emitter.message(f"\nNext Transaction: Approve Token Transfer to {self.contract_name}", color='blue', bold=True)
approve_function = token_agent.contract.functions.approve(self.contract_address, supply)
approve_receipt = self.blockchain.send_transaction(contract_function=approve_function,
transacting_power=transacting_power,
confirmations=confirmations)
if progress:
progress.update(1)
if emitter:
emitter.message(f"\nNext Transaction: Transfer Tokens to {self.contract_name}", color='blue', bold=True)
funding_function = self.contract.functions.tokenDeposit(supply)
funding_receipt = self.blockchain.send_transaction(contract_function=funding_function,
transacting_power=transacting_power,
confirmations=confirmations)
if progress:
progress.update(1)
return approve_receipt, funding_receipt

View File

@ -1,555 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0; // TODO use 0.7.x version and revert changes ?
import "zeppelin/math/SafeMath.sol";
import "zeppelin/token/ERC20/SafeERC20.sol";
import "zeppelin/utils/Address.sol";
import "zeppelin/ownership/Ownable.sol";
import "contracts/NuCypherToken.sol";
import "contracts/IStakingEscrow.sol";
import "contracts/lib/AdditionalMath.sol";
/**
* @notice The WorkLock distribution contract
*/
contract WorkLock is Ownable {
using SafeERC20 for NuCypherToken;
using SafeMath for uint256;
using AdditionalMath for uint256;
using Address for address payable;
using Address for address;
event Deposited(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, uint256 completedWork);
event Canceled(address indexed sender, uint256 value);
event BiddersChecked(address indexed sender, uint256 startIndex, uint256 endIndex);
event ForceRefund(address indexed sender, address indexed bidder, uint256 refundETH);
event CompensationWithdrawn(address indexed sender, uint256 value);
event Shutdown(address indexed sender);
struct WorkInfo {
uint256 depositedETH;
uint256 completedWork;
bool claimed;
uint128 index;
}
uint16 public constant SLOWING_REFUND = 100;
uint256 private constant MAX_ETH_SUPPLY = 2e10 ether;
NuCypherToken public immutable token;
IStakingEscrow public immutable escrow;
/*
* @dev WorkLock calculations:
* bid = minBid + bonusETHPart
* bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens
* bonusDepositRate = bonusTokenSupply / bonusETHSupply
* claimedTokens = minAllowableLockedTokens + bonusETHPart * bonusDepositRate
* bonusRefundRate = bonusDepositRate * SLOWING_REFUND / boostingRefund
* refundETH = completedWork / refundRate
*/
uint256 public immutable boostingRefund;
uint256 public immutable minAllowedBid;
uint16 public immutable stakingPeriods;
// copy from the escrow contract
uint256 public immutable maxAllowableLockedTokens;
uint256 public immutable minAllowableLockedTokens;
uint256 public tokenSupply;
uint256 public startBidDate;
uint256 public endBidDate;
uint256 public endCancellationDate;
uint256 public bonusETHSupply;
mapping(address => WorkInfo) public workInfo;
mapping(address => uint256) public compensation;
address[] public bidders;
// if value == bidders.length then WorkLock is fully checked
uint256 public nextBidderToCheck;
/**
* @dev Checks timestamp regarding cancellation window
*/
modifier afterCancellationWindow()
{
require(block.timestamp >= endCancellationDate,
"Operation is allowed when cancellation phase is over");
_;
}
/**
* @param _token Token contract
* @param _escrow Escrow contract
* @param _startBidDate Timestamp when bidding starts
* @param _endBidDate Timestamp when bidding will end
* @param _endCancellationDate Timestamp when cancellation will ends
* @param _boostingRefund Coefficient to boost refund ETH
* @param _stakingPeriods Amount of periods during which tokens will be locked after claiming
* @param _minAllowedBid Minimum allowed ETH amount for bidding
*/
constructor(
NuCypherToken _token,
IStakingEscrow _escrow,
uint256 _startBidDate,
uint256 _endBidDate,
uint256 _endCancellationDate,
uint256 _boostingRefund,
uint16 _stakingPeriods,
uint256 _minAllowedBid
) {
uint256 totalSupply = _token.totalSupply();
require(totalSupply > 0 && // token contract is deployed and accessible
_escrow.secondsPerPeriod() > 0 && // escrow contract is deployed and accessible
_escrow.token() == _token && // same token address for worklock and escrow
_endBidDate > _startBidDate && // bidding period lasts some time
_endBidDate > block.timestamp && // there is time to make a bid
_endCancellationDate >= _endBidDate && // cancellation window includes bidding
_minAllowedBid > 0 && // min allowed bid was set
_boostingRefund > 0 && // boosting coefficient was set
_stakingPeriods >= _escrow.minLockedPeriods()); // staking duration is consistent with escrow contract
// worst case for `ethToWork()` and `workToETH()`,
// when ethSupply == MAX_ETH_SUPPLY and tokenSupply == totalSupply
require(MAX_ETH_SUPPLY * totalSupply * SLOWING_REFUND / MAX_ETH_SUPPLY / totalSupply == SLOWING_REFUND &&
MAX_ETH_SUPPLY * totalSupply * _boostingRefund / MAX_ETH_SUPPLY / totalSupply == _boostingRefund);
token = _token;
escrow = _escrow;
startBidDate = _startBidDate;
endBidDate = _endBidDate;
endCancellationDate = _endCancellationDate;
boostingRefund = _boostingRefund;
stakingPeriods = _stakingPeriods;
minAllowedBid = _minAllowedBid;
maxAllowableLockedTokens = _escrow.maxAllowableLockedTokens();
minAllowableLockedTokens = _escrow.minAllowableLockedTokens();
}
/**
* @notice Deposit tokens to contract
* @param _value Amount of tokens to transfer
*/
function tokenDeposit(uint256 _value) external {
require(block.timestamp < endBidDate, "Can't deposit more tokens after end of bidding");
token.safeTransferFrom(msg.sender, address(this), _value);
tokenSupply += _value;
emit Deposited(msg.sender, _value);
}
/**
* @notice Calculate amount of tokens that will be get for specified amount of ETH
* @dev This value will be fixed only after end of bidding
*/
function ethToTokens(uint256 _ethAmount) public view returns (uint256) {
if (_ethAmount < minAllowedBid) {
return 0;
}
// when all participants bid with the same minimum amount of eth
if (bonusETHSupply == 0) {
return tokenSupply / bidders.length;
}
uint256 bonusETH = _ethAmount - minAllowedBid;
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
return minAllowableLockedTokens + bonusETH.mul(bonusTokenSupply).div(bonusETHSupply);
}
/**
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
*/
function ethToWork(uint256 _ethAmount, uint256 _tokenSupply, uint256 _ethSupply)
internal view returns (uint256)
{
return _ethAmount.mul(_tokenSupply).mul(SLOWING_REFUND).divCeil(_ethSupply.mul(boostingRefund));
}
/**
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
* @dev This value will be fixed only after end of bidding
* @param _ethToReclaim Specified sum of ETH staker wishes to reclaim following completion of work
* @param _restOfDepositedETH Remaining ETH in staker's deposit once ethToReclaim sum has been subtracted
* @dev _ethToReclaim + _restOfDepositedETH = depositedETH
*/
function ethToWork(uint256 _ethToReclaim, uint256 _restOfDepositedETH) internal view returns (uint256) {
uint256 baseETHSupply = bidders.length * minAllowedBid;
// when all participants bid with the same minimum amount of eth
if (bonusETHSupply == 0) {
return ethToWork(_ethToReclaim, tokenSupply, baseETHSupply);
}
uint256 baseETH = 0;
uint256 bonusETH = 0;
// If the staker's total remaining deposit (including the specified sum of ETH to reclaim)
// is lower than the minimum bid size,
// then only the base part is used to calculate the work required to reclaim ETH
if (_ethToReclaim + _restOfDepositedETH <= minAllowedBid) {
baseETH = _ethToReclaim;
// If the staker's remaining deposit (not including the specified sum of ETH to reclaim)
// is still greater than the minimum bid size,
// then only the bonus part is used to calculate the work required to reclaim ETH
} else if (_restOfDepositedETH >= minAllowedBid) {
bonusETH = _ethToReclaim;
// If the staker's remaining deposit (not including the specified sum of ETH to reclaim)
// is lower than the minimum bid size,
// then both the base and bonus parts must be used to calculate the work required to reclaim ETH
} else {
bonusETH = _ethToReclaim + _restOfDepositedETH - minAllowedBid;
baseETH = _ethToReclaim - bonusETH;
}
uint256 baseTokenSupply = bidders.length * minAllowableLockedTokens;
uint256 work = 0;
if (baseETH > 0) {
work = ethToWork(baseETH, baseTokenSupply, baseETHSupply);
}
if (bonusETH > 0) {
uint256 bonusTokenSupply = tokenSupply - baseTokenSupply;
work += ethToWork(bonusETH, bonusTokenSupply, bonusETHSupply);
}
return work;
}
/**
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
* @dev This value will be fixed only after end of bidding
*/
function ethToWork(uint256 _ethAmount) public view returns (uint256) {
return ethToWork(_ethAmount, 0);
}
/**
* @notice Calculate amount of ETH that will be refund for completing specified amount of work
*/
function workToETH(uint256 _completedWork, uint256 _ethSupply, uint256 _tokenSupply)
internal view returns (uint256)
{
return _completedWork.mul(_ethSupply).mul(boostingRefund).div(_tokenSupply.mul(SLOWING_REFUND));
}
/**
* @notice Calculate amount of ETH that will be refund for completing specified amount of work
* @dev This value will be fixed only after end of bidding
*/
function workToETH(uint256 _completedWork, uint256 _depositedETH) public view returns (uint256) {
uint256 baseETHSupply = bidders.length * minAllowedBid;
// when all participants bid with the same minimum amount of eth
if (bonusETHSupply == 0) {
return workToETH(_completedWork, baseETHSupply, tokenSupply);
}
uint256 bonusWork = 0;
uint256 bonusETH = 0;
uint256 baseTokenSupply = bidders.length * minAllowableLockedTokens;
if (_depositedETH > minAllowedBid) {
bonusETH = _depositedETH - minAllowedBid;
uint256 bonusTokenSupply = tokenSupply - baseTokenSupply;
bonusWork = ethToWork(bonusETH, bonusTokenSupply, bonusETHSupply);
if (_completedWork <= bonusWork) {
return workToETH(_completedWork, bonusETHSupply, bonusTokenSupply);
}
}
_completedWork -= bonusWork;
return bonusETH + workToETH(_completedWork, baseETHSupply, baseTokenSupply);
}
/**
* @notice Get remaining work to full refund
*/
function getRemainingWork(address _bidder) external view returns (uint256) {
WorkInfo storage info = workInfo[_bidder];
uint256 completedWork = escrow.getCompletedWork(_bidder).sub(info.completedWork);
uint256 remainingWork = ethToWork(info.depositedETH);
if (remainingWork <= completedWork) {
return 0;
}
return remainingWork - completedWork;
}
/**
* @notice Get length of bidders array
*/
function getBiddersLength() external view returns (uint256) {
return bidders.length;
}
/**
* @notice Bid for tokens by transferring ETH
*/
function bid() external payable {
require(block.timestamp >= startBidDate, "Bidding is not open yet");
require(block.timestamp < endBidDate, "Bidding is already finished");
WorkInfo storage info = workInfo[msg.sender];
// first bid
if (info.depositedETH == 0) {
require(msg.value >= minAllowedBid, "Bid must be at least minimum");
require(bidders.length < tokenSupply / minAllowableLockedTokens, "Not enough tokens for more bidders");
info.index = uint128(bidders.length);
bidders.push(msg.sender);
bonusETHSupply = bonusETHSupply.add(msg.value - minAllowedBid);
} else {
bonusETHSupply = bonusETHSupply.add(msg.value);
}
info.depositedETH = info.depositedETH.add(msg.value);
emit Bid(msg.sender, msg.value);
}
/**
* @notice Cancel bid and refund deposited ETH
*/
function cancelBid() external {
require(block.timestamp < endCancellationDate,
"Cancellation allowed only during cancellation window");
WorkInfo storage info = workInfo[msg.sender];
require(info.depositedETH > 0, "No bid to cancel");
require(!info.claimed, "Tokens are already claimed");
uint256 refundETH = info.depositedETH;
info.depositedETH = 0;
// remove from bidders array, move last bidder to the empty place
uint256 lastIndex = bidders.length - 1;
if (info.index != lastIndex) {
address lastBidder = bidders[lastIndex];
bidders[info.index] = lastBidder;
workInfo[lastBidder].index = info.index;
}
bidders.pop();
if (refundETH > minAllowedBid) {
bonusETHSupply = bonusETHSupply.sub(refundETH - minAllowedBid);
}
payable(msg.sender).sendValue(refundETH);
emit Canceled(msg.sender, refundETH);
}
/**
* @notice Cancels distribution, makes possible to retrieve all bids and owner gets all tokens
*/
function shutdown() external onlyOwner {
require(!isClaimingAvailable(), "Claiming has already been enabled");
internalShutdown();
}
/**
* @notice Cancels distribution, makes possible to retrieve all bids and owner gets all tokens
*/
function internalShutdown() internal {
startBidDate = 0;
endBidDate = 0;
endCancellationDate = type(uint256).max; // "infinite" cancellation window
token.safeTransfer(owner(), tokenSupply);
emit Shutdown(msg.sender);
}
/**
* @notice Make force refund to bidders who can get tokens more than maximum allowed
* @param _biddersForRefund Sorted list of unique bidders. Only bidders who must receive a refund
*/
function forceRefund(address payable[] calldata _biddersForRefund) external afterCancellationWindow {
require(nextBidderToCheck != bidders.length, "Bidders have already been checked");
uint256 length = _biddersForRefund.length;
require(length > 0, "Must be at least one bidder for a refund");
uint256 minNumberOfBidders = tokenSupply.divCeil(maxAllowableLockedTokens);
if (bidders.length < minNumberOfBidders) {
internalShutdown();
return;
}
address previousBidder = _biddersForRefund[0];
uint256 minBid = workInfo[previousBidder].depositedETH;
uint256 maxBid = minBid;
// get minimum and maximum bids
for (uint256 i = 1; i < length; i++) {
address bidder = _biddersForRefund[i];
uint256 depositedETH = workInfo[bidder].depositedETH;
require(bidder > previousBidder && depositedETH > 0, "Addresses must be an array of unique bidders");
if (minBid > depositedETH) {
minBid = depositedETH;
} else if (maxBid < depositedETH) {
maxBid = depositedETH;
}
previousBidder = bidder;
}
uint256[] memory refunds = new uint256[](length);
// first step - align at a minimum bid
if (minBid != maxBid) {
for (uint256 i = 0; i < length; i++) {
address bidder = _biddersForRefund[i];
WorkInfo storage info = workInfo[bidder];
if (info.depositedETH > minBid) {
refunds[i] = info.depositedETH - minBid;
info.depositedETH = minBid;
bonusETHSupply -= refunds[i];
}
}
}
require(ethToTokens(minBid) > maxAllowableLockedTokens,
"At least one of bidders has allowable bid");
// final bids adjustment (only for bonus part)
// (min_whale_bid * token_supply - max_stake * eth_supply) / (token_supply - max_stake * n_whales)
uint256 maxBonusTokens = maxAllowableLockedTokens - minAllowableLockedTokens;
uint256 minBonusETH = minBid - minAllowedBid;
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
uint256 refundETH = minBonusETH.mul(bonusTokenSupply)
.sub(maxBonusTokens.mul(bonusETHSupply))
.divCeil(bonusTokenSupply - maxBonusTokens.mul(length));
uint256 resultBid = minBid.sub(refundETH);
bonusETHSupply -= length * refundETH;
for (uint256 i = 0; i < length; i++) {
address bidder = _biddersForRefund[i];
WorkInfo storage info = workInfo[bidder];
refunds[i] += refundETH;
info.depositedETH = resultBid;
}
// reset verification
nextBidderToCheck = 0;
// save a refund
for (uint256 i = 0; i < length; i++) {
address bidder = _biddersForRefund[i];
compensation[bidder] += refunds[i];
emit ForceRefund(msg.sender, bidder, refunds[i]);
}
}
/**
* @notice Withdraw compensation after force refund
*/
function withdrawCompensation() external {
uint256 refund = compensation[msg.sender];
require(refund > 0, "There is no compensation");
compensation[msg.sender] = 0;
payable(msg.sender).sendValue(refund);
emit CompensationWithdrawn(msg.sender, refund);
}
/**
* @notice Check that the claimed tokens are within `maxAllowableLockedTokens` for all participants,
* starting from the last point `nextBidderToCheck`
* @dev Method stops working when the remaining gas is less than `_gasToSaveState`
* and saves the state in `nextBidderToCheck`.
* If all bidders have been checked then `nextBidderToCheck` will be equal to the length of the bidders array
*/
function verifyBiddingCorrectness(uint256 _gasToSaveState) external afterCancellationWindow returns (uint256) {
require(nextBidderToCheck != bidders.length, "Bidders have already been checked");
// all participants bid with the same minimum amount of eth
uint256 index = nextBidderToCheck;
if (bonusETHSupply == 0) {
require(tokenSupply / bidders.length <= maxAllowableLockedTokens, "Not enough bidders");
index = bidders.length;
}
uint256 maxBonusTokens = maxAllowableLockedTokens - minAllowableLockedTokens;
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
uint256 maxBidFromMaxStake = minAllowedBid + maxBonusTokens.mul(bonusETHSupply).div(bonusTokenSupply);
while (index < bidders.length && gasleft() > _gasToSaveState) {
address bidder = bidders[index];
require(workInfo[bidder].depositedETH <= maxBidFromMaxStake, "Bid is greater than max allowable bid");
index++;
}
if (index != nextBidderToCheck) {
emit BiddersChecked(msg.sender, nextBidderToCheck, index);
nextBidderToCheck = index;
}
return nextBidderToCheck;
}
/**
* @notice Checks if claiming available
*/
function isClaimingAvailable() public view returns (bool) {
return block.timestamp >= endCancellationDate &&
nextBidderToCheck == bidders.length;
}
/**
* @notice Claimed tokens will be deposited and locked as stake in the StakingEscrow contract.
*/
function claim() external returns (uint256 claimedTokens) {
require(isClaimingAvailable(), "Claiming has not been enabled yet");
WorkInfo storage info = workInfo[msg.sender];
require(!info.claimed, "Tokens are already claimed");
claimedTokens = ethToTokens(info.depositedETH);
require(claimedTokens > 0, "Nothing to claim");
info.claimed = true;
token.approve(address(escrow), claimedTokens);
escrow.depositFromWorkLock(msg.sender, claimedTokens, stakingPeriods);
info.completedWork = escrow.setWorkMeasurement(msg.sender, true);
emit Claimed(msg.sender, claimedTokens);
}
/**
* @notice Get available refund for bidder
*/
function getAvailableRefund(address _bidder) public view returns (uint256) {
WorkInfo storage info = workInfo[_bidder];
// nothing to refund
if (info.depositedETH == 0) {
return 0;
}
uint256 currentWork = escrow.getCompletedWork(_bidder);
uint256 completedWork = currentWork.sub(info.completedWork);
// no work that has been completed since last refund
if (completedWork == 0) {
return 0;
}
uint256 refundETH = workToETH(completedWork, info.depositedETH);
if (refundETH > info.depositedETH) {
refundETH = info.depositedETH;
}
return refundETH;
}
/**
* @notice Refund ETH for the completed work
*/
function refund() external returns (uint256 refundETH) {
WorkInfo storage info = workInfo[msg.sender];
require(info.claimed, "Tokens must be claimed before refund");
refundETH = getAvailableRefund(msg.sender);
require(refundETH > 0, "Nothing to refund: there is no ETH to refund or no completed work");
if (refundETH == info.depositedETH) {
escrow.setWorkMeasurement(msg.sender, false);
}
info.depositedETH = info.depositedETH.sub(refundETH);
// convert refund back to work to eliminate potential rounding errors
uint256 completedWork = ethToWork(refundETH, info.depositedETH);
info.completedWork = info.completedWork.add(completedWork);
emit Refund(msg.sender, refundETH, completedWork);
payable(msg.sender).sendValue(refundETH);
}
}

View File

@ -106,15 +106,3 @@ STAKEHOLDER_BANNER = r"""
The Holder of Stakes.
"""
WORKLOCK_BANNER = r"""
_ _ _ _ _
| | | | | | | | | |
| | | | ___ _ __ | | __| | ___ ___ | | __
| |/\| | / _ \ | '__|| |/ /| | / _ \ / __|| |/ /
\ /\ /| (_) || | | < | |____| (_) || (__ | <
\/ \/ \___/ |_| |_|\_\\_____/ \___/ \___||_|\_\
{}
"""

View File

@ -23,7 +23,6 @@ import click
from nucypher.blockchain.eth.actors import Staker
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
from nucypher.blockchain.eth.constants import (
POLICY_MANAGER_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME
)
from nucypher.blockchain.eth.networks import NetworksInventory
@ -179,7 +178,7 @@ def events(general_config, registry_options, contract_name, from_block, to_block
if event_name:
raise click.BadOptionUsage(option_name='--event-name', message='--event-name requires --contract-name')
# FIXME should we force a contract name to be specified?
contract_names = [STAKING_ESCROW_CONTRACT_NAME, POLICY_MANAGER_CONTRACT_NAME]
contract_names = [STAKING_ESCROW_CONTRACT_NAME,]
else:
contract_names = [contract_name]

View File

@ -1,414 +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/>.
"""
from pathlib import Path
import click
import maya
import os
import tabulate
from decimal import Decimal
from eth_typing.evm import ChecksumAddress
from web3 import Web3
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.actors import Bidder
from nucypher.blockchain.eth.agents import ContractAgency, WorkLockAgent
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.signers import Signer, ClefSigner
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import prettify_eth_amount
from nucypher.cli.actions.auth import get_client_password
from nucypher.cli.actions.select import select_client_account
from nucypher.cli.utils import connect_to_blockchain, get_registry, setup_emitter
from nucypher.cli.config import group_general_config, GroupGeneralConfig
from nucypher.cli.literature import (
AVAILABLE_CLAIM_NOTICE,
BID_AMOUNT_PROMPT_WITH_MIN_BID,
BID_INCREASE_AMOUNT_PROMPT,
BIDDERS_ALREADY_VERIFIED,
BIDDING_WINDOW_CLOSED,
BIDS_VALID_NO_FORCE_REFUND_INDICATED,
CANCELLATION_WINDOW_CLOSED,
CLAIM_ALREADY_PLACED,
CLAIMING_NOT_AVAILABLE,
COMPLETED_BID_VERIFICATION,
CONFIRM_BID_VERIFICATION,
CONFIRM_COLLECT_WORKLOCK_REFUND,
CONFIRM_REQUEST_WORKLOCK_COMPENSATION,
CONFIRM_WORKLOCK_CLAIM,
EXISTING_BID_AMOUNT_NOTICE,
PROMPT_BID_VERIFY_GAS_LIMIT,
REQUESTING_WORKLOCK_COMPENSATION,
SUBMITTING_WORKLOCK_CLAIM,
SUBMITTING_WORKLOCK_REFUND_REQUEST,
SUCCESSFUL_BID_CANCELLATION,
VERIFICATION_ESTIMATES,
WHALE_WARNING,
WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE,
WORKLOCK_CLAIM_ADVISORY
)
from nucypher.cli.options import (
group_options,
option_force,
option_hw_wallet,
option_network,
option_provider_uri,
option_registry_filepath,
option_signer_uri,
option_participant_address)
from nucypher.cli.painting.transactions import paint_receipt_summary
from nucypher.cli.painting.worklock import (
paint_bidder_status,
paint_bidding_notice,
paint_worklock_claim,
paint_worklock_status
)
from nucypher.cli.types import DecimalRange, EIP55_CHECKSUM_ADDRESS
from nucypher.config.constants import NUCYPHER_ENVVAR_PROVIDER_URI
class WorkLockOptions:
__option_name__ = 'worklock_options'
def __init__(self,
participant_address: ChecksumAddress,
signer_uri: str,
provider_uri: str,
registry_filepath: Path,
network: str):
self.bidder_address = participant_address
self.signer_uri = signer_uri
self.provider_uri = provider_uri
self.registry_filepath = registry_filepath
self.network = network
def setup(self, general_config) -> tuple:
emitter = setup_emitter(general_config) # TODO: Restore Banner: network=self.network.capitalize()
registry = get_registry(network=self.network, registry_filepath=self.registry_filepath)
blockchain = connect_to_blockchain(emitter=emitter, provider_uri=self.provider_uri)
return emitter, registry, blockchain
def get_bidder_address(self, emitter, registry):
if not self.bidder_address:
self.bidder_address = select_client_account(emitter=emitter,
provider_uri=self.provider_uri,
signer_uri=self.signer_uri,
network=self.network,
registry=registry,
show_eth_balance=True)
return self.bidder_address
def __create_bidder(self,
registry,
domain: str,
transacting: bool = True,
hw_wallet: bool = False,
) -> Bidder:
is_clef = ClefSigner.is_valid_clef_uri(self.signer_uri)
testnet = self.network != NetworksInventory.MAINNET
signer = Signer.from_signer_uri(self.signer_uri, testnet=testnet) if self.signer_uri else None
password_required = (not is_clef and not hw_wallet)
if signer and transacting and password_required:
client_password = get_client_password(checksum_address=self.bidder_address)
signer.unlock_account(account=self.bidder_address, password=client_password)
transacting_power = None
if transacting:
transacting_power = TransactingPower(account=self.bidder_address, signer=signer)
transacting_power.unlock(password=client_password)
bidder = Bidder(registry=registry,
transacting_power=transacting_power,
checksum_address=self.bidder_address if not transacting_power else None,
domain=domain)
return bidder
def create_bidder(self, registry, hw_wallet: bool = False):
return self.__create_bidder(registry=registry,
domain=self.network,
hw_wallet=hw_wallet,
transacting=True)
def create_transactionless_bidder(self, registry):
return self.__create_bidder(registry, transacting=False, domain=self.network)
group_worklock_options = group_options(
WorkLockOptions,
participant_address=option_participant_address,
signer_uri=option_signer_uri,
provider_uri=option_provider_uri(required=True, default=os.environ.get(NUCYPHER_ENVVAR_PROVIDER_URI)),
network=option_network(default=NetworksInventory.DEFAULT, validate=True), # TODO: See 2214
registry_filepath=option_registry_filepath,
)
@click.group()
def worklock():
"""Participate in NuCypher's WorkLock to obtain a NU stake"""
@worklock.command()
@group_worklock_options
@group_general_config
def status(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions):
"""Show current WorkLock information"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
paint_worklock_status(emitter=emitter, registry=registry)
if worklock_options.bidder_address:
bidder = worklock_options.create_transactionless_bidder(registry=registry)
paint_bidder_status(emitter=emitter, bidder=bidder)
@worklock.command()
@group_general_config
@group_worklock_options
@option_force
@option_hw_wallet
@click.option('--value', help="ETH value to escrow", type=DecimalRange(min=0))
def escrow(general_config: GroupGeneralConfig,
worklock_options: WorkLockOptions,
force: bool,
hw_wallet: bool,
value: Decimal):
"""Create an ETH escrow, or increase an existing escrow"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
now = maya.now().epoch
if not worklock_agent.start_bidding_date <= now <= worklock_agent.end_bidding_date:
emitter.echo(BIDDING_WINDOW_CLOSED, color='red')
raise click.Abort()
_bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
existing_bid_amount = bidder.get_deposited_eth
if not value:
if force:
raise click.MissingParameter("Missing --value.")
if not existing_bid_amount: # It's the first bid
minimum_bid = bidder.worklock_agent.minimum_allowed_bid
minimum_bid_in_eth = Web3.fromWei(minimum_bid, 'ether')
prompt = BID_AMOUNT_PROMPT_WITH_MIN_BID.format(minimum_bid_in_eth=minimum_bid_in_eth)
else: # There's an existing bid and the bidder is increasing the amount
emitter.message(EXISTING_BID_AMOUNT_NOTICE.format(eth_amount=Web3.fromWei(existing_bid_amount, 'ether')))
minimum_bid_in_eth = Web3.fromWei(1, 'ether')
prompt = BID_INCREASE_AMOUNT_PROMPT
value = click.prompt(prompt, type=DecimalRange(min=minimum_bid_in_eth))
value = int(Web3.toWei(Decimal(value), 'ether'))
if not force:
if not existing_bid_amount:
paint_bidding_notice(emitter=emitter, bidder=bidder)
click.confirm(f"Place WorkLock escrow of {prettify_eth_amount(value)}?", abort=True)
else:
click.confirm(f"Increase current escrow ({prettify_eth_amount(existing_bid_amount)}) "
f"by {prettify_eth_amount(value)}?", abort=True)
receipt = bidder.place_bid(value=value)
emitter.message("Publishing WorkLock Escrow...")
maximum = NU.from_units(bidder.economics.maximum_allowed_locked)
available_claim = NU.from_units(bidder.available_claim)
message = f'\nCurrent escrow: {prettify_eth_amount(bidder.get_deposited_eth)} | Allocation: {available_claim}\n'
if available_claim > maximum:
message += f"\nThis allocation is currently above the allowed max ({maximum}), " \
f"so the escrow may be partially refunded.\n"
message += f'Note that the available allocation value may fluctuate until the escrow period closes and ' \
f'allocations are finalized.\n'
emitter.echo(message, color='yellow')
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
@worklock.command()
@group_general_config
@group_worklock_options
@option_force
@option_hw_wallet
def cancel_escrow(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
"""Cancel your escrow and receive your ETH back"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
now = maya.now().epoch
if not worklock_agent.start_bidding_date <= now <= worklock_agent.end_cancellation_date:
emitter.echo(CANCELLATION_WINDOW_CLOSED, color='red')
raise click.Abort()
bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
if not force:
value = bidder.get_deposited_eth
click.confirm(f"Confirm escrow cancellation of {prettify_eth_amount(value)} for {bidder_address}?", abort=True)
receipt = bidder.cancel_bid()
emitter.echo(SUCCESSFUL_BID_CANCELLATION, color='green')
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
return # Exit
@worklock.command()
@option_force
@option_hw_wallet
@group_worklock_options
@group_general_config
def claim(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
"""Claim tokens for your escrow, and start staking them"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
if not worklock_agent.is_claiming_available():
emitter.echo(CLAIMING_NOT_AVAILABLE, color='red')
raise click.Abort()
bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
unspent_bid = bidder.available_compensation
if unspent_bid:
emitter.echo(WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE.format(amount=prettify_eth_amount(unspent_bid)))
if not force:
message = CONFIRM_REQUEST_WORKLOCK_COMPENSATION.format(bidder_address=bidder_address)
click.confirm(message, abort=True)
emitter.echo(REQUESTING_WORKLOCK_COMPENSATION)
receipt = bidder.withdraw_compensation()
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
has_claimed = bidder.has_claimed
if bool(has_claimed):
emitter.echo(CLAIM_ALREADY_PLACED.format(bidder_address=bidder.checksum_address), color='red')
raise click.Abort()
tokens = NU.from_units(bidder.available_claim)
emitter.echo(AVAILABLE_CLAIM_NOTICE.format(tokens=tokens), color='green', bold=True)
if not force:
lock_duration = bidder.worklock_agent.worklock_parameters()[-2]
emitter.echo(WORKLOCK_CLAIM_ADVISORY.format(lock_duration=lock_duration), color='blue')
click.confirm(CONFIRM_WORKLOCK_CLAIM.format(bidder_address=bidder_address), abort=True)
emitter.echo(SUBMITTING_WORKLOCK_CLAIM)
receipt = bidder.claim()
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
paint_worklock_claim(emitter=emitter,
bidder_address=bidder_address,
network=worklock_options.network,
provider_uri=worklock_options.provider_uri)
@worklock.command()
@group_worklock_options
@group_general_config
def remaining_work(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions):
"""Check how much work is pending until you can get all your escrowed ETH back"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_transactionless_bidder(registry=registry)
emitter.echo(f"Work Remaining for {bidder_address}: {bidder.remaining_work}")
@worklock.command()
@option_force
@option_hw_wallet
@group_worklock_options
@group_general_config
def refund(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
"""Reclaim ETH unlocked by your work"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
if not force:
click.confirm(CONFIRM_COLLECT_WORKLOCK_REFUND.format(bidder_address=bidder_address), abort=True)
emitter.echo(SUBMITTING_WORKLOCK_REFUND_REQUEST)
receipt = bidder.refund_deposit()
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
@worklock.command()
@group_general_config
@group_worklock_options
@option_force
@option_hw_wallet
@click.option('--gas-limit', help="Gas limit per each verification transaction", type=click.IntRange(min=60000))
# TODO: Consider moving to administrator (nucypher-deploy) #1758
def enable_claiming(general_config: GroupGeneralConfig,
worklock_options: WorkLockOptions,
force: bool,
hw_wallet: bool,
gas_limit: int):
"""Ensure correctness of WorkLock participants and enable allocation"""
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
bidder_address = worklock_options.get_bidder_address(emitter, registry)
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
whales = bidder.get_whales()
if whales:
headers = ("Participants that require correction", "Current bonus")
columns = (whales.keys(), map(prettify_eth_amount, whales.values()))
emitter.echo(tabulate.tabulate(dict(zip(headers, columns)), headers=headers, floatfmt="fancy_grid"))
if not force:
click.confirm(f"Confirm force refund to at least {len(whales)} participants using {bidder_address}?", abort=True)
force_refund_receipt = bidder.force_refund()
emitter.echo(WHALE_WARNING.format(number=len(whales)), color='green')
paint_receipt_summary(receipt=force_refund_receipt,
emitter=emitter,
chain_name=bidder.staking_agent.blockchain.client.chain_name,
transaction_type=f"force-refund")
else:
emitter.echo(BIDS_VALID_NO_FORCE_REFUND_INDICATED, color='yellow')
if not bidder.worklock_agent.bidders_checked():
confirmation = gas_limit and force
while not confirmation:
if not gas_limit:
min_gas = 180000
gas_limit = click.prompt(PROMPT_BID_VERIFY_GAS_LIMIT.format(min_gas=min_gas), type=click.IntRange(min=min_gas))
bidders_per_transaction = bidder.worklock_agent.estimate_verifying_correctness(gas_limit=gas_limit)
if not force:
message = CONFIRM_BID_VERIFICATION.format(bidder_address=bidder_address,
gas_limit=gas_limit,
bidders_per_transaction=bidders_per_transaction)
confirmation = click.confirm(message)
gas_limit = gas_limit if confirmation else None
else:
emitter.echo(VERIFICATION_ESTIMATES.format(gas_limit=gas_limit,
bidders_per_transaction=bidders_per_transaction))
confirmation = True
verification_receipts = bidder.verify_bidding_correctness(gas_limit=gas_limit)
emitter.echo(COMPLETED_BID_VERIFICATION, color='green')
for iteration, receipt in verification_receipts.items():
paint_receipt_summary(receipt=receipt,
emitter=emitter,
chain_name=bidder.staking_agent.blockchain.client.chain_name,
transaction_type=f"verify-correctness[{iteration}]")
else:
emitter.echo(BIDDERS_ALREADY_VERIFIED, color='yellow')

View File

@ -569,116 +569,6 @@ Compiled with solc version {solc_version}
'''
#
# Worklock
#
BID_AMOUNT_PROMPT_WITH_MIN_BID = "Enter escrow amount in ETH (at least {minimum_bid_in_eth} ETH)"
BID_INCREASE_AMOUNT_PROMPT = "Enter the amount in ETH that you want to increase your escrow"
EXISTING_BID_AMOUNT_NOTICE = "⚠️ You have an existing escrow of {eth_amount} ETH"
WORKLOCK_AGREEMENT = """
WorkLock Participant Notice
---------------------------------
- By participating in NuCypher's WorkLock you are committing to operating a staking
NuCypher node once the allocation period opens.
- WorkLock allocation is provided in the form of a stake and will be locked for
the stake duration ({duration} periods). During this time,
you are obligated to maintain a networked and available
NuCypher node bonded to the staker address {bidder_address}.
- ETH in escrow will be available for refund at a rate of {refund_rate}
per confirmed period. This rate may vary until {end_date}.
- You agree to allow NuCypher network users to carry out uninterrupted work orders
at will without interference. Failure to keep your node online or fulfill re-encryption
work orders will result in loss of staked NU as described in the NuCypher slashing protocol:
https://docs.nucypher.com/en/latest/architecture/slashing.html
- Correctly servicing work orders will result in rewards paid out in ethers retro-actively
and on-demand.
Accept WorkLock terms and node operator obligation?""" # TODO: Show a special message for first bidder, since there's no refund rate yet?
BIDDING_WINDOW_CLOSED = "❌ You can't escrow, the escrow period is closed."
CANCELLATION_WINDOW_CLOSED = "❌ You can't cancel your escrow, the cancellation period is closed."
SUCCESSFUL_BID_CANCELLATION = "✅ Escrow canceled\n"
WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE = """
Note that WorkLock did not use your entire escrow due to a maximum allocation limit.
Therefore, an unspent amount of {amount} is available for refund.
"""
CONFIRM_REQUEST_WORKLOCK_COMPENSATION = """
Before requesting the NU allocation for {bidder_address},
you will need to be refunded your unspent escrow amount.
Proceed with request?
"""
REQUESTING_WORKLOCK_COMPENSATION = "Requesting refund of unspent escrow amount..."
CLAIMING_NOT_AVAILABLE = "❌ You can't request a NU allocation yet, allocations are not currently available."
CLAIM_ALREADY_PLACED = "⚠️ An allocation was already assigned to {bidder_address}"
AVAILABLE_CLAIM_NOTICE = "\nYou have an available allocation of {tokens} 🎉 \n"
WORKLOCK_CLAIM_ADVISORY = """
Note: Allocating WorkLock NU will initialize a new stake to be locked for {lock_duration} periods.
"""
CONFIRM_WORKLOCK_CLAIM = "Continue WorkLock allocation for participant {bidder_address}?"
SUBMITTING_WORKLOCK_CLAIM = "Submitting allocation request..."
CONFIRM_COLLECT_WORKLOCK_REFUND = "Collect ETH refund for participant {bidder_address}?"
SUBMITTING_WORKLOCK_REFUND_REQUEST = "Submitting WorkLock refund request..."
PROMPT_BID_VERIFY_GAS_LIMIT = "Enter gas limit per each verification transaction (at least {min_gas})"
COMPLETED_BID_VERIFICATION = "Escrow amounts have been checked\n"
BIDS_VALID_NO_FORCE_REFUND_INDICATED = "All escrows are correct, force refund is not needed\n"
CONFIRM_BID_VERIFICATION = """
Confirm verification of escrow from {bidder_address} using {gas_limit} gas
for {bidders_per_transaction} participants per each transaction?
"""
VERIFICATION_ESTIMATES = "Using {gas_limit} gas for {bidders_per_transaction} participants per each transaction\n"
WHALE_WARNING = "At least {number} participants got a force refund\n"
BIDDERS_ALREADY_VERIFIED = "All escrow amounts have already been checked\n"
SUCCESSFUL_WORKLOCK_CLAIM = """
Successfully allocated WorkLock NU for {bidder_address}.
You can check that the stake was created correctly by running:
nucypher status stakers --staking-address {bidder_address} --network {network} --provider {provider_uri}
Next Steps for WorkLock Participants
====================================
Congratulations! 🎉 You're officially a Staker in the NuCypher network.
See the official NuCypher documentation for a comprehensive guide on next steps!
As a first step, you need to bond a worker to your stake by running:
nucypher stake bond-worker --worker-address <WORKER ADDRESS>
"""
#
# Ursula

View File

@ -24,7 +24,6 @@ from nucypher.cli.commands import (
stake,
status,
ursula,
worklock,
cloudworkers,
contacts,
porter
@ -75,7 +74,6 @@ ENTRY_POINTS = (
enrico.enrico, # Encryptor of Data
ursula.ursula, # Untrusted Re-Encryption Proxy
stake.stake, # Stake Management
worklock.worklock, # WorkLock
# Utility Commands
status.status, # Network Status

View File

@ -1,158 +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 maya
from nucypher.blockchain.eth.agents import ContractAgency, WorkLockAgent
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import prettify_eth_amount
from nucypher.cli.literature import SUCCESSFUL_WORKLOCK_CLAIM, WORKLOCK_AGREEMENT
def paint_bidding_notice(emitter, bidder):
message = WORKLOCK_AGREEMENT.format(refund_rate=prettify_eth_amount(bidder.worklock_agent.get_bonus_refund_rate()),
end_date=maya.MayaDT(bidder.economics.bidding_end_date).local_datetime(),
bidder_address=bidder.checksum_address,
duration=bidder.economics.worklock_commitment_duration)
emitter.echo(message)
return
def paint_worklock_claim(emitter, bidder_address: str, network: str, provider_uri: str):
message = SUCCESSFUL_WORKLOCK_CLAIM.format(bidder_address=bidder_address,
network=network,
provider_uri=provider_uri)
emitter.echo(message, color='green')
def paint_worklock_status(emitter, registry: BaseContractRegistry):
from maya import MayaDT
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
blockchain = worklock_agent.blockchain
# Time
bidding_start = MayaDT(worklock_agent.start_bidding_date)
bidding_end = MayaDT(worklock_agent.end_bidding_date)
cancellation_end = MayaDT(worklock_agent.end_cancellation_date)
bidding_duration = bidding_end - bidding_start
cancellation_duration = cancellation_end - bidding_start
now = maya.now()
bidding_remaining = bidding_end - now if bidding_end > now else 'Closed'
cancellation_remaining = cancellation_end - now if cancellation_end > now else "Closed"
cancellation_open = bidding_start <= now <= cancellation_end
bidding_open = bidding_start <= now <= bidding_end
# Refund
refund_multiple = worklock_agent.boosting_refund / worklock_agent.slowing_refund
emitter.echo(f"\nWorkLock ({worklock_agent.contract_address})", bold=True, color='green')
payload = f"""
Time
Escrow Period ({'Open' if bidding_open else 'Closed'})
------------------------------------------------------
Allocations Available . {'Yes' if worklock_agent.is_claiming_available() else 'No'}
Start Date ............ {bidding_start}
End Date .............. {bidding_end}
Duration .............. {bidding_duration}
Time Remaining ........ {bidding_remaining}
Cancellation Period ({'Open' if cancellation_open else 'Closed'})
------------------------------------------------------
End Date .............. {cancellation_end}
Duration .............. {cancellation_duration}
Time Remaining ........ {cancellation_remaining}
Economics
Participation
------------------------------------------------------
Lot Size .............. {NU.from_units(worklock_agent.lot_value)}
Min. Allowed Escrow ... {prettify_eth_amount(worklock_agent.minimum_allowed_bid)}
Participants .......... {worklock_agent.get_bidders_population()}
ETH Supply ............ {prettify_eth_amount(worklock_agent.get_eth_supply())}
ETH Pool .............. {prettify_eth_amount(blockchain.client.get_balance(worklock_agent.contract_address))}
Base (minimum escrow)
------------------------------------------------------
Base Deposit Rate ..... {worklock_agent.get_base_deposit_rate()} NU per base ETH
Bonus (surplus over minimum escrow)
------------------------------------------------------
Bonus ETH Supply ...... {prettify_eth_amount(worklock_agent.get_bonus_eth_supply())}
Bonus Lot Size ........ {NU.from_units(worklock_agent.get_bonus_lot_value())}
Bonus Deposit Rate .... {worklock_agent.get_bonus_deposit_rate()} NU per bonus ETH
Refunds
------------------------------------------------------
Refund Rate Multiple .. {refund_multiple:.2f}
Bonus Refund Rate ..... {worklock_agent.get_bonus_refund_rate()} units of work to unlock 1 bonus ETH
Base Refund Rate ...... {worklock_agent.get_base_refund_rate()} units of work to unlock 1 base ETH
* NOTE: bonus ETH is refunded before base ETH
"""
emitter.echo(payload)
def paint_bidder_status(emitter, bidder):
claim = NU.from_units(bidder.available_claim)
if claim > bidder.economics.maximum_allowed_locked:
claim = f"{claim} (Above the allowed max. The escrow will be partially refunded)"
deposited_eth = bidder.get_deposited_eth
bonus_eth = deposited_eth - bidder.economics.worklock_min_allowed_bid
message = f"""
WorkLock Participant {bidder.checksum_address}
"""
if bidder.has_claimed:
message += f"""
NU Claimed? ............ Yes
Locked ETH ............. {prettify_eth_amount(bidder.get_deposited_eth)}"""
else:
message += f"""
NU Claimed? ............ No
Total Escrow ........... {prettify_eth_amount(deposited_eth)}
Base ETH ........... {prettify_eth_amount(bidder.economics.worklock_min_allowed_bid)}
Bonus ETH .......... {prettify_eth_amount(bonus_eth)}
NU Allocated ........... {claim}"""
compensation = bidder.available_compensation
if compensation:
message += f"""
Unspent Escrow Amount .. {prettify_eth_amount(compensation)}"""
message += f"""\n
Completed Work ......... {bidder.completed_work}
Available Refund ....... {prettify_eth_amount(bidder.available_refund)}
Refunded Work .......... {bidder.refunded_work}
Remaining Work ......... {bidder.remaining_work}
"""
emitter.echo(message)

View File

@ -32,16 +32,6 @@ PeriodDelta = NewType('PeriodDelta', int)
ContractReturnValue = TypeVar('ContractReturnValue', bound=Union[TxReceipt, Wei, int, str, bool])
class WorklockParameters(Tuple):
token_supply: NuNits
start_bid_date: Timestamp
end_bid_date: Timestamp
end_cancellation_date: Timestamp
boosting_refund: int
staking_periods: int
min_allowed_bid: Wei
class StakingEscrowParameters(Tuple):
seconds_per_period: int
minting_coefficient: int

View File

@ -15,7 +15,6 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from nucypher.blockchain.eth.events import ContractEventsThrottler
from nucypher.blockchain.eth.utils import estimate_block_number_for_period
try:
from prometheus_client import Gauge, Enum, Counter, Info, Histogram, Summary
@ -28,7 +27,7 @@ from eth_typing.evm import ChecksumAddress
import nucypher
from nucypher.blockchain.eth.actors import NucypherTokenActor
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent, StakingEscrowAgent, WorkLockAgent, \
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, \
PREApplicationAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
@ -36,7 +35,7 @@ from nucypher.datastore.queries import get_reencryption_requests
from typing import Dict, Union, Type
ContractAgents = Union[StakingEscrowAgent, WorkLockAgent, PolicyManagerAgent]
ContractAgents = Union[StakingEscrowAgent]
class MetricsCollector(ABC):
@ -241,43 +240,6 @@ class OperatorMetricsCollector(BaseMetricsCollector):
self.metrics["worker_token_balance_gauge"].set(int(nucypher_worker_token_actor.token_balance))
class WorkLockMetricsCollector(BaseMetricsCollector):
"""Collector for WorkLock specific metrics."""
def __init__(self, staker_address: ChecksumAddress, contract_registry: BaseContractRegistry):
super().__init__()
self.staker_address = staker_address
self.contract_registry = contract_registry
def initialize(self, metrics_prefix: str, registry: CollectorRegistry) -> None:
self.metrics = {
"available_refund_gauge": Gauge(f'{metrics_prefix}_available_refund',
'Available refund',
registry=registry),
"worklock_remaining_work_gauge": Gauge(f'{metrics_prefix}_worklock_refund_remaining_work',
'Worklock remaining work',
registry=registry),
"worklock_refund_completed_work_gauge": Gauge(f'{metrics_prefix}_worklock_refund_completedWork',
'Worklock completed work',
registry=registry),
}
def _collect_internal(self) -> None:
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.contract_registry)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=self.contract_registry)
self.metrics["available_refund_gauge"].set(
worklock_agent.get_available_refund(checksum_address=self.staker_address))
self.metrics["worklock_remaining_work_gauge"].set(
worklock_agent.get_remaining_work(checksum_address=self.staker_address)
)
self.metrics["worklock_refund_completed_work_gauge"].set(
staking_agent.get_completed_work(bidder_address=self.staker_address) -
worklock_agent.get_refunded_work(checksum_address=self.staker_address)
)
class EventMetricsCollector(BaseMetricsCollector):
"""General collector for emitted events."""
def __init__(self,
@ -401,22 +363,3 @@ class OperatorBondedEventMetricsCollector(EventMetricsCollector):
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
self.metrics["current_worker_is_me_gauge"].set(
contract_agent.get_worker_from_staker(self.staker_address) == self.operator_address)
class WorkLockRefundEventMetricsCollector(EventMetricsCollector):
"""Collector for WorkLock Refund event."""
def __init__(self, staker_address: ChecksumAddress, event_name: str = 'Refund', *args, **kwargs):
super().__init__(event_name=event_name, argument_filters={'sender': staker_address}, *args, **kwargs)
self.staker_address = staker_address
def initialize(self, metrics_prefix: str, registry: CollectorRegistry) -> None:
super().initialize(metrics_prefix=metrics_prefix, registry=registry)
self.metrics["worklock_deposited_eth_gauge"] = Gauge(f'{metrics_prefix}_worklock_current_deposited_eth',
'Worklock deposited ETH',
registry=registry)
def _event_occurred(self, event) -> None:
super()._event_occurred(event)
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
self.metrics["worklock_deposited_eth_gauge"].set(contract_agent.get_deposited_eth(self.staker_address))

View File

@ -37,20 +37,19 @@ from nucypher.utilities.prometheus.collector import (
BlockchainMetricsCollector,
StakerMetricsCollector,
OperatorMetricsCollector,
WorkLockMetricsCollector,
EventMetricsCollector,
ReStakeEventMetricsCollector,
WindDownEventMetricsCollector,
OperatorBondedEventMetricsCollector,
CommitmentMadeEventMetricsCollector,
WorkLockRefundEventMetricsCollector)
CommitmentMadeEventMetricsCollector
)
from typing import List
from twisted.internet import reactor, task
from twisted.web.resource import Resource
from nucypher.blockchain.eth.agents import StakingEscrowAgent, PolicyManagerAgent, WorkLockAgent
from nucypher.blockchain.eth.agents import StakingEscrowAgent
class PrometheusMetricsConfig:
@ -205,24 +204,6 @@ def create_metrics_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[Met
metrics_prefix=metrics_prefix)
collectors.extend(staking_events_collectors)
# Policy Events
policy_events_collectors = create_policy_events_metric_collectors(ursula=ursula,
metrics_prefix=metrics_prefix)
collectors.extend(policy_events_collectors)
#
# WorkLock information - only collected for mainnet
#
if ursula.domain == NetworksInventory.MAINNET:
# WorkLock metrics
collectors.append(WorkLockMetricsCollector(staker_address=ursula.checksum_address,
contract_registry=ursula.registry))
# WorkLock Events
worklock_events_collectors = create_worklock_events_metric_collectors(ursula=ursula,
metrics_prefix=metrics_prefix)
collectors.extend(worklock_events_collectors)
return collectors
@ -305,35 +286,3 @@ def create_staking_events_metric_collectors(ursula: 'Ursula', metrics_prefix: st
))
return collectors
def create_worklock_events_metric_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[MetricsCollector]:
"""Create collectors for worklock-related events."""
# Refund
collectors: List[MetricsCollector] = [WorkLockRefundEventMetricsCollector(
event_args_config={
"refundETH": (Gauge, f'{metrics_prefix}_worklock_refund_refundETH',
'Refunded ETH'),
},
staker_address=ursula.checksum_address,
contract_agent_class=WorkLockAgent,
contract_registry=ursula.registry
)]
return collectors
def create_policy_events_metric_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[MetricsCollector]:
"""Create collectors for policy-related events."""
# Withdrawn
collectors: List[MetricsCollector] = [EventMetricsCollector(
event_name='Withdrawn',
event_args_config={
"value": (Gauge, f'{metrics_prefix}_policy_withdrawn_reward', 'Policy reward')
},
argument_filters={"recipient": ursula.checksum_address},
contract_agent_class=PolicyManagerAgent,
contract_registry=ursula.registry
)]
return collectors

View File

@ -30,18 +30,10 @@ CONTRACTS = {
'token': ['NuCypherToken',
'TokenRecipient'],
'main': ['StakingEscrow',
'PolicyManager',
'Adjudicator',
'WorkLock'],
'SimplePREApplication', # TODO change to PREApplication when ready
'Adjudicator'],
'proxy': ['Dispatcher',
'Upgradeable'],
'staking': ['StakingInterface',
'StakingInterfaceRouter',
'AbstractStakingContract',
'InitializableStakingContract',
'PoolingStakingContract',
'PoolingStakingContractV2',
'WorkLockPoolingContract']
}

View File

@ -1,226 +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 random
import pytest
from eth_tester.exceptions import TransactionFailed
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.actors import Bidder
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, WorkLockAgent
from nucypher.blockchain.eth.constants import NULL_ADDRESS
@pytest.mark.skip()
def test_create_bidder(testerchain, test_registry, agency, application_economics):
bidder_address = testerchain.unassigned_accounts[0]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(domain=TEMPORARY_DOMAIN,
registry=test_registry,
transacting_power=tpower)
assert bidder.checksum_address == bidder_address
assert bidder.registry == test_registry
assert not bidder.get_deposited_eth
assert not bidder.completed_work
assert not bidder.remaining_work
assert not bidder.refunded_work
@pytest.mark.skip()
def test_bidding(testerchain, agency, application_economics, test_registry):
min_allowed_bid = application_economics.worklock_min_allowed_bid
max_bid = 2000 * min_allowed_bid
small_bids = [random.randrange(min_allowed_bid, 2 * min_allowed_bid) for _ in range(10)]
total_small_bids = sum(small_bids)
min_potential_whale_bid = (max_bid - total_small_bids) // 9
whales_bids = [random.randrange(min_potential_whale_bid, max_bid) for _ in range(9)]
initial_bids = small_bids + whales_bids
for i, bid in enumerate(initial_bids):
bidder_address = testerchain.client.accounts[i]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
domain=TEMPORARY_DOMAIN,
transacting_power=tpower)
assert bidder.get_deposited_eth == 0
receipt = bidder.place_bid(value=bid)
assert receipt['status'] == 1
assert bidder.get_deposited_eth == bid
@pytest.mark.skip()
def test_cancel_bid(testerchain, agency, application_economics, test_registry):
# Wait until the bidding window closes...
testerchain.time_travel(seconds=application_economics.bidding_duration + 1)
bidder_address = testerchain.client.accounts[1]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
assert bidder.get_deposited_eth # Bid
receipt = bidder.cancel_bid() # Cancel
assert receipt['status'] == 1
assert not bidder.get_deposited_eth # No more bid
# Can't cancel a bid twice in a row
with pytest.raises((TransactionFailed, ValueError)):
_receipt = bidder.cancel_bid()
@pytest.mark.skip()
def test_get_remaining_work(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
remaining = bidder.remaining_work
assert remaining
@pytest.mark.skip()
def test_verify_correctness_before_refund(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
with pytest.raises(Bidder.CancellationWindowIsOpen):
_receipt = bidder.claim()
# Wait until the cancellation window closes...
testerchain.time_travel(seconds=application_economics.cancellation_window_duration + 1)
with pytest.raises(Bidder.BidderError):
_receipt = bidder.verify_bidding_correctness(gas_limit=100000)
assert not worklock_agent.bidders_checked()
assert bidder.get_whales()
assert not worklock_agent.is_claiming_available()
@pytest.mark.skip()
def test_force_refund(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
whales = bidder.get_whales()
# Simulate force refund
new_whales = whales.copy()
while new_whales:
whales.update(new_whales)
whales = bidder._reduce_bids(whales)
new_whales = bidder.get_whales()
bidder_address = testerchain.client.accounts[1]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
receipt = bidder.force_refund()
assert receipt['status'] == 1
assert not bidder.get_whales()
assert not worklock_agent.bidders_checked()
# Compare off-chain and on-chain calculations
min_bid = application_economics.worklock_min_allowed_bid
for whale, bonus in whales.items():
contract_bid = worklock_agent.get_deposited_eth(whale)
assert bonus == contract_bid - min_bid
@pytest.mark.skip()
def test_verify_correctness(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
assert not worklock_agent.bidders_checked()
with pytest.raises(Bidder.ClaimError):
_receipt = bidder.claim()
receipts = bidder.verify_bidding_correctness(gas_limit=100000)
assert worklock_agent.bidders_checked()
assert worklock_agent.is_claiming_available()
for iteration, receipt in receipts.items():
assert receipt['status'] == 1
@pytest.mark.skip()
def test_withdraw_compensation(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[12]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
assert worklock_agent.get_available_compensation(checksum_address=bidder_address) > 0
receipt = bidder.withdraw_compensation()
assert receipt['status'] == 1
assert worklock_agent.get_available_compensation(checksum_address=bidder_address) == 0
@pytest.mark.skip()
def test_claim(testerchain, agency, application_economics, test_registry):
bidder_address = testerchain.client.accounts[11]
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
bidder = Bidder(registry=test_registry,
transacting_power=tpower,
domain=TEMPORARY_DOMAIN)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
# Ensure that the bidder is not staking.
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder.checksum_address, periods=10)
assert locked_tokens == 0
receipt = bidder.claim()
assert receipt['status'] == 1
# Cant claim more than once
with pytest.raises(Bidder.ClaimError):
_receipt = bidder.claim()
assert bidder.get_deposited_eth > application_economics.worklock_min_allowed_bid
assert bidder.completed_work == 0
assert bidder.remaining_work <= application_economics.maximum_allowed_locked // 2
assert bidder.refunded_work == 0
# Ensure that the claimant is now the holder of an unbonded stake.
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder.checksum_address, periods=10)
assert locked_tokens <= application_economics.maximum_allowed_locked
# Confirm the stake is unbonded
worker_address = staking_agent.get_worker_from_staker(staker_address=bidder.checksum_address)
assert worker_address == NULL_ADDRESS

View File

@ -1,216 +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 nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, WorkLockAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterface
@pytest.mark.skip()
def test_create_worklock_agent(testerchain, test_registry, agency, application_economics):
agent = WorkLockAgent(registry=test_registry)
assert agent.contract_address
same_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
assert agent == same_agent
assert not agent.is_claiming_available()
@pytest.mark.skip()
def test_bidding(testerchain, agency, application_economics, test_registry):
small_bid = application_economics.worklock_min_allowed_bid
big_bid = 5 * application_economics.worklock_min_allowed_bid
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
# Round 1
for multiplier, bidder in enumerate(testerchain.client.accounts[:11], start=1):
bid = big_bid * multiplier
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
receipt = agent.bid(transacting_power=tpower, value=bid)
assert receipt['status'] == 1
# Round 2
for multiplier, bidder in enumerate(testerchain.client.accounts[:11], start=1):
bid = (small_bid * 2) * multiplier
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
receipt = agent.bid(transacting_power=tpower, value=bid)
assert receipt['status'] == 1
@pytest.mark.skip()
def test_get_deposited_eth(testerchain, agency, application_economics, test_registry):
small_bid = application_economics.worklock_min_allowed_bid
small_bidder = testerchain.client.accounts[-1]
tpower = TransactingPower(account=small_bidder, signer=Web3Signer(testerchain.client))
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
receipt = agent.bid(transacting_power=tpower, value=small_bid)
assert receipt['status'] == 1
bid = agent.get_deposited_eth(small_bidder)
assert bid == small_bid
@pytest.mark.skip()
def test_get_base_deposit_rate(agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
base_deposit_rate = agent.get_base_deposit_rate()
assert base_deposit_rate == application_economics.min_authorization / application_economics.worklock_min_allowed_bid
@pytest.mark.skip()
def test_get_base_refund_rate(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
base_refund_rate = agent.get_base_refund_rate()
slowing_refund = agent.contract.functions.SLOWING_REFUND().call()
assert base_refund_rate == (application_economics.min_authorization / application_economics.worklock_min_allowed_bid) * \
(slowing_refund / application_economics.worklock_boosting_refund_rate)
@pytest.mark.skip()
def test_cancel_bid(testerchain, agency, application_economics, test_registry):
bidder = testerchain.client.accounts[1]
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
assert agent.get_deposited_eth(bidder) # Bid
receipt = agent.cancel_bid(transacting_power=tpower) # Cancel
assert receipt['status'] == 1
assert not agent.get_deposited_eth(bidder) # No more bid
# Can't cancel a bid twice in a row
with pytest.raises((TransactionFailed, ValueError)):
_receipt = agent.cancel_bid(transacting_power=tpower)
@pytest.mark.skip()
def test_get_remaining_work(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
bidder = testerchain.client.accounts[0]
remaining = agent.get_remaining_work(checksum_address=bidder)
assert remaining > 0
@pytest.mark.skip()
def test_early_claim(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
bidder = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
with pytest.raises(TransactionFailed):
_receipt = agent.claim(transacting_power=tpower)
@pytest.mark.skip()
def test_cancel_after_bidding(testerchain, agency, application_economics, test_registry):
# Wait until the bidding window closes...
testerchain.time_travel(seconds=application_economics.bidding_duration + 1)
bidder = testerchain.client.accounts[0]
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
assert agent.get_deposited_eth(bidder) # Bid
receipt = agent.cancel_bid(transacting_power=tpower) # Cancel
assert receipt['status'] == 1
assert not agent.get_deposited_eth(bidder) # No more bid
@pytest.mark.skip()
def test_claim_before_checking(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
bidder = testerchain.client.accounts[2]
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
assert not agent.is_claiming_available()
with pytest.raises(TransactionFailed):
_receipt = agent.claim(transacting_power=tpower)
# Wait until the cancellation window closes...
testerchain.time_travel(seconds=application_economics.cancellation_end_date + 1)
assert not agent.is_claiming_available()
with pytest.raises(TransactionFailed):
_receipt = agent.claim(transacting_power=tpower)
@pytest.mark.skip()
def test_force_refund(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
caller = testerchain.client.accounts[0]
tpower = TransactingPower(account=caller, signer=Web3Signer(testerchain.client))
with pytest.raises(BlockchainInterface.InterfaceError):
_receipt = agent.verify_bidding_correctness(transacting_power=tpower, gas_limit=100000)
receipt = agent.force_refund(transacting_power=tpower, addresses=testerchain.client.accounts[2:11])
assert receipt['status'] == 1
assert agent.get_available_compensation(testerchain.client.accounts[2]) > 0
@pytest.mark.skip()
def test_verify_correctness(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry) # type: WorkLockAgent
caller = testerchain.client.accounts[0]
tpower = TransactingPower(account=caller, signer=Web3Signer(testerchain.client))
assert not agent.bidders_checked()
assert agent.estimate_verifying_correctness(gas_limit=100000) == 10
receipt = agent.verify_bidding_correctness(transacting_power=tpower, gas_limit=100000)
assert receipt['status'] == 1
assert agent.bidders_checked()
assert agent.is_claiming_available()
@pytest.mark.skip()
def test_withdraw_compensation(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
bidder = testerchain.client.accounts[2]
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
balance = testerchain.w3.eth.getBalance(bidder)
receipt = agent.withdraw_compensation(transacting_power=tpower)
assert receipt['status'] == 1
assert testerchain.w3.eth.getBalance(bidder) > balance
assert agent.get_available_compensation(testerchain.client.accounts[2]) == 0
@pytest.mark.skip()
def test_successful_claim(testerchain, agency, application_economics, test_registry):
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
bidder = testerchain.client.accounts[2]
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
# Ensure that the bidder is not staking.
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder, periods=5)
assert locked_tokens == 0
receipt = agent.claim(transacting_power=tpower)
assert receipt['status'] == 1
# Cant claim more than once
with pytest.raises(TransactionFailed):
_receipt = agent.claim(transacting_power=tpower)
# Ensure that the claimant is now the holder of a stake.
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder, periods=5)
assert locked_tokens > 0

View File

@ -18,7 +18,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import pytest
from nucypher.blockchain.eth.agents import WorkLockAgent, PREApplicationAgent
from nucypher.blockchain.eth.agents import PREApplicationAgent
from nucypher.blockchain.eth.constants import PRE_APPLICATION_CONTRACT_NAME
from nucypher.blockchain.eth.deployers import PREApplicationDeployer

View File

@ -1,95 +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 constant_sorrow import constants
from nucypher.blockchain.economics import EconomicsFactory, Economics
from nucypher.blockchain.eth.agents import WorkLockAgent
from nucypher.blockchain.eth.constants import WORKLOCK_CONTRACT_NAME
from nucypher.blockchain.eth.deployers import WorklockDeployer
@pytest.fixture(scope='module')
def baseline_deployment(staking_escrow_stub_deployer, transacting_power):
staking_escrow_stub_deployer.deploy(deployment_mode=constants.INIT, transacting_power=transacting_power)
@pytest.fixture(scope="module")
def worklock_deployer(baseline_deployment,
testerchain,
test_registry,
application_economics):
worklock_deployer = WorklockDeployer(registry=test_registry, economics=application_economics)
return worklock_deployer
@pytest.mark.skip()
def test_worklock_deployment(worklock_deployer,
baseline_deployment,
staking_escrow_stub_deployer,
deployment_progress,
test_registry,
testerchain,
transacting_power):
# Deploy
assert worklock_deployer.contract_name == WORKLOCK_CONTRACT_NAME
deployment_receipts = worklock_deployer.deploy(progress=deployment_progress,
transacting_power=transacting_power) # < ---- DEPLOY
# deployment steps must match expected number of steps
steps = worklock_deployer.deployment_steps
assert deployment_progress.num_steps == len(steps) == len(deployment_receipts) == 3
# Ensure every step is successful
for step_title in steps:
assert deployment_receipts[step_title]['status'] == 1
# Ensure the correct staking escrow address is set
staking_escrow_address = worklock_deployer.contract.functions.escrow().call()
assert staking_escrow_stub_deployer.contract_address == staking_escrow_address
@pytest.mark.skip()
def test_make_agent(worklock_deployer, test_registry):
agent = worklock_deployer.make_agent()
# Retrieve the PolicyManagerAgent singleton
another_worklock_agent = WorkLockAgent(registry=test_registry)
assert agent == another_worklock_agent # __eq__
# Compare the contract address for equality
assert agent.contract_address == another_worklock_agent.contract_address
@pytest.mark.skip()
def test_deployment_parameters(worklock_deployer, test_registry, application_economics):
# Ensure restoration of deployment parameters
agent = worklock_deployer.make_agent()
params = agent.worklock_parameters()
supply, start, end, end_cancellation, boost, locktime, min_bid = params
assert application_economics.worklock_supply == supply
assert application_economics.bidding_start_date == start
assert application_economics.bidding_end_date == end
assert application_economics.cancellation_end_date == end_cancellation
assert application_economics.worklock_boosting_refund_rate == boost
assert application_economics.worklock_commitment_duration == locktime
assert application_economics.worklock_min_allowed_bid == min_bid

View File

@ -28,7 +28,6 @@ from nucypher.blockchain.eth.constants import (
ADJUDICATOR_CONTRACT_NAME,
DISPATCHER_CONTRACT_NAME,
NUCYPHER_TOKEN_CONTRACT_NAME,
POLICY_MANAGER_CONTRACT_NAME,
STAKING_ESCROW_CONTRACT_NAME, STAKING_ESCROW_STUB_CONTRACT_NAME
)
from nucypher.blockchain.eth.deployers import (
@ -91,7 +90,6 @@ def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
assert staking_agent.owner == testerchain.etherbase_account
assert policy_agent.owner == testerchain.etherbase_account
assert adjudicator_agent.owner == testerchain.etherbase_account
maclane = testerchain.unassigned_accounts[0]
@ -115,7 +113,6 @@ def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
assert result.exit_code == 0
assert staking_agent.owner == maclane
assert policy_agent.owner == testerchain.etherbase_account
assert adjudicator_agent.owner == testerchain.etherbase_account
michwill = testerchain.unassigned_accounts[1]
@ -256,20 +253,6 @@ def test_manual_deployment_of_idle_network(click_runner):
deployed_contracts.extend([STAKING_ESCROW_STUB_CONTRACT_NAME, DISPATCHER_CONTRACT_NAME])
assert list(new_registry.enrolled_names) == deployed_contracts
# 3. Deploy PolicyManager
command = ('contracts',
'--contract-name', POLICY_MANAGER_CONTRACT_NAME,
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--registry-infile', str(ALTERNATE_REGISTRY_FILEPATH_2.absolute()))
result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
deployed_contracts.extend([POLICY_MANAGER_CONTRACT_NAME, DISPATCHER_CONTRACT_NAME])
assert list(new_registry.enrolled_names) == deployed_contracts
# 4. Deploy Adjudicator
command = ('contracts',
'--contract-name', ADJUDICATOR_CONTRACT_NAME,

View File

@ -1,316 +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 random
import tempfile
import pytest
from eth_utils import to_wei
from web3 import Web3
from nucypher.blockchain.eth.actors import Bidder, Staker
from nucypher.blockchain.eth.agents import (
ContractAgency,
WorkLockAgent
)
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.token import NU
from nucypher.characters.lawful import Ursula
from nucypher.cli.commands.worklock import worklock
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.powers import TransactingPower
from nucypher.policy.payment import SubscriptionManagerPayment
from tests.constants import (INSECURE_DEVELOPMENT_PASSWORD, MOCK_IP_ADDRESS, TEST_PROVIDER_URI, MOCK_PROVIDER_URI)
from tests.utils.ursula import select_test_port
@pytest.fixture(scope='module')
def bids(testerchain):
bids_distribution = dict()
min_bid_eth_value = 1
max_bid_eth_value = 10
whale = testerchain.client.accounts[0]
bids_distribution[whale] = 50_000
for bidder in testerchain.client.accounts[1:12]:
bids_distribution[bidder] = random.randrange(min_bid_eth_value, max_bid_eth_value)
return bids_distribution
@pytest.mark.skip()
def test_status(click_runner, testerchain, agency_local_registry, application_economics):
command = ('status',
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
assert str(NU.from_units(application_economics.worklock_supply)) in result.output
assert str(Web3.fromWei(application_economics.worklock_min_allowed_bid, 'ether')) in result.output
@pytest.mark.skip()
def test_bid(click_runner, testerchain, agency_local_registry, application_economics, bids):
# Wait until biding window starts
testerchain.time_travel(seconds=90)
base_command = ('escrow',
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
total_bids = 0
# Multiple bidders
for bidder, bid_eth_value in bids.items():
pre_bid_balance = testerchain.client.get_balance(bidder)
assert pre_bid_balance > to_wei(bid_eth_value, 'ether')
command = (*base_command, '--participant-address', bidder, '--value', bid_eth_value)
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
post_bid_balance = testerchain.client.get_balance(bidder)
difference = pre_bid_balance - post_bid_balance
assert difference >= to_wei(bid_eth_value, 'ether')
total_bids += to_wei(bid_eth_value, 'ether')
assert testerchain.client.get_balance(worklock_agent.contract_address) == total_bids
@pytest.mark.skip()
def test_cancel_bid(click_runner, testerchain, agency_local_registry, application_economics, bids):
bidders = list(bids.keys())
bidder = bidders[-1]
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
command = ('cancel-escrow',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert not agent.get_deposited_eth(bidder) # No more bid
# Wait until the end of the bidding period
testerchain.time_travel(seconds=application_economics.bidding_duration + 2)
bidder = bidders[-2]
command = ('cancel-escrow',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert not agent.get_deposited_eth(bidder) # No more bid
@pytest.mark.skip()
def test_enable_claiming(click_runner, testerchain, agency_local_registry, application_economics):
# Wait until the end of the cancellation period
testerchain.time_travel(seconds=application_economics.cancellation_window_duration + 2)
bidder = testerchain.client.accounts[0]
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
assert not agent.is_claiming_available()
assert not agent.bidders_checked()
command = ('enable-claiming',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--force',
'--network', TEMPORARY_DOMAIN,
'--gas-limit', 100000)
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert agent.is_claiming_available()
assert agent.bidders_checked()
@pytest.mark.skip()
def test_claim(click_runner, testerchain, agency_local_registry, application_economics):
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
bidder = testerchain.client.accounts[2]
command = ('claim',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
whale = testerchain.client.accounts[0]
assert agent.get_available_compensation(checksum_address=whale) > 0
command = ('claim',
'--participant-address', whale,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert agent.get_available_compensation(checksum_address=whale) == 0
# TODO: Check successful new stake in StakingEscrow
@pytest.mark.skip()
def test_remaining_work(click_runner, testerchain, agency_local_registry, application_economics):
bidder = testerchain.client.accounts[2]
# Ensure there is remaining work one layer below
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
assert remaining_work > 0
command = ('remaining-work',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
# Ensure were displaying the bidder address and remaining work in the output
assert bidder in result.output
assert str(remaining_work) in result.output
@pytest.mark.skip()
def test_refund(click_runner, testerchain, agency_local_registry, application_economics):
bidder = testerchain.client.accounts[2]
operator_address = testerchain.unassigned_accounts[-1]
#
# WorkLock Staker-Operator
#
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
# Bidder is now STAKER. Bond a worker.
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
staker = Staker(transacting_power=tpower,
domain=TEMPORARY_DOMAIN,
registry=agency_local_registry)
receipt = staker.bond_worker(operator_address=operator_address)
assert receipt['status'] == 1
worker = Ursula(is_me=True,
domain=TEMPORARY_DOMAIN,
provider_uri=MOCK_PROVIDER_URI,
registry=agency_local_registry,
checksum_address=bidder,
signer=Web3Signer(testerchain.client),
operator_address=operator_address,
rest_host=MOCK_IP_ADDRESS,
rest_port=select_test_port(),
db_filepath=tempfile.mkdtemp(),
payment_method=SubscriptionManagerPayment(provider=TEST_PROVIDER_URI, network=TEMPORARY_DOMAIN))
# Ensure there is work to do
remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
assert remaining_work > 0
# Do some work
testerchain.time_travel(periods=1)
for i in range(3):
txhash = worker.commit_to_next_period()
testerchain.wait_for_receipt(txhash)
assert receipt['status'] == 1
testerchain.time_travel(periods=1)
command = ('refund',
'--participant-address', bidder,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
# Less work to do...
new_remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
assert new_remaining_work < remaining_work
@pytest.mark.skip()
def test_participant_status(click_runner, testerchain, agency_local_registry, application_economics):
tpower = TransactingPower(account=testerchain.client.accounts[2],
signer=Web3Signer(testerchain.client))
bidder = Bidder(transacting_power=tpower,
domain=TEMPORARY_DOMAIN,
registry=agency_local_registry)
command = ('status',
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--participant-address', bidder.checksum_address,
'--provider', TEST_PROVIDER_URI,
'--signer', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
# Bidder-specific data is displayed
assert bidder.checksum_address in result.output
assert str(bidder.remaining_work) in result.output
assert str(bidder.available_refund) in result.output
# Worklock economics are displayed
assert str(application_economics.worklock_boosting_refund_rate) in result.output
assert str(NU.from_units(application_economics.worklock_supply)) in result.output

View File

@ -1,60 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.0;
import "contracts/NuCypherToken.sol";
/**
* @notice Contract for using in WorkLock tests
*/
contract StakingEscrowForWorkLockMock {
struct StakerInfo {
uint256 value;
bool measureWork;
uint256 completedWork;
uint16 periods;
}
NuCypherToken public immutable token;
uint32 public immutable secondsPerPeriod = 1;
uint256 public immutable minAllowableLockedTokens;
uint256 public immutable maxAllowableLockedTokens;
uint16 public immutable minLockedPeriods;
mapping (address => StakerInfo) public stakerInfo;
constructor(
NuCypherToken _token,
uint256 _minAllowableLockedTokens,
uint256 _maxAllowableLockedTokens,
uint16 _minLockedPeriods
) {
token = _token;
minAllowableLockedTokens = _minAllowableLockedTokens;
maxAllowableLockedTokens = _maxAllowableLockedTokens;
minLockedPeriods = _minLockedPeriods;
}
function getCompletedWork(address _staker) external view returns (uint256) {
return stakerInfo[_staker].completedWork;
}
function setWorkMeasurement(address _staker, bool _measureWork) external returns (uint256) {
stakerInfo[_staker].measureWork = _measureWork;
return stakerInfo[_staker].completedWork;
}
function depositFromWorkLock(address _staker, uint256 _value, uint16 _periods) external {
stakerInfo[_staker].value = _value;
stakerInfo[_staker].periods = _periods;
token.transferFrom(msg.sender, address(this), _value);
}
function setCompletedWork(address _staker, uint256 _completedWork) external {
stakerInfo[_staker].completedWork = _completedWork;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -20,22 +20,17 @@ from pathlib import Path
import pytest
import requests
from eth_utils import to_wei
from constant_sorrow import constants
from web3.exceptions import ValidationError
from nucypher.blockchain.economics import Economics
from nucypher.blockchain.eth.agents import StakingEscrowAgent, WorkLockAgent
from nucypher.blockchain.eth.deployers import (
AdjudicatorDeployer,
BaseContractDeployer,
NucypherTokenDeployer,
StakingEscrowDeployer,
WorklockDeployer
StakingEscrowDeployer
)
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContractRegistry
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.sol.compile.constants import SOLIDITY_SOURCE_ROOT, TEST_SOLIDITY_SOURCE_ROOT
from nucypher.blockchain.eth.sol.compile.types import SourceBundle

View File

@ -1,683 +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 random
from unittest.mock import call, patch, PropertyMock
import maya
import pytest
from eth_utils import to_wei
from web3 import Web3
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.actors import Bidder
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.utils import prettify_eth_amount
from nucypher.cli.commands import worklock as worklock_command
from nucypher.cli.commands.worklock import worklock
from nucypher.cli.literature import (
BID_AMOUNT_PROMPT_WITH_MIN_BID,
BID_INCREASE_AMOUNT_PROMPT,
BIDDING_WINDOW_CLOSED,
CLAIMING_NOT_AVAILABLE,
COLLECT_ETH_PASSWORD,
CONFIRM_BID_VERIFICATION,
CONFIRM_COLLECT_WORKLOCK_REFUND,
CONFIRM_REQUEST_WORKLOCK_COMPENSATION,
CONFIRM_WORKLOCK_CLAIM,
GENERIC_SELECT_ACCOUNT,
SELECTED_ACCOUNT,
WORKLOCK_CLAIM_ADVISORY
)
from nucypher.config.constants import TEMPORARY_DOMAIN
from tests.constants import MOCK_PROVIDER_URI, YES, NO, INSECURE_DEVELOPMENT_PASSWORD
from tests.mock.agents import MockContractAgent
@pytest.fixture()
def surrogate_transacting_power(mock_testerchain):
address = mock_testerchain.etherbase_account
signer = Web3Signer(mock_testerchain.client)
tpower = TransactingPower(account=address, signer=signer)
return tpower
@pytest.fixture()
def surrogate_bidder(mock_testerchain, test_registry, mock_worklock_agent, surrogate_transacting_power):
bidder = Bidder(registry=test_registry,
transacting_power=surrogate_transacting_power,
domain=TEMPORARY_DOMAIN)
return bidder
def assert_successful_transaction_echo(bidder_address: str, cli_output: str):
expected = (bidder_address,
MockContractAgent.FAKE_RECEIPT['blockHash'].hex(),
MockContractAgent.FAKE_RECEIPT['blockNumber'],
MockContractAgent.FAKE_RECEIPT['transactionHash'].hex())
for output in expected:
assert str(output) in cli_output, f'"{output}" not in bidding output'
@pytest.mark.skip('remove me')
def test_status(click_runner, mock_worklock_agent, test_registry_source_manager):
command = ('status', '--provider', MOCK_PROVIDER_URI, '--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
@pytest.mark.skip('remove me')
def test_account_selection(click_runner, mocker, mock_testerchain, mock_worklock_agent, test_registry_source_manager):
accounts = list(mock_testerchain.client.accounts)
index = random.choice(range(len(accounts)))
the_chosen_one = accounts[index]
# I spy
mock_select = mocker.spy(worklock_command, 'select_client_account')
command = ('cancel-escrow',
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
user_input = '\n'.join((str(index), INSECURE_DEVELOPMENT_PASSWORD, YES, YES))
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
# Check call
mock_select.assert_called_once()
# Check output
assert GENERIC_SELECT_ACCOUNT in result.output
assert SELECTED_ACCOUNT.format(choice=index, chosen_account=the_chosen_one) in result.output
assert COLLECT_ETH_PASSWORD.format(checksum_address=the_chosen_one) in result.output
@pytest.fixture()
def bidding_command(application_economics, surrogate_bidder):
minimum = application_economics.worklock_min_allowed_bid
bid_value = random.randint(minimum, minimum*100)
command = ('escrow',
'--participant-address', surrogate_bidder.checksum_address,
'--value', bid_value,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
return command
@pytest.mark.skip('remove me')
def test_bid_too_soon(click_runner,
mocker,
mock_worklock_agent,
application_economics,
test_registry_source_manager,
surrogate_bidder,
mock_testerchain,
bidding_command):
a_month_in_seconds = 3600*24*30
# Bidding window is not open yet
the_past = maya.now().epoch - a_month_in_seconds
user_input = INSECURE_DEVELOPMENT_PASSWORD
with patch.object(maya, 'now', return_value=mocker.Mock(epoch=the_past)):
result = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=user_input)
assert result.exit_code == 1
assert BIDDING_WINDOW_CLOSED in result.output
# Let's assume that the previous check pass for some reason. It still should fail, at the Bidder layer
now = mock_testerchain.get_blocktime()
a_month_too_soon = now - a_month_in_seconds
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=a_month_too_soon)
with pytest.raises(Bidder.BiddingIsClosed):
_ = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=INSECURE_DEVELOPMENT_PASSWORD)
@pytest.mark.skip('remove me')
def test_bid_too_late(click_runner,
mocker,
mock_worklock_agent,
application_economics,
test_registry_source_manager,
surrogate_bidder,
mock_testerchain,
bidding_command):
a_month_in_seconds = 3600*24*30
# Bidding window is closed
the_future = maya.now().epoch + a_month_in_seconds
user_input = INSECURE_DEVELOPMENT_PASSWORD
with patch.object(maya, 'now', return_value=mocker.Mock(epoch=the_future)):
result = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=user_input)
assert result.exit_code == 1
assert BIDDING_WINDOW_CLOSED in result.output
# Let's assume that the previous check pass for some reason. It still should fail, at the Bidder layer
now = mock_testerchain.get_blocktime()
a_month_too_late = now + a_month_in_seconds
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=a_month_too_late)
with pytest.raises(Bidder.BiddingIsClosed):
_ = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=INSECURE_DEVELOPMENT_PASSWORD)
@pytest.mark.skip('remove me')
def test_valid_bid(click_runner,
mocker,
mock_worklock_agent,
application_economics,
test_registry_source_manager,
surrogate_bidder,
mock_testerchain,
surrogate_transacting_power):
now = mock_testerchain.get_blocktime()
sometime_later = now + 100
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
minimum = application_economics.worklock_min_allowed_bid
bid_value = random.randint(minimum, minimum * 100)
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
# Spy on the corresponding CLI function we are testing
mock_ensure = mocker.spy(Bidder, 'ensure_bidding_is_open')
mock_place_bid = mocker.spy(Bidder, 'place_bid')
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
deposited_eth_sequence = (
0, # When deciding if it's a new bid or increasing the existing one
0, # When placing the bid, inside Bidder.place_bid
bid_value, # When printing the CLI result, after the bid is placed ..
bid_value, # .. we use it twice
)
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
command = ('escrow',
'--participant-address', surrogate_bidder.checksum_address,
'--value', bid_value_in_eth,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
user_input = INSECURE_DEVELOPMENT_PASSWORD
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
assert result.exit_code == 0
# OK - Let's see what happened
# Bidder
mock_ensure.assert_called_once() # checked that the bidding window was open via actors layer
mock_place_bid.assert_called_once()
mock_place_bid.assert_called_once_with(surrogate_bidder, value=bid_value)
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
# Transactions
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.bid])
mock_worklock_agent.bid.assert_called_with(transacting_power=surrogate_transacting_power, value=bid_value)
# Calls
expected_calls = (mock_worklock_agent.eth_to_tokens, )
for expected_call in expected_calls:
expected_call.assert_called()
# CLI output
assert prettify_eth_amount(bid_value) in result.output
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_cancel_bid(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder,
surrogate_transacting_power):
# Spy on the corresponding CLI function we are testing
mock_cancel = mocker.spy(Bidder, 'cancel_bid')
command = ('cancel-escrow',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
assert result.exit_code == 0
# Bidder
mock_cancel.assert_called_once()
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
# Transactions
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.cancel_bid])
mock_worklock_agent.cancel_bid.called_once_with(transacting_power=surrogate_transacting_power)
# Calls
mock_worklock_agent.get_deposited_eth.assert_called_once()
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_enable_claiming(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder,
application_economics,
mock_testerchain,
surrogate_transacting_power):
# Spy on the corresponding CLI function we are testing
mock_force_refund = mocker.spy(Bidder, 'force_refund')
mock_verify = mocker.spy(Bidder, 'verify_bidding_correctness')
mock_get_whales = mocker.spy(Bidder, 'get_whales')
# Cancellation window is closed
now = mock_testerchain.get_blocktime()
sometime_later = now+(3600*30)
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
# Prepare bidders
bidders = mock_testerchain.client.accounts[0:10]
num_bidders = len(bidders)
bonus_lot_value = application_economics.worklock_supply - application_economics.min_authorization * num_bidders
bids_before = [to_wei(50_000, 'ether')]
min_bid_eth_value = to_wei(1, 'ether')
max_bid_eth_value = to_wei(10, 'ether')
for i in range(num_bidders - 1):
bids_before.append(random.randrange(min_bid_eth_value, max_bid_eth_value))
bonus_eth_supply_before = sum(bids_before) - application_economics.worklock_min_allowed_bid * num_bidders
bids_after = [min_bid_eth_value] * num_bidders
bonus_eth_supply_after = 0
min_bid = min(bids_before)
bidder_to_exclude = bids_before.index(min_bid)
bidders_to_check = bidders.copy()
del bidders_to_check[bidder_to_exclude]
mock_worklock_agent.get_bonus_eth_supply.side_effect = [bonus_eth_supply_before, bonus_eth_supply_after, bonus_eth_supply_after]
mock_worklock_agent.get_bonus_lot_value.return_value = bonus_lot_value
mock_worklock_agent.get_bidders.return_value = bidders
mock_worklock_agent.get_deposited_eth.side_effect = [*bids_before, *bids_after, *bids_after]
mock_worklock_agent.bidders_checked.side_effect = [False, False, False, False, True]
mock_worklock_agent.next_bidder_to_check.side_effect = [0, num_bidders // 2, num_bidders]
mock_worklock_agent.estimate_verifying_correctness.side_effect = [3, 6]
command = ('enable-claiming',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
gas_limit_1 = 200000
gas_limit_2 = 300000
user_input = '\n'.join((INSECURE_DEVELOPMENT_PASSWORD, YES, str(gas_limit_1), NO, str(gas_limit_2), YES))
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
confirmation = CONFIRM_BID_VERIFICATION.format(bidder_address=surrogate_bidder.checksum_address,
gas_limit=gas_limit_1,
bidders_per_transaction=3)
assert confirmation in result.output
confirmation = CONFIRM_BID_VERIFICATION.format(bidder_address=surrogate_bidder.checksum_address,
gas_limit=gas_limit_2,
bidders_per_transaction=6)
assert confirmation in result.output
# Bidder
mock_force_refund.assert_called_once()
mock_verify.assert_called_once()
mock_get_whales.assert_called()
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
# Manual checking of force_refund tx because of unpredictable order of actual bidders_to_check array
transaction_executions = mock_worklock_agent.force_refund.call_args_list
assert len(transaction_executions) == 1
_agent_args, agent_kwargs = transaction_executions[0]
tpower, addresses = agent_kwargs.values()
assert tpower.account == surrogate_bidder.checksum_address
assert sorted(addresses) == sorted(bidders_to_check)
mock_worklock_agent.verify_bidding_correctness.assert_has_calls([
call(transacting_power=surrogate_transacting_power, gas_limit=gas_limit_2),
call(transacting_power=surrogate_transacting_power, gas_limit=gas_limit_2)
])
mock_worklock_agent.assert_only_transactions([mock_worklock_agent.force_refund,
mock_worklock_agent.verify_bidding_correctness])
# Calls
mock_worklock_agent.estimate_verifying_correctness.assert_has_calls([
call(gas_limit=gas_limit_1),
call(gas_limit=gas_limit_2)
])
mock_worklock_agent.get_bidders.assert_called()
mock_worklock_agent.get_bonus_lot_value.assert_called()
mock_worklock_agent.get_bonus_eth_supply.assert_called()
mock_worklock_agent.next_bidder_to_check.assert_called()
mock_worklock_agent.get_deposited_eth.assert_called()
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_initial_claim(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder,
surrogate_transacting_power):
bidder_address = surrogate_bidder.checksum_address
command = ('claim',
'--participant-address', bidder_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
# First, let's test that if claiming is not available, command fails
mock_worklock_agent.is_claiming_available.return_value = False
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
assert result.exit_code == 1
assert CLAIMING_NOT_AVAILABLE in result.output
# Let's continue with our test and try the command again. But don't forget to restore the previous mock
mock_worklock_agent.is_claiming_available.return_value = True
# Spy on the corresponding CLI function we are testing
mock_withdraw_compensation = mocker.spy(Bidder, 'withdraw_compensation')
mock_claim = mocker.spy(Bidder, 'claim')
# TODO: Test this functionality in isolation
mocker.patch.object(Bidder, '_ensure_cancellation_window')
# Bidder has not claimed yet
mocker.patch.object(
Bidder, 'has_claimed',
new_callable=mocker.PropertyMock,
return_value=False
)
# Customize mock worklock agent method worklock parameters so position -2 returns lock periods
mock_worklock_agent.worklock_parameters.return_value = [0xAA, 0xBB, 30, 0xCC]
user_input = '\n'.join((INSECURE_DEVELOPMENT_PASSWORD, YES, YES))
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
assert CONFIRM_REQUEST_WORKLOCK_COMPENSATION.format(bidder_address=bidder_address) in result.output
assert WORKLOCK_CLAIM_ADVISORY.format(lock_duration=30) in result.output
assert CONFIRM_WORKLOCK_CLAIM.format(bidder_address=bidder_address) in result.output
mock_worklock_agent.claim.assert_called_once_with(transacting_power=surrogate_transacting_power)
# Bidder
mock_withdraw_compensation.assert_called_once()
mock_claim.assert_called_once()
assert_successful_transaction_echo(bidder_address=bidder_address, cli_output=result.output)
# Transactions
mock_worklock_agent.withdraw_compensation.assert_called_with(transacting_power=surrogate_transacting_power)
mock_worklock_agent.claim.assert_called_with(transacting_power=surrogate_transacting_power)
# Calls
expected_calls = (mock_worklock_agent.get_deposited_eth,
mock_worklock_agent.eth_to_tokens)
for expected_call in expected_calls:
expected_call.assert_called()
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_already_claimed(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder,
surrogate_transacting_power):
# Spy on the corresponding CLI function we are testing
mock_withdraw_compensation = mocker.spy(Bidder, 'withdraw_compensation')
mock_claim = mocker.spy(Bidder, 'claim')
# TODO: Test this functionality in isolation
mocker.patch.object(Bidder, '_ensure_cancellation_window')
# Bidder already claimed
mocker.patch.object(
Bidder, 'has_claimed',
new_callable=mocker.PropertyMock,
return_value=True
)
command = ('claim',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--force')
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
assert result.exit_code == 1 # TODO: Decide if this case should error (like now) or simply do nothing
# Bidder
mock_withdraw_compensation.assert_called_once()
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
mock_claim.assert_not_called()
# Transactions
mock_worklock_agent.withdraw_compensation.assert_called_with(transacting_power=surrogate_transacting_power)
mock_worklock_agent.claim.assert_not_called()
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_remaining_work(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder):
remaining_work = 100
mock_remaining_work = mocker.patch.object(Bidder,
'remaining_work',
new_callable=mocker.PropertyMock,
return_value=remaining_work)
command = ('remaining-work',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
assert str(remaining_work) in result.output, "Remaining work was not echoed."
# Bidder
mock_remaining_work.assert_called_once()
# Transactions
mock_worklock_agent.assert_no_transactions()
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_refund(click_runner,
mocker,
mock_worklock_agent,
surrogate_bidder,
surrogate_transacting_power):
# Spy on the corresponding CLI function we are testing
mock_refund = mocker.spy(Bidder, 'refund_deposit')
bidder_address = surrogate_bidder.checksum_address
command = ('refund',
'--participant-address', bidder_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
user_input = INSECURE_DEVELOPMENT_PASSWORD + '\n' + YES
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0
# Output
assert CONFIRM_COLLECT_WORKLOCK_REFUND.format(bidder_address=bidder_address) in result.output
# Bidder
mock_refund.assert_called_once()
assert_successful_transaction_echo(bidder_address=bidder_address, cli_output=result.output)
# Transactions
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.refund])
mock_worklock_agent.refund.assert_called_with(transacting_power=surrogate_transacting_power)
@pytest.mark.skip('remove me')
@pytest.mark.usefixtures("test_registry_source_manager")
def test_participant_status(click_runner,
mock_worklock_agent,
surrogate_bidder):
command = ('status',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(worklock, command, catch_exceptions=False)
assert result.exit_code == 0
expected_calls = (mock_worklock_agent.check_claim,
mock_worklock_agent.eth_to_tokens,
mock_worklock_agent.get_deposited_eth,
mock_worklock_agent.get_eth_supply,
mock_worklock_agent.get_base_deposit_rate,
mock_worklock_agent.get_bonus_lot_value,
mock_worklock_agent.get_bonus_deposit_rate,
mock_worklock_agent.get_bonus_refund_rate,
mock_worklock_agent.get_base_refund_rate,
mock_worklock_agent.get_remaining_work,
mock_worklock_agent.get_refunded_work)
# Calls
for expected_call in expected_calls:
expected_call.assert_called()
@pytest.mark.skip('remove me')
def test_interactive_new_bid(click_runner,
mocker,
mock_worklock_agent,
application_economics,
test_registry_source_manager,
surrogate_bidder,
mock_testerchain):
now = mock_testerchain.get_blocktime()
sometime_later = now + 100
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
minimum = application_economics.worklock_min_allowed_bid
bid_value = random.randint(minimum, minimum * 100)
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
wrong_bid = random.randint(1, minimum - 1)
wrong_bid_in_eth = Web3.fromWei(wrong_bid, 'ether')
# Spy on the corresponding CLI function we are testing
mock_place_bid = mocker.spy(Bidder, 'place_bid')
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
deposited_eth_sequence = (
0, # When deciding if it's a new bid or increasing the new one (in this case, a new bid)
0, # When placing the bid, inside Bidder.place_bid
bid_value, # When printing the CLI result, after the bid is placed ..
bid_value, # .. we use it twice
)
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
command = ('escrow',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,)
user_input = "\n".join((INSECURE_DEVELOPMENT_PASSWORD, str(wrong_bid_in_eth), str(bid_value_in_eth), YES))
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
assert result.exit_code == 0
# OK - Let's see what happened
# Bidder
mock_place_bid.assert_called_once()
# Output
minimum_in_eth = Web3.fromWei(minimum, 'ether')
expected_error = f"Error: {wrong_bid_in_eth} is smaller than the minimum valid value {minimum_in_eth}"
assert expected_error in result.output
expected_prompt = BID_AMOUNT_PROMPT_WITH_MIN_BID.format(minimum_bid_in_eth=Web3.fromWei(minimum, 'ether'))
assert 2 == result.output.count(expected_prompt)
@pytest.mark.skip('remove me')
def test_interactive_increase_bid(click_runner,
mocker,
mock_worklock_agent,
application_economics,
test_registry_source_manager,
surrogate_bidder,
mock_testerchain):
now = mock_testerchain.get_blocktime()
sometime_later = now + 100
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
minimum = application_economics.worklock_min_allowed_bid
bid_value = random.randint(1, minimum - 1)
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
# Spy on the corresponding CLI function we are testing
mock_place_bid = mocker.spy(Bidder, 'place_bid')
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
deposited_eth_sequence = (
minimum, # When deciding if it's a new bid or increasing the existing one (in this case, increasing)
minimum, # When placing the bid, inside Bidder.place_bid
minimum + bid_value, # When printing the CLI result, after the bid is placed ..
minimum + bid_value, # .. we use it twice
)
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
command = ('escrow',
'--participant-address', surrogate_bidder.checksum_address,
'--provider', MOCK_PROVIDER_URI,
'--signer', MOCK_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,)
user_input = "\n".join((INSECURE_DEVELOPMENT_PASSWORD, str(bid_value_in_eth), YES))
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
assert result.exit_code == 0
# OK - Let's see what happened
# Bidder
mock_place_bid.assert_called_once()
# Output
expected_prompt = BID_INCREASE_AMOUNT_PROMPT
assert 1 == result.output.count(expected_prompt)

View File

@ -24,8 +24,7 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
StakingEscrowAgent,
WorkLockAgent
StakingEscrowAgent
)
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.registry import InMemoryContractRegistry