Exemple #1
0
def test_sampling_distribution(testerchain, token, deploy_contract):

    #
    # SETUP
    #

    max_allowed_locked_tokens = 5 * 10 ** 8
    _staking_coefficient = 2 * 10 ** 7
    staking_escrow_contract, _ = deploy_contract(
        contract_name=STAKING_ESCROW_CONTRACT_NAME,
        _token=token.address,
        _hoursPerPeriod=1,
        _miningCoefficient=4 * _staking_coefficient,
        _lockedPeriodsCoefficient=4,
        _rewardedPeriods=4,
        _minLockedPeriods=2,
        _minAllowableLockedTokens=100,
        _maxAllowableLockedTokens=max_allowed_locked_tokens,
        _minWorkerPeriods=1,
        _isTestContract=False
    )
    staking_agent = StakingEscrowAgent(registry=None, contract=staking_escrow_contract)

    policy_manager, _ = deploy_contract(
        'PolicyManagerForStakingEscrowMock', token.address, staking_escrow_contract.address
    )
    tx = staking_escrow_contract.functions.setPolicyManager(policy_manager.address).transact()
    testerchain.wait_for_receipt(tx)

    # Travel to the start of the next period to prevent problems with unexpected overflow first period
    testerchain.time_travel(hours=1)

    creator = testerchain.etherbase_account

    # Give Escrow tokens for reward and initialize contract
    tx = token.functions.approve(staking_escrow_contract.address, 10 ** 9).transact({'from': creator})
    testerchain.wait_for_receipt(tx)

    tx = staking_escrow_contract.functions.initialize(10 ** 9).transact({'from': creator})
    testerchain.wait_for_receipt(tx)

    stakers = testerchain.stakers_accounts
    amount = token.functions.balanceOf(creator).call() // len(stakers)

    # Airdrop
    for staker in stakers:
        tx = token.functions.transfer(staker, amount).transact({'from': creator})
        testerchain.wait_for_receipt(tx)

    all_locked_tokens = len(stakers) * amount
    for staker in stakers:
        balance = token.functions.balanceOf(staker).call()
        tx = token.functions.approve(staking_escrow_contract.address, balance).transact({'from': staker})
        testerchain.wait_for_receipt(tx)

        staking_agent.deposit_tokens(amount=balance, lock_periods=10, sender_address=staker, staker_address=staker)
        staking_agent.set_worker(staker_address=staker, worker_address=staker)
        staking_agent.confirm_activity(staker)

    # Wait next period and check all locked tokens
    testerchain.time_travel(hours=1)

    #
    # Test sampling distribution
    #

    ERROR_TOLERANCE = 0.05  # With this tolerance, all sampling ratios should between 5% and 15% (expected is 10%)
    SAMPLES = 1000
    quantity = 3
    counter = Counter()

    sampled, failed = 0, 0
    while sampled < SAMPLES:
        try:
            addresses = set(staking_agent.sample(quantity=quantity, additional_ursulas=1, duration=1))
            addresses.discard(NULL_ADDRESS)
        except staking_agent.NotEnoughStakers:
            failed += 1
            continue
        else:
            sampled += 1
            counter.update(addresses)

    total_times = sum(counter.values())

    expected = amount / all_locked_tokens
    for staker in stakers:
        times = counter[staker]
        sampled_ratio = times / total_times
        abs_error = abs(expected - sampled_ratio)
        assert abs_error < ERROR_TOLERANCE
