mirror of https://github.com/nucypher/nucypher.git
commit
788992381b
|
@ -20,16 +20,13 @@ import json
|
||||||
import time
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Callable, Union
|
from typing import Callable, Union
|
||||||
from typing import Dict, Iterable, List, Optional, Tuple
|
from typing import Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
import maya
|
import maya
|
||||||
from constant_sorrow.constants import FULL
|
from constant_sorrow.constants import FULL
|
||||||
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
|
|
||||||
from eth_typing import ChecksumAddress
|
from eth_typing import ChecksumAddress
|
||||||
from eth_utils import to_canonical_address
|
|
||||||
from hexbytes import HexBytes
|
from hexbytes import HexBytes
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
from web3.exceptions import ValidationError
|
|
||||||
from web3.types import TxReceipt
|
from web3.types import TxReceipt
|
||||||
|
|
||||||
from nucypher.acumen.nicknames import Nickname
|
from nucypher.acumen.nicknames import Nickname
|
||||||
|
@ -42,14 +39,10 @@ from nucypher.blockchain.eth.agents import (
|
||||||
ContractAgency,
|
ContractAgency,
|
||||||
NucypherTokenAgent,
|
NucypherTokenAgent,
|
||||||
StakingEscrowAgent,
|
StakingEscrowAgent,
|
||||||
WorkLockAgent,
|
|
||||||
PREApplicationAgent
|
PREApplicationAgent
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.constants import (
|
from nucypher.blockchain.eth.constants import (
|
||||||
NULL_ADDRESS,
|
NULL_ADDRESS,
|
||||||
POLICY_MANAGER_CONTRACT_NAME,
|
|
||||||
DISPATCHER_CONTRACT_NAME,
|
|
||||||
STAKING_ESCROW_CONTRACT_NAME,
|
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.decorators import (
|
from nucypher.blockchain.eth.decorators import (
|
||||||
only_me,
|
only_me,
|
||||||
|
@ -61,7 +54,6 @@ from nucypher.blockchain.eth.deployers import (
|
||||||
BaseContractDeployer,
|
BaseContractDeployer,
|
||||||
NucypherTokenDeployer,
|
NucypherTokenDeployer,
|
||||||
StakingEscrowDeployer,
|
StakingEscrowDeployer,
|
||||||
WorklockDeployer,
|
|
||||||
PREApplicationDeployer,
|
PREApplicationDeployer,
|
||||||
SubscriptionManagerDeployer
|
SubscriptionManagerDeployer
|
||||||
)
|
)
|
||||||
|
@ -80,8 +72,7 @@ from nucypher.blockchain.eth.token import (
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.utils import (
|
from nucypher.blockchain.eth.utils import (
|
||||||
calculate_period_duration,
|
calculate_period_duration,
|
||||||
datetime_to_period,
|
datetime_to_period
|
||||||
prettify_eth_amount
|
|
||||||
)
|
)
|
||||||
from nucypher.characters.banners import STAKEHOLDER_BANNER
|
from nucypher.characters.banners import STAKEHOLDER_BANNER
|
||||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||||
|
@ -201,7 +192,7 @@ class ContractAdministrator(BaseActor):
|
||||||
)
|
)
|
||||||
|
|
||||||
aux_deployer_classes = (
|
aux_deployer_classes = (
|
||||||
WorklockDeployer,
|
# Add more deployer classes here
|
||||||
)
|
)
|
||||||
|
|
||||||
# For ownership transfers.
|
# For ownership transfers.
|
||||||
|
@ -1088,273 +1079,3 @@ class StakeHolder:
|
||||||
stake = sum(staking_agent.owned_tokens(staker_address=account) for account in self.signer.accounts)
|
stake = sum(staking_agent.owned_tokens(staker_address=account) for account in self.signer.accounts)
|
||||||
nu_stake = NU.from_units(stake)
|
nu_stake = NU.from_units(stake)
|
||||||
return nu_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
|
|
||||||
|
|
|
@ -26,12 +26,11 @@ from constant_sorrow.constants import ( # type: ignore
|
||||||
TRANSACTION,
|
TRANSACTION,
|
||||||
CONTRACT_ATTRIBUTE
|
CONTRACT_ATTRIBUTE
|
||||||
)
|
)
|
||||||
from eth_typing.encoding import HexStr
|
|
||||||
from eth_typing.evm import ChecksumAddress
|
from eth_typing.evm import ChecksumAddress
|
||||||
from eth_utils.address import to_checksum_address
|
from eth_utils.address import to_checksum_address
|
||||||
from hexbytes.main import HexBytes
|
from hexbytes.main import HexBytes
|
||||||
from web3.contract import Contract, ContractFunction
|
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 (
|
from nucypher.blockchain.eth.constants import (
|
||||||
ADJUDICATOR_CONTRACT_NAME,
|
ADJUDICATOR_CONTRACT_NAME,
|
||||||
|
@ -39,10 +38,8 @@ from nucypher.blockchain.eth.constants import (
|
||||||
ETH_ADDRESS_BYTE_LENGTH,
|
ETH_ADDRESS_BYTE_LENGTH,
|
||||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||||
NULL_ADDRESS,
|
NULL_ADDRESS,
|
||||||
POLICY_MANAGER_CONTRACT_NAME,
|
|
||||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME,
|
SUBSCRIPTION_MANAGER_CONTRACT_NAME,
|
||||||
STAKING_ESCROW_CONTRACT_NAME,
|
STAKING_ESCROW_CONTRACT_NAME,
|
||||||
WORKLOCK_CONTRACT_NAME,
|
|
||||||
PRE_APPLICATION_CONTRACT_NAME
|
PRE_APPLICATION_CONTRACT_NAME
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.decorators import contract_api
|
from nucypher.blockchain.eth.decorators import contract_api
|
||||||
|
@ -62,7 +59,6 @@ from nucypher.types import (
|
||||||
RawSubStakeInfo,
|
RawSubStakeInfo,
|
||||||
Period,
|
Period,
|
||||||
Work,
|
Work,
|
||||||
WorklockParameters,
|
|
||||||
StakerFlags,
|
StakerFlags,
|
||||||
StakerInfo,
|
StakerInfo,
|
||||||
StakingProviderInfo,
|
StakingProviderInfo,
|
||||||
|
@ -1140,284 +1136,6 @@ class PREApplicationAgent(EthereumContractAgent):
|
||||||
return receipt
|
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:
|
class ContractAgency:
|
||||||
"""Where agents live and die."""
|
"""Where agents live and die."""
|
||||||
|
|
||||||
|
|
|
@ -20,26 +20,18 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
DISPATCHER_CONTRACT_NAME = 'Dispatcher'
|
DISPATCHER_CONTRACT_NAME = 'Dispatcher'
|
||||||
STAKING_INTERFACE_ROUTER_CONTRACT_NAME = "StakingInterfaceRouter"
|
|
||||||
NUCYPHER_TOKEN_CONTRACT_NAME = 'NuCypherToken'
|
NUCYPHER_TOKEN_CONTRACT_NAME = 'NuCypherToken'
|
||||||
STAKING_ESCROW_CONTRACT_NAME = 'StakingEscrow'
|
STAKING_ESCROW_CONTRACT_NAME = 'StakingEscrow'
|
||||||
STAKING_ESCROW_STUB_CONTRACT_NAME = 'StakingEscrowStub'
|
STAKING_ESCROW_STUB_CONTRACT_NAME = 'StakingEscrowStub'
|
||||||
POLICY_MANAGER_CONTRACT_NAME = 'PolicyManager'
|
|
||||||
STAKING_INTERFACE_CONTRACT_NAME = 'StakingInterface'
|
|
||||||
ADJUDICATOR_CONTRACT_NAME = 'Adjudicator'
|
ADJUDICATOR_CONTRACT_NAME = 'Adjudicator'
|
||||||
WORKLOCK_CONTRACT_NAME = 'WorkLock'
|
|
||||||
PRE_APPLICATION_CONTRACT_NAME = 'SimplePREApplication' # TODO: Use the real PREApplication
|
PRE_APPLICATION_CONTRACT_NAME = 'SimplePREApplication' # TODO: Use the real PREApplication
|
||||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME = 'SubscriptionManager'
|
SUBSCRIPTION_MANAGER_CONTRACT_NAME = 'SubscriptionManager'
|
||||||
|
|
||||||
NUCYPHER_CONTRACT_NAMES = (
|
NUCYPHER_CONTRACT_NAMES = (
|
||||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||||
STAKING_ESCROW_CONTRACT_NAME,
|
STAKING_ESCROW_CONTRACT_NAME,
|
||||||
POLICY_MANAGER_CONTRACT_NAME,
|
|
||||||
ADJUDICATOR_CONTRACT_NAME,
|
ADJUDICATOR_CONTRACT_NAME,
|
||||||
DISPATCHER_CONTRACT_NAME,
|
DISPATCHER_CONTRACT_NAME,
|
||||||
STAKING_INTERFACE_CONTRACT_NAME,
|
|
||||||
STAKING_INTERFACE_ROUTER_CONTRACT_NAME,
|
|
||||||
WORKLOCK_CONTRACT_NAME,
|
|
||||||
PRE_APPLICATION_CONTRACT_NAME,
|
PRE_APPLICATION_CONTRACT_NAME,
|
||||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME
|
SUBSCRIPTION_MANAGER_CONTRACT_NAME
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,11 +32,9 @@ from web3.contract import Contract
|
||||||
from nucypher.blockchain.economics import Economics
|
from nucypher.blockchain.economics import Economics
|
||||||
from nucypher.blockchain.eth.agents import (
|
from nucypher.blockchain.eth.agents import (
|
||||||
AdjudicatorAgent,
|
AdjudicatorAgent,
|
||||||
ContractAgency,
|
|
||||||
EthereumContractAgent,
|
EthereumContractAgent,
|
||||||
NucypherTokenAgent,
|
NucypherTokenAgent,
|
||||||
StakingEscrowAgent,
|
StakingEscrowAgent,
|
||||||
WorkLockAgent,
|
|
||||||
PREApplicationAgent,
|
PREApplicationAgent,
|
||||||
SubscriptionManagerAgent
|
SubscriptionManagerAgent
|
||||||
)
|
)
|
||||||
|
@ -545,7 +543,10 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
||||||
STUB_MIN_ALLOWED_TOKENS = NU(15_000, 'NU').to_units()
|
STUB_MIN_ALLOWED_TOKENS = NU(15_000, 'NU').to_units()
|
||||||
STUB_MAX_ALLOWED_TOKENS = NU(30_000_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)
|
super().__init__(*args, **kwargs)
|
||||||
self.__dispatcher_contract = None
|
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,
|
self.token_contract = self.blockchain.get_contract_by_name(registry=self.registry,
|
||||||
contract_name=token_contract_name)
|
contract_name=token_contract_name)
|
||||||
self.threshold_staking_address = staking_interface
|
self.threshold_staking_address = staking_interface
|
||||||
self.worklock = self._get_contract(deployer_class=WorklockDeployer)
|
self.worklock_address = worklock_address
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def _deploy_stub(self,
|
def _deploy_stub(self,
|
||||||
transacting_power: TransactingPower,
|
transacting_power: TransactingPower,
|
||||||
|
@ -600,7 +588,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
||||||
**overrides):
|
**overrides):
|
||||||
constructor_kwargs = {}
|
constructor_kwargs = {}
|
||||||
constructor_kwargs.update({"_token": self.token_contract.address,
|
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})
|
"_tStaking": self.threshold_staking_address})
|
||||||
constructor_kwargs.update(overrides)
|
constructor_kwargs.update(overrides)
|
||||||
constructor_kwargs = {k: v for k, v in constructor_kwargs.items() if v is not None}
|
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._contract = contract
|
||||||
self.deployment_receipts = dict(zip(self.deployment_steps, (receipt, )))
|
self.deployment_receipts = dict(zip(self.deployment_steps, (receipt, )))
|
||||||
return self.deployment_receipts
|
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
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -106,15 +106,3 @@ STAKEHOLDER_BANNER = r"""
|
||||||
|
|
||||||
The Holder of Stakes.
|
The Holder of Stakes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
WORKLOCK_BANNER = r"""
|
|
||||||
_ _ _ _ _
|
|
||||||
| | | | | | | | | |
|
|
||||||
| | | | ___ _ __ | | __| | ___ ___ | | __
|
|
||||||
| |/\| | / _ \ | '__|| |/ /| | / _ \ / __|| |/ /
|
|
||||||
\ /\ /| (_) || | | < | |____| (_) || (__ | <
|
|
||||||
\/ \/ \___/ |_| |_|\_\\_____/ \___/ \___||_|\_\
|
|
||||||
|
|
||||||
══ {} ══
|
|
||||||
"""
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ import click
|
||||||
from nucypher.blockchain.eth.actors import Staker
|
from nucypher.blockchain.eth.actors import Staker
|
||||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
|
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
|
||||||
from nucypher.blockchain.eth.constants import (
|
from nucypher.blockchain.eth.constants import (
|
||||||
POLICY_MANAGER_CONTRACT_NAME,
|
|
||||||
STAKING_ESCROW_CONTRACT_NAME
|
STAKING_ESCROW_CONTRACT_NAME
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
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:
|
if event_name:
|
||||||
raise click.BadOptionUsage(option_name='--event-name', message='--event-name requires --contract-name')
|
raise click.BadOptionUsage(option_name='--event-name', message='--event-name requires --contract-name')
|
||||||
# FIXME should we force a contract name to be specified?
|
# 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:
|
else:
|
||||||
contract_names = [contract_name]
|
contract_names = [contract_name]
|
||||||
|
|
||||||
|
|
|
@ -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')
|
|
|
@ -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
|
# Ursula
|
||||||
|
|
|
@ -24,7 +24,6 @@ from nucypher.cli.commands import (
|
||||||
stake,
|
stake,
|
||||||
status,
|
status,
|
||||||
ursula,
|
ursula,
|
||||||
worklock,
|
|
||||||
cloudworkers,
|
cloudworkers,
|
||||||
contacts,
|
contacts,
|
||||||
porter
|
porter
|
||||||
|
@ -75,7 +74,6 @@ ENTRY_POINTS = (
|
||||||
enrico.enrico, # Encryptor of Data
|
enrico.enrico, # Encryptor of Data
|
||||||
ursula.ursula, # Untrusted Re-Encryption Proxy
|
ursula.ursula, # Untrusted Re-Encryption Proxy
|
||||||
stake.stake, # Stake Management
|
stake.stake, # Stake Management
|
||||||
worklock.worklock, # WorkLock
|
|
||||||
|
|
||||||
# Utility Commands
|
# Utility Commands
|
||||||
status.status, # Network Status
|
status.status, # Network Status
|
||||||
|
|
|
@ -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)
|
|
|
@ -32,16 +32,6 @@ PeriodDelta = NewType('PeriodDelta', int)
|
||||||
ContractReturnValue = TypeVar('ContractReturnValue', bound=Union[TxReceipt, Wei, int, str, bool])
|
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):
|
class StakingEscrowParameters(Tuple):
|
||||||
seconds_per_period: int
|
seconds_per_period: int
|
||||||
minting_coefficient: int
|
minting_coefficient: int
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from nucypher.blockchain.eth.events import ContractEventsThrottler
|
from nucypher.blockchain.eth.events import ContractEventsThrottler
|
||||||
from nucypher.blockchain.eth.utils import estimate_block_number_for_period
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from prometheus_client import Gauge, Enum, Counter, Info, Histogram, Summary
|
from prometheus_client import Gauge, Enum, Counter, Info, Histogram, Summary
|
||||||
|
@ -28,7 +27,7 @@ from eth_typing.evm import ChecksumAddress
|
||||||
|
|
||||||
import nucypher
|
import nucypher
|
||||||
from nucypher.blockchain.eth.actors import NucypherTokenActor
|
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
|
PREApplicationAgent
|
||||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
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
|
from typing import Dict, Union, Type
|
||||||
|
|
||||||
ContractAgents = Union[StakingEscrowAgent, WorkLockAgent, PolicyManagerAgent]
|
ContractAgents = Union[StakingEscrowAgent]
|
||||||
|
|
||||||
|
|
||||||
class MetricsCollector(ABC):
|
class MetricsCollector(ABC):
|
||||||
|
@ -241,43 +240,6 @@ class OperatorMetricsCollector(BaseMetricsCollector):
|
||||||
self.metrics["worker_token_balance_gauge"].set(int(nucypher_worker_token_actor.token_balance))
|
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):
|
class EventMetricsCollector(BaseMetricsCollector):
|
||||||
"""General collector for emitted events."""
|
"""General collector for emitted events."""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
@ -401,22 +363,3 @@ class OperatorBondedEventMetricsCollector(EventMetricsCollector):
|
||||||
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
|
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
|
||||||
self.metrics["current_worker_is_me_gauge"].set(
|
self.metrics["current_worker_is_me_gauge"].set(
|
||||||
contract_agent.get_worker_from_staker(self.staker_address) == self.operator_address)
|
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))
|
|
||||||
|
|
|
@ -37,20 +37,19 @@ from nucypher.utilities.prometheus.collector import (
|
||||||
BlockchainMetricsCollector,
|
BlockchainMetricsCollector,
|
||||||
StakerMetricsCollector,
|
StakerMetricsCollector,
|
||||||
OperatorMetricsCollector,
|
OperatorMetricsCollector,
|
||||||
WorkLockMetricsCollector,
|
|
||||||
EventMetricsCollector,
|
EventMetricsCollector,
|
||||||
ReStakeEventMetricsCollector,
|
ReStakeEventMetricsCollector,
|
||||||
WindDownEventMetricsCollector,
|
WindDownEventMetricsCollector,
|
||||||
OperatorBondedEventMetricsCollector,
|
OperatorBondedEventMetricsCollector,
|
||||||
CommitmentMadeEventMetricsCollector,
|
CommitmentMadeEventMetricsCollector
|
||||||
WorkLockRefundEventMetricsCollector)
|
)
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from twisted.internet import reactor, task
|
from twisted.internet import reactor, task
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
|
|
||||||
from nucypher.blockchain.eth.agents import StakingEscrowAgent, PolicyManagerAgent, WorkLockAgent
|
from nucypher.blockchain.eth.agents import StakingEscrowAgent
|
||||||
|
|
||||||
|
|
||||||
class PrometheusMetricsConfig:
|
class PrometheusMetricsConfig:
|
||||||
|
@ -205,24 +204,6 @@ def create_metrics_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[Met
|
||||||
metrics_prefix=metrics_prefix)
|
metrics_prefix=metrics_prefix)
|
||||||
collectors.extend(staking_events_collectors)
|
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
|
return collectors
|
||||||
|
|
||||||
|
|
||||||
|
@ -305,35 +286,3 @@ def create_staking_events_metric_collectors(ursula: 'Ursula', metrics_prefix: st
|
||||||
))
|
))
|
||||||
|
|
||||||
return collectors
|
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
|
|
||||||
|
|
|
@ -30,18 +30,10 @@ CONTRACTS = {
|
||||||
'token': ['NuCypherToken',
|
'token': ['NuCypherToken',
|
||||||
'TokenRecipient'],
|
'TokenRecipient'],
|
||||||
'main': ['StakingEscrow',
|
'main': ['StakingEscrow',
|
||||||
'PolicyManager',
|
'SimplePREApplication', # TODO change to PREApplication when ready
|
||||||
'Adjudicator',
|
'Adjudicator'],
|
||||||
'WorkLock'],
|
|
||||||
'proxy': ['Dispatcher',
|
'proxy': ['Dispatcher',
|
||||||
'Upgradeable'],
|
'Upgradeable'],
|
||||||
'staking': ['StakingInterface',
|
|
||||||
'StakingInterfaceRouter',
|
|
||||||
'AbstractStakingContract',
|
|
||||||
'InitializableStakingContract',
|
|
||||||
'PoolingStakingContract',
|
|
||||||
'PoolingStakingContractV2',
|
|
||||||
'WorkLockPoolingContract']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -18,7 +18,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import pytest
|
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.constants import PRE_APPLICATION_CONTRACT_NAME
|
||||||
from nucypher.blockchain.eth.deployers import PREApplicationDeployer
|
from nucypher.blockchain.eth.deployers import PREApplicationDeployer
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -28,7 +28,6 @@ from nucypher.blockchain.eth.constants import (
|
||||||
ADJUDICATOR_CONTRACT_NAME,
|
ADJUDICATOR_CONTRACT_NAME,
|
||||||
DISPATCHER_CONTRACT_NAME,
|
DISPATCHER_CONTRACT_NAME,
|
||||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||||
POLICY_MANAGER_CONTRACT_NAME,
|
|
||||||
STAKING_ESCROW_CONTRACT_NAME, STAKING_ESCROW_STUB_CONTRACT_NAME
|
STAKING_ESCROW_CONTRACT_NAME, STAKING_ESCROW_STUB_CONTRACT_NAME
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.deployers import (
|
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)
|
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
|
||||||
|
|
||||||
assert staking_agent.owner == testerchain.etherbase_account
|
assert staking_agent.owner == testerchain.etherbase_account
|
||||||
assert policy_agent.owner == testerchain.etherbase_account
|
|
||||||
assert adjudicator_agent.owner == testerchain.etherbase_account
|
assert adjudicator_agent.owner == testerchain.etherbase_account
|
||||||
|
|
||||||
maclane = testerchain.unassigned_accounts[0]
|
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 result.exit_code == 0
|
||||||
|
|
||||||
assert staking_agent.owner == maclane
|
assert staking_agent.owner == maclane
|
||||||
assert policy_agent.owner == testerchain.etherbase_account
|
|
||||||
assert adjudicator_agent.owner == testerchain.etherbase_account
|
assert adjudicator_agent.owner == testerchain.etherbase_account
|
||||||
|
|
||||||
michwill = testerchain.unassigned_accounts[1]
|
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])
|
deployed_contracts.extend([STAKING_ESCROW_STUB_CONTRACT_NAME, DISPATCHER_CONTRACT_NAME])
|
||||||
assert list(new_registry.enrolled_names) == deployed_contracts
|
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
|
# 4. Deploy Adjudicator
|
||||||
command = ('contracts',
|
command = ('contracts',
|
||||||
'--contract-name', ADJUDICATOR_CONTRACT_NAME,
|
'--contract-name', ADJUDICATOR_CONTRACT_NAME,
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
@ -20,22 +20,17 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
from eth_utils import to_wei
|
|
||||||
|
|
||||||
from constant_sorrow import constants
|
from constant_sorrow import constants
|
||||||
from web3.exceptions import ValidationError
|
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 (
|
from nucypher.blockchain.eth.deployers import (
|
||||||
AdjudicatorDeployer,
|
|
||||||
BaseContractDeployer,
|
BaseContractDeployer,
|
||||||
NucypherTokenDeployer,
|
NucypherTokenDeployer,
|
||||||
StakingEscrowDeployer,
|
StakingEscrowDeployer
|
||||||
WorklockDeployer
|
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterfaceFactory
|
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.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.constants import SOLIDITY_SOURCE_ROOT, TEST_SOLIDITY_SOURCE_ROOT
|
||||||
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
||||||
|
|
|
@ -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)
|
|
|
@ -24,8 +24,7 @@ from nucypher.blockchain.eth.agents import (
|
||||||
AdjudicatorAgent,
|
AdjudicatorAgent,
|
||||||
ContractAgency,
|
ContractAgency,
|
||||||
NucypherTokenAgent,
|
NucypherTokenAgent,
|
||||||
StakingEscrowAgent,
|
StakingEscrowAgent
|
||||||
WorkLockAgent
|
|
||||||
)
|
)
|
||||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
||||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||||
|
|
Loading…
Reference in New Issue