Exemple #2
0
class Staker(NucypherTokenActor):
    """
    Baseclass for staking-related operations on the blockchain.
    """
    class StakerError(NucypherTokenActor.ActorError):
        pass

    def __init__(self,
                 is_me: bool,
                 economics: TokenEconomics = None,
                 *args,
                 **kwargs) -> None:

        super().__init__(*args, **kwargs)
        self.log = Logger("staker")
        self.stake_tracker = StakeTracker(
            checksum_addresses=[self.checksum_address])
        self.staking_agent = StakingEscrowAgent(blockchain=self.blockchain)
        self.economics = economics or TokenEconomics()
        self.is_me = is_me

    @property
    def stakes(self) -> List[Stake]:
        stakes = self.stake_tracker.stakes(
            checksum_address=self.checksum_address)
        return stakes

    @property
    def is_staking(self) -> bool:
        """Checks if this Staker currently has active stakes / locked tokens."""
        return bool(self.stakes)

    def locked_tokens(self, periods: int = 0) -> NU:
        """Returns the amount of tokens this staker has locked for a given duration in periods."""
        raw_value = self.staking_agent.get_locked_tokens(
            staker_address=self.checksum_address, periods=periods)
        value = NU.from_nunits(raw_value)
        return value

    @property
    def current_stake(self) -> NU:
        """
        The total number of staked tokens, either locked or unlocked in the current period.
        """
        if self.stakes:
            return NU(sum(int(stake.value) for stake in self.stakes), 'NuNit')
        else:
            return NU.ZERO()

    @only_me
    def divide_stake(self,
                     stake_index: int,
                     target_value: NU,
                     additional_periods: int = None,
                     expiration: maya.MayaDT = None) -> tuple:

        # Calculate duration in periods
        if additional_periods and expiration:
            raise ValueError(
                "Pass the number of lock periods or an expiration MayaDT; not both."
            )

        # Select stake to divide from local cache
        try:
            current_stake = self.stakes[stake_index]
        except KeyError:
            if len(self.stakes):
                message = f"Cannot divide stake - No stake exists with index {stake_index}."
            else:
                message = "Cannot divide stake - There are no active stakes."
            raise Stake.StakingError(message)

        # Calculate stake duration in periods
        if expiration:
            additional_periods = datetime_to_period(
                datetime=expiration) - current_stake.end_period
            if additional_periods <= 0:
                raise Stake.StakingError(
                    f"New expiration {expiration} must be at least 1 period from the "
                    f"current stake's end period ({current_stake.end_period})."
                )

        # Do it already!
        modified_stake, new_stake = current_stake.divide(
            target_value=target_value, additional_periods=additional_periods)

        # Update staking cache element
        self.stake_tracker.refresh(checksum_addresses=[self.checksum_address])

        return modified_stake, new_stake

    @only_me
    def initialize_stake(self,
                         amount: NU,
                         lock_periods: int = None,
                         expiration: maya.MayaDT = None,
                         entire_balance: bool = False) -> Stake:
        """Create a new stake."""

        # Duration
        if lock_periods and expiration:
            raise ValueError(
                "Pass the number of lock periods or an expiration MayaDT; not both."
            )
        if expiration:
            lock_periods = calculate_period_duration(future_time=expiration)

        # Value
        if entire_balance and amount:
            raise ValueError("Specify an amount or entire balance, not both")
        if entire_balance:
            amount = self.token_balance
        if not self.token_balance >= amount:
            raise self.StakerError(
                f"Insufficient token balance ({self.token_agent}) "
                f"for new stake initialization of {amount}")

        # Ensure the new stake will not exceed the staking limit
        if (self.current_stake +
                amount) > self.economics.maximum_allowed_locked:
            raise Stake.StakingError(
                f"Cannot divide stake - "
                f"Maximum stake value exceeded with a target value of {amount}."
            )

        # Write to blockchain
        new_stake = Stake.initialize_stake(staker=self,
                                           amount=amount,
                                           lock_periods=lock_periods)

        # Update stake tracker cache element
        self.stake_tracker.refresh(checksum_addresses=[self.checksum_address])
        return new_stake

    #
    # Reward and Collection
    #

    @only_me
    @save_receipt
    def set_worker(self, worker_address: str) -> str:
        # TODO: Set a Worker for this staker, not just in StakingEscrow
        receipt = self.staking_agent.set_worker(
            staker_address=self.checksum_address,
            worker_address=worker_address)
        return receipt

    @only_me
    @save_receipt
    def mint(self) -> Tuple[str, str]:
        """Computes and transfers tokens to the staker's account"""
        receipt = self.staking_agent.mint(staker_address=self.checksum_address)
        return receipt

    def calculate_reward(self) -> int:
        staking_reward = self.staking_agent.calculate_staking_reward(
            staker_address=self.checksum_address)
        return staking_reward

    @only_me
    @save_receipt
    def collect_policy_reward(self,
                              collector_address=None,
                              policy_agent: PolicyAgent = None):
        """Collect rewarded ETH"""
        policy_agent = policy_agent if policy_agent is not None else PolicyAgent(
            blockchain=self.blockchain)

        withdraw_address = collector_address or self.checksum_address
        receipt = policy_agent.collect_policy_reward(
            collector_address=withdraw_address,
            staker_address=self.checksum_address)
        return receipt

    @only_me
    @save_receipt
    def collect_staking_reward(self) -> str:
        """Withdraw tokens rewarded for staking."""
        receipt = self.staking_agent.collect_staking_reward(
            staker_address=self.checksum_address)
        return receipt

    @only_me
    @save_receipt
    def withdraw(self, amount: NU) -> str:
        """Withdraw tokens (assuming they're unlocked)"""
        receipt = self.staking_agent.withdraw(
            staker_address=self.checksum_address, amount=int(amount))
        return receipt
Exemple #3
0
class StakeHolder(BaseConfiguration):

    _NAME = 'stakeholder'
    TRANSACTION_GAS = {}

    class NoFundingAccount(BaseConfiguration.ConfigurationError):
        pass

    class NoStakes(BaseConfiguration.ConfigurationError):
        pass

    def __init__(self,
                 blockchain: BlockchainInterface,
                 sync_now: bool = True,
                 *args,
                 **kwargs):

        super().__init__(*args, **kwargs)

        self.log = Logger(f"stakeholder")

        # Blockchain and Contract connection
        self.blockchain = blockchain
        self.staking_agent = StakingEscrowAgent(blockchain=blockchain)
        self.token_agent = NucypherTokenAgent(blockchain=blockchain)
        self.economics = TokenEconomics()

        # Mode
        self.connect(blockchain=blockchain)

        self.__accounts = list()
        self.__stakers = dict()
        self.__transacting_powers = dict()

        self.__get_accounts()

        if sync_now:
            self.read_onchain_stakes()  # Stakes

    #
    # Configuration
    #

    def static_payload(self) -> dict:
        """Values to read/write from stakeholder JSON configuration files"""
        payload = dict(provider_uri=self.blockchain.provider_uri,
                       blockchain=self.blockchain.to_dict(),
                       accounts=self.__accounts,
                       stakers=self.__serialize_stakers())
        return payload

    @classmethod
    def from_configuration_file(cls,
                                filepath: str = None,
                                sync_now: bool = True,
                                **overrides) -> 'StakeHolder':
        filepath = filepath or cls.default_filepath()
        payload = cls._read_configuration_file(filepath=filepath)

        # Sub config
        blockchain_payload = payload.pop('blockchain')
        blockchain = BlockchainInterface.from_dict(payload=blockchain_payload)
        blockchain.connect(sync_now=sync_now)  # TODO: Leave this here?

        payload.update(dict(blockchain=blockchain))

        payload.update(overrides)
        instance = cls(filepath=filepath, **payload)
        return instance

    @validate_checksum_address
    def attach_transacting_power(self,
                                 checksum_address: str,
                                 password: str = None) -> None:
        try:
            transacting_power = self.__transacting_powers[checksum_address]
        except KeyError:
            transacting_power = TransactingPower(blockchain=self.blockchain,
                                                 password=password,
                                                 account=checksum_address)
            self.__transacting_powers[checksum_address] = transacting_power
        transacting_power.activate(password=password)

    def to_configuration_file(self, *args, **kwargs) -> str:
        filepath = super().to_configuration_file(*args, **kwargs)
        return filepath

    def connect(self, blockchain: BlockchainInterface = None) -> None:
        """Go Online"""
        if not self.staking_agent:
            self.staking_agent = StakingEscrowAgent(blockchain=blockchain)
        if not self.token_agent:
            self.token_agent = NucypherTokenAgent(blockchain=blockchain)
        self.blockchain = self.token_agent.blockchain

    #
    # Account Utilities
    #

    @property
    def accounts(self) -> list:
        return self.__accounts

    def __get_accounts(self) -> None:
        accounts = self.blockchain.client.accounts
        self.__accounts.extend(accounts)

    #
    # Staking Utilities
    #

    def read_onchain_stakes(self, account: str = None) -> None:
        if account:
            accounts = [account]
        else:
            accounts = self.__accounts

        for account in accounts:
            stakes = list(
                self.staking_agent.get_all_stakes(staker_address=account))
            if stakes:
                staker = Staker(is_me=True,
                                checksum_address=account,
                                blockchain=self.blockchain)
                self.__stakers[account] = staker

    @property
    def total_stake(self) -> NU:
        total = sum(staker.locked_tokens() for staker in self.stakers)
        return total

    @property
    def stakers(self) -> List[Staker]:
        return list(self.__stakers.values())

    @property
    def stakes(self) -> list:
        payload = list()
        for staker in self.__stakers.values():
            payload.extend(staker.stakes)
        return payload

    @property
    def account_balances(self) -> dict:
        balances = dict()
        for account in self.__accounts:
            funds = {
                'ETH': self.blockchain.client.get_balance(account),
                'NU': self.token_agent.get_balance(account)
            }
            balances.update({account: funds})
        return balances

    @property
    def staker_balances(self) -> dict:
        balances = dict()
        for staker in self.stakers:
            staker_funds = {
                'ETH': staker.eth_balance,
                'NU': staker.token_balance
            }
            balances[staker.checksum_address] = {
                staker.checksum_address: staker_funds
            }
        return balances

    def __serialize_stakers(self) -> list:
        payload = list()
        for staker in self.stakers:
            payload.append(staker.to_dict())
        return payload

    def get_active_staker(self, address: str) -> Staker:
        self.read_onchain_stakes(account=address)
        try:
            return self.__stakers[address]
        except KeyError:
            raise self.NoStakes(f"{address} does not have any stakes.")

    def create_worker_configuration(self, staking_address: str,
                                    worker_address: str, password: str,
                                    **configuration):
        """Generates a worker JSON configuration file for a given staking address."""
        from nucypher.config.characters import UrsulaConfiguration
        worker_configuration = UrsulaConfiguration.generate(
            checksum_address=staking_address,
            worker_address=worker_address,
            password=password,
            config_root=self.config_root,
            federated_only=False,
            provider_uri=self.blockchain.provider_uri,
            **configuration)
        return worker_configuration

    #
    # Actions
    #

    def set_worker(self,
                   staker_address: str,
                   worker_address: str,
                   password: str = None):
        self.attach_transacting_power(checksum_address=staker_address,
                                      password=password)
        staker = self.get_active_staker(address=staker_address)
        receipt = self.staking_agent.set_worker(
            staker_address=staker.checksum_address,
            worker_address=worker_address)

        self.to_configuration_file(override=True)
        return receipt

    def initialize_stake(
        self,
        amount: NU,
        duration: int,
        checksum_address: str,
        password: str = None,
    ) -> Stake:

        # Existing Staker address
        if not is_checksum_address(checksum_address):
            raise ValueError(
                f"{checksum_address} is an invalid EIP-55 checksum address.")

        try:
            staker = self.__stakers[checksum_address]
        except KeyError:
            if checksum_address not in self.__accounts:
                raise ValueError(
                    f"{checksum_address} is an unknown wallet address.")
            else:
                staker = Staker(is_me=True,
                                checksum_address=checksum_address,
                                blockchain=self.blockchain)

        # Don the transacting power for the staker's account.
        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)
        new_stake = staker.initialize_stake(amount=amount,
                                            lock_periods=duration)

        # Update local cache and save to disk.
        self.__stakers[checksum_address] = staker
        staker.stake_tracker.refresh(
            checksum_addresses=[staker.checksum_address])
        self.to_configuration_file(override=True)

        return new_stake

    def divide_stake(
        self,
        address: str,
        index: int,
        value: NU,
        duration: int,
        password: str = None,
    ):

        staker = self.get_active_staker(address=address)
        if not staker.is_staking:
            raise Stake.StakingError(
                f"{staker.checksum_address} has no published stakes.")

        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)
        result = staker.divide_stake(stake_index=index,
                                     additional_periods=duration,
                                     target_value=value)

        # Save results to disk
        self.to_configuration_file(override=True)
        return result

    def calculate_rewards(self) -> dict:
        rewards = dict()
        for staker in self.stakers:
            reward = staker.calculate_reward()
            rewards[staker.checksum_address] = reward
        return rewards

    def collect_rewards(self,
                        staker_address: str,
                        password: str = None,
                        withdraw_address: str = None,
                        staking: bool = True,
                        policy: bool = True) -> Dict[str, dict]:

        if not staking and not policy:
            raise ValueError(
                "Either staking or policy must be True in order to collect rewards"
            )

        try:
            staker = self.get_active_staker(address=staker_address)
        except self.NoStakes:
            staker = Staker(is_me=True,
                            checksum_address=staker_address,
                            blockchain=self.blockchain)

        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)

        receipts = dict()
        if staking:
            receipts['staking_reward'] = staker.collect_staking_reward()
        if policy:
            receipts['policy_reward'] = staker.collect_policy_reward(
                collector_address=withdraw_address)

        self.to_configuration_file(override=True)
        return receipts