Merge pull request #1055 from szotov/economics

Improving TokenEconomics for tests
pull/1304/head
K Prasch 2019-09-06 00:40:42 -07:00 committed by GitHub
commit ee4b183950
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 690 additions and 361 deletions

View File

@ -21,6 +21,8 @@ from typing import Tuple
from math import log
from nucypher.blockchain.eth.agents import ContractAgency, NucypherTokenAgent, StakingEscrowAgent, AdjudicatorAgent
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.token import NU
@ -28,6 +30,153 @@ LOG2 = Decimal(log(2))
class TokenEconomics:
"""
Parameters to use in token and escrow blockchain deployments
from high-level human-understandable parameters.
--------------------------
Formula for staking in one period:
(totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k2
K2 - Staking coefficient
K1 - Locked periods coefficient
if allLockedPeriods > maximum_rewarded_periods then allLockedPeriods = maximum_rewarded_periods
kappa * log(2) / halving_delay === (k1 + allLockedPeriods) / k2
"""
# Token Denomination
__token_decimals = 18
nunits_per_token = 10 ** __token_decimals # Smallest unit designation
# Period Definition
__default_hours_per_period = 24
# Time Constraints
__default_minimum_worker_periods = 2
__default_minimum_locked_periods = 30 # 720 Hours minimum
# Value Constraints
__default_minimum_allowed_locked = NU(15_000, 'NU').to_nunits()
__default_maximum_allowed_locked = NU(4_000_000, 'NU').to_nunits()
# Slashing parameters
HASH_ALGORITHM_KECCAK256 = 0
HASH_ALGORITHM_SHA256 = 1
HASH_ALGORITHM_RIPEMD160 = 2
__default_hash_algorithm = HASH_ALGORITHM_SHA256
__default_base_penalty = 100
__default_penalty_history_coefficient = 10
__default_percentage_penalty_coefficient = 8
__default_reward_coefficient = 2
def __init__(self,
initial_supply: int,
total_supply: int,
staking_coefficient: int,
locked_periods_coefficient: int,
maximum_rewarded_periods: int,
hours_per_period: int = __default_hours_per_period,
minimum_locked_periods: int = __default_minimum_locked_periods,
minimum_allowed_locked: int = __default_minimum_allowed_locked,
maximum_allowed_locked: int = __default_maximum_allowed_locked,
minimum_worker_periods: int = __default_minimum_worker_periods,
hash_algorithm: int = __default_hash_algorithm,
base_penalty: int = __default_base_penalty,
penalty_history_coefficient: int = __default_penalty_history_coefficient,
percentage_penalty_coefficient: int = __default_percentage_penalty_coefficient,
reward_coefficient: int = __default_reward_coefficient
):
"""
:param initial_supply: Tokens at t=0
:param total_supply: Tokens at t=8
:param staking_coefficient: K2
:param locked_periods_coefficient: K1
:param maximum_rewarded_periods: Max periods that will be additionally rewarded
:param hours_per_period: Hours in single period
:param minimum_locked_periods: Min amount of periods during which tokens can be locked
:param minimum_allowed_locked: Min amount of tokens that can be locked
:param maximum_allowed_locked: Max amount of tokens that can be locked
:param minimum_worker_periods: Min amount of periods while a worker can't be changed
:param hash_algorithm: Hashing algorithm
:param base_penalty: Base for the penalty calculation
:param penalty_history_coefficient: Coefficient for calculating the penalty depending on the history
:param percentage_penalty_coefficient: Coefficient for calculating the percentage penalty
:param reward_coefficient: Coefficient for calculating the reward
"""
self.initial_supply = initial_supply
# Remaining / Reward Supply - Escrow Parameter
self.reward_supply = total_supply - initial_supply
self.total_supply = total_supply
self.staking_coefficient = staking_coefficient
self.locked_periods_coefficient = locked_periods_coefficient
self.maximum_rewarded_periods = maximum_rewarded_periods
self.hours_per_period = hours_per_period
self.minimum_locked_periods = minimum_locked_periods
self.minimum_allowed_locked = minimum_allowed_locked
self.maximum_allowed_locked = maximum_allowed_locked
self.minimum_worker_periods = minimum_worker_periods
self.seconds_per_period = hours_per_period * 60 * 60 # Seconds in single period
self.hash_algorithm = hash_algorithm
self.base_penalty = base_penalty
self.penalty_history_coefficient = penalty_history_coefficient
self.percentage_penalty_coefficient = percentage_penalty_coefficient
self.reward_coefficient = reward_coefficient
@property
def erc20_initial_supply(self) -> int:
return int(self.initial_supply)
@property
def erc20_reward_supply(self) -> int:
return int(self.reward_supply)
@property
def erc20_total_supply(self) -> int:
return int(self.total_supply)
@property
def staking_deployment_parameters(self) -> Tuple[int, ...]:
"""Cast coefficient attributes to uint256 compatible type for solidity+EVM"""
deploy_parameters = (
# Period
self.hours_per_period, # Hours in single period
# Coefficients
self.staking_coefficient, # Staking coefficient (k2)
self.locked_periods_coefficient, # Locked periods coefficient (k1)
self.maximum_rewarded_periods, # Max periods that will be additionally rewarded (awarded_periods)
# Constraints
self.minimum_locked_periods, # Min amount of periods during which tokens can be locked
self.minimum_allowed_locked, # Min amount of tokens that can be locked
self.maximum_allowed_locked, # Max amount of tokens that can be locked
self.minimum_worker_periods # Min amount of periods while a worker can't be changed
)
return tuple(map(int, deploy_parameters))
@property
def slashing_deployment_parameters(self) -> Tuple[int, ...]:
"""Cast coefficient attributes to uint256 compatible type for solidity+EVM"""
deployment_parameters = [
self.hash_algorithm,
self.base_penalty,
self.penalty_history_coefficient,
self.percentage_penalty_coefficient,
self.reward_coefficient
]
return tuple(map(int, deployment_parameters))
class StandardTokenEconomics(TokenEconomics):
"""
Calculate parameters to use in token and escrow blockchain deployments
from high-level human-understandable parameters.
@ -40,7 +189,7 @@ class TokenEconomics:
K2 - Staking coefficient
K1 - Locked periods coefficient
if allLockedPeriods > awarded_periods then allLockedPeriods = awarded_periods
if allLockedPeriods > maximum_rewarded_periods then allLockedPeriods = maximum_rewarded_periods
kappa * log(2) / halving_delay === (k1 + allLockedPeriods) / k2
...but also...
@ -60,22 +209,6 @@ class TokenEconomics:
# Decimal
_precision = 28
# Token Denomination
__token_decimals = 18
nunits_per_token = 10 ** __token_decimals # Smallest unit designation
# Period Definition
hours_per_period = 24 # Hours in single period
seconds_per_period = hours_per_period * 60 * 60 # Seconds in single period
# Time Constraints
minimum_worker_periods = 2
minimum_locked_periods = 30 # 720 Hours minimum
# Value Constraints
minimum_allowed_locked = NU(15_000, 'NU').to_nunits()
maximum_allowed_locked = NU(4_000_000, 'NU').to_nunits()
# Supply
__default_initial_supply = NU(int(1_000_000_000), 'NU').to_nunits()
__default_initial_inflation = 1
@ -88,7 +221,8 @@ class TokenEconomics:
initial_inflation: int = __default_initial_inflation,
halving_delay: int = __default_token_halving,
reward_saturation: int = __default_reward_saturation,
small_stake_multiplier: Decimal = __default_small_stake_multiplier
small_stake_multiplier: Decimal = __default_small_stake_multiplier,
**kwargs
):
"""
:param initial_supply: Tokens at t=0
@ -110,9 +244,6 @@ class TokenEconomics:
# ERC20 Token parameter (See Equation 4 in Mining paper)
total_supply = initial_supply * (1 + initial_inflation * halving_delay / LOG2)
# Remaining / Reward Supply - Escrow Parameter
reward_supply = total_supply - initial_supply
# k2 - Escrow parameter
staking_coefficient = 365 ** 2 * reward_saturation * halving_delay / LOG2 / (1 - small_stake_multiplier)
@ -120,78 +251,82 @@ class TokenEconomics:
locked_periods_coefficient = 365 * reward_saturation * small_stake_multiplier / (1 - small_stake_multiplier)
# Awarded periods- Escrow parameter
maximum_locked_periods = reward_saturation * 365
maximum_rewarded_periods = reward_saturation * 365
# Injected
self.initial_supply = initial_supply
self.initial_inflation = initial_inflation
self.token_halving = halving_delay
self.token_saturation = reward_saturation
self.small_stake_multiplier = small_stake_multiplier
# Calculated
self.__total_supply = total_supply
self.reward_supply = reward_supply
self.staking_coefficient = staking_coefficient
self.locked_periods_coefficient = locked_periods_coefficient
self.maximum_locked_periods = maximum_locked_periods
@property
def erc20_initial_supply(self) -> int:
return int(self.initial_supply)
@property
def erc20_reward_supply(self) -> int:
return int(self.reward_supply)
@property
def erc20_total_supply(self) -> int:
return int(self.__total_supply)
@property
def staking_deployment_parameters(self) -> Tuple[int, ...]:
"""Cast coefficient attributes to uint256 compatible type for solidity+EVM"""
deploy_parameters = (
# Period
self.hours_per_period, # Hours in single period
# Coefficients
self.staking_coefficient, # Staking coefficient (k2)
self.locked_periods_coefficient, # Locked periods coefficient (k1)
self.maximum_locked_periods, # Max periods that will be additionally rewarded (awarded_periods)
# Constraints
self.minimum_locked_periods, # Min amount of periods during which tokens can be locked
self.minimum_allowed_locked, # Min amount of tokens that can be locked
self.maximum_allowed_locked, # Max amount of tokens that can be locked
self.minimum_worker_periods # Min amount of periods while a worker can't be changed
super(StandardTokenEconomics, self).__init__(
initial_supply,
total_supply,
staking_coefficient,
locked_periods_coefficient,
maximum_rewarded_periods,
**kwargs
)
return tuple(map(int, deploy_parameters))
def token_supply_at_period(self, period: int) -> int:
if period < 0:
raise ValueError("Period must be a positive integer")
with localcontext() as ctx:
ctx.prec = self._precision
# Eq. 3 of the mining paper
# https://github.com/nucypher/mining-paper/blob/master/mining-paper.pdf
t = Decimal(period)
S_0 = self.erc20_initial_supply
i_0 = 1
I_0 = i_0 * S_0 # in 1/years
T_half = self.token_halving # in years
T_half_in_days = T_half * 365
S_t = S_0 + I_0 * T_half * (1 - 2**(-t / T_half_in_days)) / LOG2
return int(S_t)
def cumulative_rewards_at_period(self, period: int) -> int:
return self.token_supply_at_period(period) - self.erc20_initial_supply
def rewards_during_period(self, period: int) -> int:
return self.cumulative_rewards_at_period(period) - self.cumulative_rewards_at_period(period-1)
class SlashingEconomics:
class TokenEconomicsFactory:
# TODO: Enforce singleton
HASH_ALGORITHM_KECCAK256 = 0
HASH_ALGORITHM_SHA256 = 1
HASH_ALGORITHM_RIPEMD160 = 2
__economics = dict()
hash_algorithm = HASH_ALGORITHM_SHA256
base_penalty = 100
penalty_history_coefficient = 10
percentage_penalty_coefficient = 8
reward_coefficient = 2
@classmethod
def get_economics(cls, registry: BaseContractRegistry) -> TokenEconomics:
registry_id = registry.id
try:
return cls.__economics[registry_id]
except KeyError:
economics = TokenEconomicsFactory.retrieve_from_blockchain(registry=registry)
cls.__economics[registry_id] = economics
return economics
@property
def deployment_parameters(self) -> Tuple[int, ...]:
"""Cast coefficient attributes to uint256 compatible type for solidity+EVM"""
@staticmethod
def retrieve_from_blockchain(registry: BaseContractRegistry) -> TokenEconomics:
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry)
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry)
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=registry)
deployment_parameters = [
self.hash_algorithm,
self.base_penalty,
self.penalty_history_coefficient,
self.percentage_penalty_coefficient,
self.reward_coefficient
]
total_supply = token_agent.contract.functions.totalSupply().call()
reward_supply = staking_agent.contract.functions.getReservedReward().call()
# it's not real initial_supply value because used current reward instead of initial
initial_supply = total_supply - reward_supply
return tuple(map(int, deployment_parameters))
staking_parameters = list(staking_agent.staking_parameters())
seconds_per_period = staking_parameters.pop(0)
staking_parameters.insert(3, seconds_per_period // 60 // 60) # hours_per_period
slashing_parameters = adjudicator_agent.slashing_parameters()
economics_parameters = (initial_supply,
total_supply,
*staking_parameters,
*slashing_parameters)
economics = TokenEconomics(*economics_parameters)
return economics

View File

@ -33,7 +33,7 @@ from eth_tester.exceptions import TransactionFailed
from eth_utils import keccak
from twisted.logger import Logger
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import TokenEconomics, StandardTokenEconomics, TokenEconomicsFactory
from nucypher.blockchain.eth.agents import (
NucypherTokenAgent,
StakingEscrowAgent,
@ -157,7 +157,8 @@ class ContractAdministrator(NucypherTokenActor):
def __init__(self,
registry: BaseContractRegistry,
deployer_address: str = None,
client_password: str = None):
client_password: str = None,
economics: TokenEconomics = None):
"""
Note: super() is not called here to avoid setting the token agent.
TODO: Review this logic ^^ "bare mode".
@ -166,6 +167,7 @@ class ContractAdministrator(NucypherTokenActor):
self.deployer_address = deployer_address
self.checksum_address = self.deployer_address
self.economics = economics or StandardTokenEconomics()
self.registry = registry
self.user_escrow_deployers = dict()
@ -202,11 +204,17 @@ class ContractAdministrator(NucypherTokenActor):
contract_name: str,
gas_limit: int = None,
plaintext_secret: str = None,
progress=None
progress=None,
*args,
**kwargs,
) -> Tuple[dict, ContractDeployer]:
Deployer = self.__get_deployer(contract_name=contract_name)
deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address)
deployer = Deployer(registry=self.registry,
deployer_address=self.deployer_address,
economics=self.economics,
*args,
**kwargs)
if Deployer._upgradeable:
if not plaintext_secret:
raise ValueError("Upgrade plaintext_secret must be passed to deploy an upgradeable contract.")
@ -395,10 +403,7 @@ class Staker(NucypherTokenActor):
class InsufficientTokens(StakerError):
pass
def __init__(self,
is_me: bool,
economics: TokenEconomics = None,
*args, **kwargs) -> None:
def __init__(self, is_me: bool, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.log = Logger("staker")
@ -409,7 +414,7 @@ class Staker(NucypherTokenActor):
# Blockchain
self.policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=self.registry)
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry)
self.economics = economics or TokenEconomics()
self.economics = TokenEconomicsFactory.get_economics(registry=self.registry)
self.stakes = StakeList(registry=self.registry, checksum_address=self.checksum_address)
def to_dict(self) -> dict:
@ -651,7 +656,6 @@ class BlockchainPolicyAuthor(NucypherTokenActor):
def __init__(self,
checksum_address: str,
economics: TokenEconomics = None,
rate: int = None,
duration_periods: int = None,
first_period_reward: int = None,
@ -668,7 +672,7 @@ class BlockchainPolicyAuthor(NucypherTokenActor):
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry)
self.policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=self.registry)
self.economics = economics or TokenEconomics()
self.economics = TokenEconomicsFactory.get_economics(registry=self.registry)
self.rate = rate
self.duration_periods = duration_periods
self.first_period_reward = first_period_reward
@ -700,7 +704,8 @@ class BlockchainPolicyAuthor(NucypherTokenActor):
# Calculate duration in periods and expiration datetime
if expiration:
duration_periods = calculate_period_duration(future_time=expiration)
duration_periods = calculate_period_duration(future_time=expiration,
seconds_per_period=self.economics.seconds_per_period)
else:
duration_periods = duration_periods or self.duration_periods
expiration = datetime_at_period(self.staking_agent.get_current_period() + duration_periods)

View File

@ -621,3 +621,17 @@ class AdjudicatorAgent(EthereumContractAgent):
def penalty_history(self, staker_address: str) -> int:
return self.contract.functions.penaltyHistory(staker_address).call()
def slashing_parameters(self) -> Tuple:
parameter_signatures = (
'hashAlgorithm', # Hashing algorithm
'basePenalty', # Base for the penalty calculation
'penaltyHistoryCoefficient', # Coefficient for calculating the penalty depending on the history
'percentagePenaltyCoefficient', # Coefficient for calculating the percentage penalty
'rewardCoefficient', # Coefficient for calculating the reward
)
def _call_function_by_name(name: str):
return getattr(self.contract.functions, name)().call()
staking_parameters = tuple(map(_call_function_by_name, parameter_signatures))
return staking_parameters

View File

@ -22,7 +22,7 @@ from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_CONFIGU
from eth_utils import is_checksum_address
from web3.contract import Contract
from nucypher.blockchain.economics import TokenEconomics, SlashingEconomics
from nucypher.blockchain.economics import TokenEconomics, StandardTokenEconomics
from nucypher.blockchain.eth.agents import (
EthereumContractAgent,
StakingEscrowAgent,
@ -67,10 +67,6 @@ class ContractDeployer:
if not isinstance(self.blockchain, BlockchainDeployerInterface):
raise ValueError("No deployer interface connection available.")
if not economics:
economics = TokenEconomics()
self.__economics = economics
#
# Defaults
#
@ -80,6 +76,7 @@ class ContractDeployer:
self.__proxy_contract = NotImplemented
self.__deployer_address = deployer_address
self.__ready_to_deploy = False
self.__economics = economics or StandardTokenEconomics()
@property
def economics(self) -> TokenEconomics:
@ -272,11 +269,8 @@ class StakingEscrowDeployer(ContractDeployer):
_upgradeable = True
_proxy_deployer = DispatcherDeployer
def __init__(self, economics: TokenEconomics = None, *args, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not economics:
economics = TokenEconomics()
self.__economics = economics
self.__dispatcher_contract = None
token_contract_name = NucypherTokenDeployer.contract_name
@ -289,7 +283,7 @@ class StakingEscrowDeployer(ContractDeployer):
raise RuntimeError("PolicyManager contract is not initialized.")
def _deploy_essential(self, gas_limit: int = None):
escrow_constructor_args = (self.token_contract.address, *self.__economics.staking_deployment_parameters)
escrow_constructor_args = (self.token_contract.address, *self.economics.staking_deployment_parameters)
the_escrow_contract, deploy_receipt = self.blockchain.deploy_contract(
self.deployer_address,
self.registry,
@ -352,7 +346,7 @@ class StakingEscrowDeployer(ContractDeployer):
# 3 - Transfer the reward supply tokens to StakingEscrow #
reward_function = self.token_contract.functions.transfer(the_escrow_contract.address,
self.__economics.erc20_reward_supply)
self.economics.erc20_reward_supply)
# TODO: Confirmations / Successful Transaction Indicator / Events ??
reward_receipt = self.blockchain.send_transaction(contract_function=reward_function,
@ -430,7 +424,6 @@ class StakingEscrowDeployer(ContractDeployer):
return rollback_receipt
class PolicyManagerDeployer(ContractDeployer):
"""
Depends on StakingEscrow and NucypherTokenAgent
@ -446,11 +439,6 @@ class PolicyManagerDeployer(ContractDeployer):
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,
name=token_contract_name)
proxy_name = StakingEscrowDeployer._proxy_deployer.contract_name
staking_contract_name = StakingEscrowDeployer.contract_name
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
@ -812,10 +800,8 @@ class AdjudicatorDeployer(ContractDeployer):
_upgradeable = True
_proxy_deployer = DispatcherDeployer
def __init__(self, economics: SlashingEconomics = None, *args, **kwargs):
if not economics:
economics = SlashingEconomics()
super().__init__(*args, economics=economics, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
staking_contract_name = StakingEscrowDeployer.contract_name
proxy_name = StakingEscrowDeployer._proxy_deployer.contract_name
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
@ -824,7 +810,7 @@ class AdjudicatorDeployer(ContractDeployer):
def _deploy_essential(self, gas_limit: int = None):
constructor_args = (self.staking_contract.address,
*self.economics.deployment_parameters)
*self.economics.slashing_deployment_parameters)
adjudicator_contract, deploy_receipt = self.blockchain.deploy_contract(self.deployer_address,
self.registry,
self.contract_name,
@ -840,8 +826,8 @@ class AdjudicatorDeployer(ContractDeployer):
progress.update(1)
proxy_deployer = self._proxy_deployer(registry=self.registry,
target_contract=adjudicator_contract,
deployer_address=self.deployer_address)
target_contract=adjudicator_contract,
deployer_address=self.deployer_address)
proxy_deploy_receipts = proxy_deployer.deploy(secret_hash=secret_hash, gas_limit=gas_limit)
proxy_deploy_receipt = proxy_deploy_receipts[proxy_deployer.deployment_steps[0]]
@ -888,9 +874,9 @@ class AdjudicatorDeployer(ContractDeployer):
use_proxy_address=False)
proxy_deployer = self._proxy_deployer(registry=self.registry,
target_contract=existing_bare_contract,
deployer_address=self.deployer_address,
bare=True)
target_contract=existing_bare_contract,
deployer_address=self.deployer_address,
bare=True)
adjudicator_contract, deploy_receipt = self._deploy_essential(gas_limit=gas_limit)

View File

@ -121,21 +121,15 @@ contract Issuer is Upgradeable {
Math.min(currentSupply1, currentSupply2) :
Math.max(currentSupply1, currentSupply2);
//totalSupply * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k2) -
//currentSupply * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k2)
//(totalSupply - currentSupply) * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k2)
uint256 allLockedPeriods = uint256(_allLockedPeriods <= rewardedPeriods ?
_allLockedPeriods : rewardedPeriods)
.add(lockedPeriodsCoefficient);
uint256 denominator = _totalLockedValue.mul(miningCoefficient);
amount =
totalSupply
.mul(_lockedValue)
.mul(allLockedPeriods)
.div(denominator).sub(
currentSupply
.mul(_lockedValue)
.mul(allLockedPeriods)
.div(denominator));
amount = totalSupply.sub(currentSupply)
.mul(_lockedValue)
.mul(allLockedPeriods)
.div(denominator);
// rounding the last reward
if (amount == 0) {
amount = 1;

View File

@ -1,3 +1,20 @@
"""
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 _pydecimal import Decimal
from collections import UserList
from typing import Union, Tuple, List
@ -151,7 +168,7 @@ class Stake:
first_locked_period: int,
final_locked_period: int,
index: int,
economics=None,
economics,
validate_now: bool = True):
self.log = Logger(f'stake-{checksum_address}-{index}')
@ -174,19 +191,20 @@ class Stake:
# and no confirmation can be performed in this period for this stake.
self.final_locked_period = final_locked_period
# Time
self.start_datetime = datetime_at_period(period=first_locked_period)
self.unlock_datetime = datetime_at_period(period=final_locked_period + 1)
# Blockchain
self.staking_agent = staking_agent
# Economics
from nucypher.blockchain.economics import TokenEconomics
self.economics = economics or TokenEconomics()
self.economics = economics
self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit')
self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit')
# Time
self.start_datetime = datetime_at_period(period=first_locked_period,
seconds_per_period=self.economics.seconds_per_period)
self.unlock_datetime = datetime_at_period(period=final_locked_period + 1,
seconds_per_period=self.economics.seconds_per_period)
if validate_now:
self.validate_duration()
@ -219,6 +237,7 @@ class Stake:
checksum_address: str,
index: int,
stake_info: Tuple[int, int, int],
economics,
*args, **kwargs
) -> 'Stake':
@ -230,6 +249,7 @@ class Stake:
first_locked_period=first_locked_period,
final_locked_period=final_locked_period,
value=NU(value, 'NuNit'),
economics=economics,
*args, **kwargs)
instance.worker_address = instance.staking_agent.get_worker_from_staker(staker_address=checksum_address)
@ -378,7 +398,8 @@ class Stake:
first_locked_period=self.first_locked_period,
final_locked_period=self.final_locked_period,
value=remaining_stake_value,
staking_agent=self.staking_agent)
staking_agent=self.staking_agent,
economics=self.economics)
# New Derived Stake
end_period = self.final_locked_period + additional_periods
@ -387,7 +408,8 @@ class Stake:
final_locked_period=end_period,
value=target_value,
index=NEW_STAKE,
staking_agent=self.staking_agent)
staking_agent=self.staking_agent,
economics=self.economics)
#
# Validate
@ -425,7 +447,8 @@ class Stake:
final_locked_period=final_locked_period,
value=amount,
index=NEW_STAKE,
staking_agent=staker.staking_agent)
staking_agent=staker.staking_agent,
economics=staker.economics)
# Validate
stake.validate_value()
@ -550,6 +573,8 @@ class StakeList(UserList):
super().__init__(*args, **kwargs)
self.log = Logger('stake-tracker')
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry)
from nucypher.blockchain.economics import TokenEconomicsFactory
self.economics = TokenEconomicsFactory.get_economics(registry=registry)
self.__terminal_period = NOT_STAKING
@ -590,7 +615,8 @@ class StakeList(UserList):
onchain_stake = Stake.from_stake_info(checksum_address=self.checksum_address,
stake_info=stake_info,
staking_agent=self.staking_agent,
index=onchain_index)
index=onchain_index,
economics=self.economics)
# rack the latest terminal period
if onchain_stake.final_locked_period > terminal_period:

View File

@ -20,23 +20,22 @@ from constant_sorrow.constants import UNKNOWN_DEVELOPMENT_CHAIN_ID
from eth_utils import is_address, to_checksum_address, is_hex
def epoch_to_period(epoch: int) -> int:
from nucypher.blockchain.economics import TokenEconomics
period = epoch // int(TokenEconomics.seconds_per_period)
def epoch_to_period(epoch: int, seconds_per_period: int) -> int:
period = epoch // seconds_per_period
return period
def datetime_to_period(datetime: maya.MayaDT) -> int:
def datetime_to_period(datetime: maya.MayaDT, seconds_per_period: int) -> int:
"""Converts a MayaDT instance to a period number."""
future_period = epoch_to_period(epoch=datetime.epoch)
future_period = epoch_to_period(epoch=datetime.epoch, seconds_per_period=seconds_per_period)
return int(future_period)
def datetime_at_period(period: int) -> maya.MayaDT:
def datetime_at_period(period: int, seconds_per_period: int) -> maya.MayaDT:
"""Returns the datetime object at a given period, future, or past."""
now = maya.now()
current_period = datetime_to_period(datetime=now)
current_period = datetime_to_period(datetime=now, seconds_per_period=seconds_per_period)
delta_periods = period - current_period
# +
@ -50,10 +49,10 @@ def datetime_at_period(period: int) -> maya.MayaDT:
return target_period
def calculate_period_duration(future_time: maya.MayaDT) -> int:
def calculate_period_duration(future_time: maya.MayaDT, seconds_per_period: int) -> int:
"""Takes a future MayaDT instance and calculates the duration from now, returning in periods"""
future_period = datetime_to_period(datetime=future_time)
current_period = datetime_to_period(datetime=maya.now())
future_period = datetime_to_period(datetime=future_time, seconds_per_period=seconds_per_period)
current_period = datetime_to_period(datetime=maya.now(), seconds_per_period=seconds_per_period)
periods = future_period - current_period
return periods

View File

@ -20,7 +20,7 @@ from twisted.logger import Logger
from hendrix.deploy.base import HendrixDeploy
from hendrix.experience import hey_joe
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import TokenEconomicsFactory
from nucypher.blockchain.eth.actors import NucypherTokenActor
from nucypher.blockchain.eth.agents import NucypherTokenAgent, ContractAgency
from nucypher.blockchain.eth.interfaces import BlockchainInterface
@ -164,7 +164,6 @@ class Felix(Character, NucypherTokenActor):
rest_port: int,
client_password: str = None,
crash_on_error: bool = False,
economics: TokenEconomics = None,
distribute_ether: bool = True,
registry: BaseContractRegistry = None,
*args, **kwargs):
@ -205,12 +204,9 @@ class Felix(Character, NucypherTokenActor):
self._distribution_task.clock = self._CLOCK
self.start_time = NOT_RUNNING
if not economics:
economics = TokenEconomics()
self.economics = economics
self.MAXIMUM_DISBURSEMENT = economics.maximum_allowed_locked
self.INITIAL_DISBURSEMENT = economics.minimum_allowed_locked * 3
self.economics = TokenEconomicsFactory.get_economics(registry=registry)
self.MAXIMUM_DISBURSEMENT = self.economics.maximum_allowed_locked
self.INITIAL_DISBURSEMENT = self.economics.minimum_allowed_locked * 3
# Optionally send ether with each token transaction
self.distribute_ether = distribute_ether

View File

@ -42,7 +42,6 @@ from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
import nucypher
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Worker, Staker
from nucypher.blockchain.eth.agents import StakingEscrowAgent, NucypherTokenAgent, ContractAgency
from nucypher.blockchain.eth.decorators import validate_checksum_address

View File

@ -21,19 +21,14 @@ from web3 import Web3
from nucypher.characters.lawful import StakeHolder
from nucypher.blockchain.eth.interfaces import BlockchainInterface, BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import datetime_at_period
from nucypher.characters.banners import NU_BANNER
from nucypher.cli import painting, actions
from nucypher.cli.actions import confirm_staged_stake, get_client_password, select_stake, select_client_account
from nucypher.cli.config import nucypher_click_config
from nucypher.cli.painting import paint_receipt_summary
from nucypher.cli.types import (
EIP55_CHECKSUM_ADDRESS,
STAKE_VALUE,
STAKE_DURATION,
STAKE_EXTENSION,
EXISTING_READABLE_FILE
)
from nucypher.config.characters import StakeHolderConfiguration
@ -142,6 +137,12 @@ def stake(click_config,
STAKEHOLDER = stakeholder_config.produce(initial_address=staking_address)
blockchain = BlockchainInterfaceFactory.get_interface(provider_uri=provider_uri) # Eager connection
economics = STAKEHOLDER.economics
min_locked = economics.minimum_allowed_locked
stake_value_range = click.FloatRange(min=NU.from_nunits(min_locked).to_tokens(), clamp=False)
stake_duration_range = click.IntRange(min=economics.minimum_locked_periods, clamp=False)
stake_extension_range = click.IntRange(min=1, max=economics.maximum_allowed_locked, clamp=False)
#
# Eager Actions
@ -179,10 +180,10 @@ def stake(click_config,
# TODO: Double-check dates
current_period = STAKEHOLDER.staking_agent.get_current_period()
bonded_date = datetime_at_period(period=current_period)
bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period)
min_worker_periods = STAKEHOLDER.staking_agent.staking_parameters()[7]
release_period = current_period + min_worker_periods
release_date = datetime_at_period(period=release_period)
release_date = datetime_at_period(period=release_period, seconds_per_period=economics.seconds_per_period)
emitter.echo(f"\nWorker {worker_address} successfully bonded to staker {staking_address}", color='green')
paint_receipt_summary(emitter=emitter,
@ -217,7 +218,7 @@ def stake(click_config,
# TODO: Double-check dates
current_period = STAKEHOLDER.staking_agent.get_current_period()
bonded_date = datetime_at_period(period=current_period)
bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period)
emitter.echo(f"Successfully detached worker {worker_address} from staker {staking_address}", color='green')
paint_receipt_summary(emitter=emitter,
@ -249,13 +250,14 @@ def stake(click_config,
#
if not value:
min_locked = STAKEHOLDER.economics.minimum_allowed_locked
value = click.prompt(f"Enter stake value in NU", type=STAKE_VALUE, default=NU.from_nunits(min_locked).to_tokens())
value = click.prompt(f"Enter stake value in NU",
type=stake_value_range,
default=NU.from_nunits(min_locked).to_tokens())
value = NU.from_tokens(value)
if not lock_periods:
prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)"
lock_periods = click.prompt(prompt, type=STAKE_DURATION)
lock_periods = click.prompt(prompt, type=stake_duration_range)
start_period = STAKEHOLDER.staking_agent.get_current_period()
end_period = start_period + lock_periods
@ -302,12 +304,12 @@ def stake(click_config,
# Value
if not value:
value = click.prompt(f"Enter target value (must be less than or equal to {str(current_stake.value)})",
type=STAKE_VALUE)
type=stake_value_range)
value = NU(value, 'NU')
# Duration
if not lock_periods:
extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION)
extension = click.prompt("Enter number of periods to extend", type=stake_extension_range)
else:
extension = lock_periods

View File

@ -18,9 +18,9 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
from ipaddress import ip_address
import click
from eth_utils import is_checksum_address, to_checksum_address
from eth_utils import to_checksum_address
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.token import NU
@ -43,14 +43,8 @@ class IPv4Address(click.ParamType):
return value
token_economics = TokenEconomics()
WEI = click.IntRange(min=1, clamp=False) # TODO: Better validation for ether and wei values?
# Staking
STAKE_DURATION = click.IntRange(min=token_economics.minimum_locked_periods, clamp=False)
STAKE_EXTENSION = click.IntRange(min=1, max=token_economics.maximum_allowed_locked, clamp=False)
STAKE_VALUE = click.FloatRange(min=NU.from_nunits(token_economics.minimum_allowed_locked).to_tokens(), clamp=False)
# Filesystem
EXISTING_WRITABLE_DIRECTORY = click.Path(exists=True, dir_okay=True, file_okay=False, writable=True)
EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)

View File

@ -24,7 +24,7 @@ from pytest_ethereum.deployer import Deployer
from twisted.logger import Logger
from web3 import Web3
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import TokenEconomics, StandardTokenEconomics
from nucypher.blockchain.eth.actors import ContractAdministrator
from nucypher.blockchain.eth.agents import EthereumContractAgent
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterfaceFactory
@ -87,6 +87,8 @@ class TesterBlockchain(BlockchainDeployerInterface):
_FIRST_URSULA = _FIRST_STAKER + NUMBER_OF_STAKERS_IN_BLOCKCHAIN_TESTS
_ursulas_range = range(NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS)
_default_token_economics = StandardTokenEconomics()
def __init__(self,
test_accounts=None,
poa=True,
@ -181,8 +183,8 @@ class TesterBlockchain(BlockchainDeployerInterface):
raise ValueError("Specify hours, seconds, or periods, not a combination")
if periods:
duration = (TokenEconomics.hours_per_period * periods) * (60 * 60)
base = TokenEconomics.hours_per_period * 60 * 60
duration = self._default_token_economics.seconds_per_period * periods
base = self._default_token_economics.seconds_per_period
elif hours:
duration = hours * (60*60)
base = 60 * 60
@ -200,11 +202,13 @@ class TesterBlockchain(BlockchainDeployerInterface):
delta = maya.timedelta(seconds=end_timestamp-now)
self.log.info(f"Time traveled {delta} "
f"| period {epoch_to_period(epoch=end_timestamp)} "
f"| period {epoch_to_period(epoch=end_timestamp, seconds_per_period=self._default_token_economics.seconds_per_period)} "
f"| epoch {end_timestamp}")
@classmethod
def bootstrap_network(cls) -> 'TesterBlockchain':
def bootstrap_network(cls,
economics: TokenEconomics = None
) -> Tuple['TesterBlockchain', 'InMemoryContractRegistry']:
"""For use with metric testing scripts"""
registry = InMemoryContractRegistry()
@ -216,7 +220,9 @@ class TesterBlockchain(BlockchainDeployerInterface):
testerchain.transacting_power = power
origin = testerchain.client.etherbase
deployer = ContractAdministrator(deployer_address=origin, registry=registry)
deployer = ContractAdministrator(deployer_address=origin,
registry=registry,
economics=economics or cls._default_token_economics)
secrets = dict()
for deployer_class in deployer.upgradeable_deployer_classes:
secrets[deployer_class.contract_name] = INSECURE_DEVELOPMENT_PASSWORD

View File

@ -20,32 +20,61 @@ import pytest
from eth_tester.exceptions import TransactionFailed
from web3.contract import Contract
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.eth.token import NU
SECRET_LENGTH = 32
TOTAL_SUPPLY = 2 * 10 ** 40
@pytest.fixture()
def token(testerchain, deploy_contract):
# Create an ERC20 token
token, _ = deploy_contract('NuCypherToken', 2 * 10 ** 40)
token, _ = deploy_contract('NuCypherToken', _totalSupply=TOTAL_SUPPLY)
return token
@pytest.mark.slow
def test_issuer(testerchain, token, deploy_contract):
economics = TokenEconomics(initial_supply=10 ** 30,
total_supply=TOTAL_SUPPLY,
staking_coefficient=10 ** 43,
locked_periods_coefficient=10 ** 4,
maximum_rewarded_periods=10 ** 4,
hours_per_period=1)
def calculate_reward(locked, total_locked, locked_periods):
return economics.erc20_reward_supply * locked * \
(locked_periods + economics.locked_periods_coefficient) // \
(total_locked * economics.staking_coefficient)
creator = testerchain.client.accounts[0]
ursula = testerchain.client.accounts[1]
# Only token contract is allowed in Issuer constructor
with pytest.raises((TransactionFailed, ValueError)):
deploy_contract('IssuerMock', ursula, 1, 10 ** 43, 10 ** 4, 10 ** 4)
deploy_contract(
contract_name='IssuerMock',
_token=ursula,
_hoursPerPeriod=economics.hours_per_period,
_miningCoefficient=economics.staking_coefficient,
_lockedPeriodsCoefficient=economics.locked_periods_coefficient,
_rewardedPeriods=economics.maximum_rewarded_periods
)
# Creator deploys the issuer
issuer, _ = deploy_contract('IssuerMock', token.address, 1, 10 ** 43, 10 ** 4, 10 ** 4)
issuer, _ = deploy_contract(
contract_name='IssuerMock',
_token=token.address,
_hoursPerPeriod=economics.hours_per_period,
_miningCoefficient=economics.staking_coefficient,
_lockedPeriodsCoefficient=economics.locked_periods_coefficient,
_rewardedPeriods=economics.maximum_rewarded_periods
)
events = issuer.events.Initialized.createFilter(fromBlock='latest')
# Give staker tokens for reward and initialize contract
reserved_reward = 2 * 10 ** 40 - 10 ** 30
tx = token.functions.transfer(issuer.address, reserved_reward).transact({'from': creator})
tx = token.functions.transfer(issuer.address, economics.erc20_reward_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Only owner can initialize
@ -57,7 +86,7 @@ def test_issuer(testerchain, token, deploy_contract):
events = events.get_all_entries()
assert 1 == len(events)
assert reserved_reward == events[0]['args']['reservedReward']
assert economics.erc20_reward_supply == events[0]['args']['reservedReward']
balance = token.functions.balanceOf(issuer.address).call()
# Can't initialize second time
@ -68,26 +97,30 @@ def test_issuer(testerchain, token, deploy_contract):
# Check result of minting tokens
tx = issuer.functions.testMint(0, 1000, 2000, 0).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
assert 10 == token.functions.balanceOf(ursula).call()
assert balance - 10 == token.functions.balanceOf(issuer.address).call()
reward = calculate_reward(1000, 2000, 0)
assert reward == token.functions.balanceOf(ursula).call()
assert balance - reward == token.functions.balanceOf(issuer.address).call()
# The result must be more because of a different proportion of lockedValue and totalLockedValue
tx = issuer.functions.testMint(0, 500, 500, 0).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
assert 30 == token.functions.balanceOf(ursula).call()
assert balance - 30 == token.functions.balanceOf(issuer.address).call()
reward += calculate_reward(500, 500, 0)
assert reward == token.functions.balanceOf(ursula).call()
assert balance - reward == token.functions.balanceOf(issuer.address).call()
# The result must be more because of bigger value of allLockedPeriods
tx = issuer.functions.testMint(0, 500, 500, 10 ** 4).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
assert 70 == token.functions.balanceOf(ursula).call()
assert balance - 70 == token.functions.balanceOf(issuer.address).call()
reward += calculate_reward(500, 500, 10 ** 4)
assert reward == token.functions.balanceOf(ursula).call()
assert balance - reward == token.functions.balanceOf(issuer.address).call()
# The result is the same because allLockedPeriods more then specified coefficient _rewardedPeriods
tx = issuer.functions.testMint(0, 500, 500, 2 * 10 ** 4).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
assert 110 == token.functions.balanceOf(ursula).call()
assert balance - 110 == token.functions.balanceOf(issuer.address).call()
reward += calculate_reward(500, 500, 10 ** 4)
assert reward == token.functions.balanceOf(ursula).call()
assert balance - reward == token.functions.balanceOf(issuer.address).call()
@pytest.mark.slow
@ -97,14 +130,28 @@ def test_inflation_rate(testerchain, token, deploy_contract):
During one period inflation rate must be the same
"""
economics = TokenEconomics(initial_supply=10 ** 30,
total_supply=TOTAL_SUPPLY,
staking_coefficient=2 * 10 ** 19,
locked_periods_coefficient=1,
maximum_rewarded_periods=1,
hours_per_period=1)
creator = testerchain.client.accounts[0]
ursula = testerchain.client.accounts[1]
# Creator deploys the contract
issuer, _ = deploy_contract('IssuerMock', token.address, 1, 2 * 10 ** 19, 1, 1)
issuer, _ = deploy_contract(
contract_name='IssuerMock',
_token=token.address,
_hoursPerPeriod=economics.hours_per_period,
_miningCoefficient=economics.staking_coefficient,
_lockedPeriodsCoefficient=economics.locked_periods_coefficient,
_rewardedPeriods=economics.maximum_rewarded_periods
)
# Give staker tokens for reward and initialize contract
tx = token.functions.transfer(issuer.address, 2 * 10 ** 40 - 10 ** 30).transact({'from': creator})
tx = token.functions.transfer(issuer.address, economics.erc20_reward_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = issuer.functions.initialize().transact({'from': creator})
testerchain.wait_for_receipt(tx)
@ -166,11 +213,25 @@ def test_upgrading(testerchain, token, deploy_contract):
secret2_hash = testerchain.w3.keccak(secret2)
# Deploy contract
contract_library_v1, _ = deploy_contract('Issuer', token.address, 1, 1, 1, 1)
contract_library_v1, _ = deploy_contract(
contract_name='Issuer',
_token=token.address,
_hoursPerPeriod=1,
_miningCoefficient=1,
_lockedPeriodsCoefficient=1,
_rewardedPeriods=1
)
dispatcher, _ = deploy_contract('Dispatcher', contract_library_v1.address, secret_hash)
# Deploy second version of the contract
contract_library_v2, _ = deploy_contract('IssuerV2Mock', token.address, 2, 2, 2, 2)
contract_library_v2, _ = deploy_contract(
contract_name='IssuerV2Mock',
_token=token.address,
_hoursPerPeriod=2,
_miningCoefficient=2,
_lockedPeriodsCoefficient=2,
_rewardedPeriods=2
)
contract = testerchain.client.get_contract(
abi=contract_library_v2.abi,
address=dispatcher.address,
@ -208,7 +269,14 @@ def test_upgrading(testerchain, token, deploy_contract):
assert 3 == contract.functions.valueToCheck().call()
# Can't upgrade to the previous version or to the bad version
contract_library_bad, _ = deploy_contract('IssuerBad', token.address, 2, 2, 2, 2)
contract_library_bad, _ = deploy_contract(
contract_name='IssuerBad',
_token=token.address,
_hoursPerPeriod=2,
_miningCoefficient=2,
_lockedPeriodsCoefficient=2,
_rewardedPeriods=2
)
with pytest.raises((TransactionFailed, ValueError)):
tx = dispatcher.functions.upgrade(contract_library_v1.address, secret2, secret_hash)\
.transact({'from': creator})

View File

@ -0,0 +1,73 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from nucypher.crypto.powers import TransactingPower
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
# Experimental max error
MAX_ERROR = 0.0004751
MAX_PERIODS = 100
@pytest.mark.slow
def test_reward(testerchain, agency, token_economics):
testerchain.time_travel(hours=1)
token_agent, staking_agent, _policy_agent = agency
origin = testerchain.etherbase_account
ursula = testerchain.ursula_account(0)
# Prepare one staker
_txhash = token_agent.transfer(amount=token_economics.minimum_allowed_locked,
target_address=ursula,
sender_address=origin)
testerchain.transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD,
account=ursula)
testerchain.transacting_power.activate()
_txhash = token_agent.approve_transfer(amount=token_economics.minimum_allowed_locked,
target_address=staking_agent.contract_address,
sender_address=ursula)
_txhash = staking_agent.deposit_tokens(amount=token_economics.minimum_allowed_locked,
lock_periods=100 * token_economics.maximum_rewarded_periods,
sender_address=ursula)
_txhash = staking_agent.set_worker(staker_address=ursula, worker_address=ursula)
# Get a reward for one period
_txhash = staking_agent.confirm_activity(worker_address=ursula)
testerchain.time_travel(periods=1)
_txhash = staking_agent.confirm_activity(worker_address=ursula)
assert staking_agent.calculate_staking_reward(staker_address=ursula) == 0
testerchain.time_travel(periods=1)
_txhash = staking_agent.confirm_activity(worker_address=ursula)
contract_reward = staking_agent.calculate_staking_reward(staker_address=ursula)
calculations_reward = token_economics.cumulative_rewards_at_period(1)
error = (contract_reward - calculations_reward) / calculations_reward
assert error > 0
assert error < MAX_ERROR
# Get a reward for other periods
for i in range(1, MAX_PERIODS):
testerchain.time_travel(periods=1)
_txhash = staking_agent.confirm_activity(worker_address=ursula)
contract_reward = staking_agent.calculate_staking_reward(staker_address=ursula)
calculations_reward = token_economics.cumulative_rewards_at_period(i + 1)
next_error = (contract_reward - calculations_reward) / calculations_reward
assert next_error > 0
assert next_error < error
error = next_error

View File

@ -24,10 +24,10 @@ from eth_tester.exceptions import TransactionFailed
from eth_utils import to_canonical_address
from web3.contract import Contract
from nucypher.blockchain.economics import TokenEconomics
from umbral.keys import UmbralPrivateKey
from umbral.signing import Signer
from nucypher.blockchain.eth.token import NU
from nucypher.crypto.api import sha256_digest
from nucypher.crypto.signing import SignatureStamp
@ -44,26 +44,34 @@ adjudicator_secret = os.urandom(SECRET_LENGTH)
@pytest.fixture()
def token(testerchain, deploy_contract):
def token_economics():
economics = TokenEconomics(initial_supply=10 ** 9,
total_supply=2 * 10 ** 9,
staking_coefficient=8 * 10 ** 7,
locked_periods_coefficient=4,
maximum_rewarded_periods=4,
hours_per_period=1,
minimum_locked_periods=6,
minimum_allowed_locked=100,
maximum_allowed_locked=2000,
minimum_worker_periods=2,
base_penalty=300,
percentage_penalty_coefficient=2)
return economics
@pytest.fixture()
def token(token_economics, deploy_contract):
# Create an ERC20 token
contract, _ = deploy_contract('NuCypherToken', _totalSupply=int(NU(2 * 10 ** 9, 'NuNit')))
contract, _ = deploy_contract('NuCypherToken', _totalSupply=token_economics.erc20_total_supply)
return contract
@pytest.fixture()
def escrow(testerchain, token, deploy_contract):
def escrow(testerchain, token, token_economics, deploy_contract):
# Creator deploys the escrow
contract, _ = deploy_contract(
contract_name='StakingEscrow',
_token=token.address,
_hoursPerPeriod=1,
_miningCoefficient=8*10**7,
_lockedPeriodsCoefficient=4,
_rewardedPeriods=4,
_minLockedPeriods=6,
_minAllowableLockedTokens=100,
_maxAllowableLockedTokens=2000,
_minWorkerPeriods=2
'StakingEscrow', token.address, *token_economics.staking_deployment_parameters
)
secret_hash = testerchain.w3.keccak(escrow_secret)
@ -101,22 +109,17 @@ def policy_manager(testerchain, escrow, deploy_contract):
@pytest.fixture()
def adjudicator(testerchain, escrow, slashing_economics, deploy_contract):
def adjudicator(testerchain, escrow, token_economics, deploy_contract):
escrow, _ = escrow
creator = testerchain.client.accounts[0]
secret_hash = testerchain.w3.keccak(adjudicator_secret)
deployment_parameters = list(slashing_economics.deployment_parameters)
# TODO: For some reason this test used non-standard slashing parameters (#354)
deployment_parameters[1] = 300
deployment_parameters[3] = 2
# Creator deploys the contract
contract, _ = deploy_contract(
'Adjudicator',
escrow.address,
*deployment_parameters)
*token_economics.slashing_deployment_parameters)
dispatcher, _ = deploy_contract('Dispatcher', contract.address, secret_hash)
@ -243,6 +246,7 @@ def execute_multisig_transaction(testerchain, multisig, accounts, tx):
@pytest.mark.slow
def test_all(testerchain,
token_economics,
token,
escrow,
policy_manager,
@ -250,7 +254,6 @@ def test_all(testerchain,
worklock,
user_escrow_proxy,
multisig,
slashing_economics,
mock_ursula_reencrypts,
deploy_contract):
@ -304,8 +307,7 @@ def test_all(testerchain,
testerchain.wait_for_receipt(tx)
# Initialize escrow
reward = 10 ** 9
tx = token.functions.transfer(escrow.address, reward).transact({'from': creator})
tx = token.functions.transfer(escrow.address, token_economics.erc20_reward_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.initialize().buildTransaction({'from': multisig.address, 'gasPrice': 0})
execute_multisig_transaction(testerchain, multisig, [contracts_owners[0], contracts_owners[1]], tx)
@ -361,7 +363,7 @@ def test_all(testerchain,
tx = worklock.functions.claim().transact({'from': ursula2, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert worklock.functions.getRemainingWork(ursula2).call() == deposit_rate * deposited_eth
assert reward + 1000 == token.functions.balanceOf(escrow.address).call()
assert token_economics.erc20_reward_supply + 1000 == token.functions.balanceOf(escrow.address).call()
assert 1000 == escrow.functions.getAllTokens(ursula2).call()
assert 0 == escrow.functions.getLockedTokens(ursula2).call()
assert 1000 == escrow.functions.getLockedTokens(ursula2, 1).call()
@ -474,7 +476,7 @@ def test_all(testerchain,
testerchain.wait_for_receipt(tx)
tx = escrow.functions.confirmActivity().transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
assert reward + 2000 == token.functions.balanceOf(escrow.address).call()
assert token_economics.erc20_reward_supply + 2000 == token.functions.balanceOf(escrow.address).call()
assert 9000 == token.functions.balanceOf(ursula1).call()
assert 0 == escrow.functions.getLockedTokens(ursula1).call()
assert 1000 == escrow.functions.getLockedTokens(ursula1, 1).call()
@ -494,7 +496,7 @@ def test_all(testerchain,
assert 1000 == escrow.functions.getLockedTokens(user_escrow_1.address, 1).call()
assert 1000 == escrow.functions.getLockedTokens(user_escrow_1.address, 10).call()
assert 0 == escrow.functions.getLockedTokens(user_escrow_1.address, 11).call()
assert reward + 3000 == token.functions.balanceOf(escrow.address).call()
assert token_economics.erc20_reward_supply + 3000 == token.functions.balanceOf(escrow.address).call()
assert 9000 == token.functions.balanceOf(user_escrow_1.address).call()
# Only user can deposit tokens to the staking escrow
@ -680,16 +682,7 @@ def test_all(testerchain,
policy_manager_v1 = policy_manager.functions.target().call()
# Creator deploys the contracts as the second versions
escrow_v2, _ = deploy_contract(
contract_name='StakingEscrow',
_token=token.address,
_hoursPerPeriod=1,
_miningCoefficient=8 * 10 ** 7,
_lockedPeriodsCoefficient=4,
_rewardedPeriods=4,
_minLockedPeriods=6,
_minAllowableLockedTokens=100,
_maxAllowableLockedTokens=2000,
_minWorkerPeriods=2
'StakingEscrow', token.address, *token_economics.staking_deployment_parameters
)
policy_manager_v2, _ = deploy_contract('PolicyManager', escrow.address)
# Ursula and Alice can't upgrade contracts, only owner can
@ -839,12 +832,7 @@ def test_all(testerchain,
total_lock = escrow.functions.lockedPerPeriod(current_period).call()
alice1_balance = token.functions.balanceOf(alice1).call()
deployment_parameters = list(slashing_economics.deployment_parameters)
# TODO: For some reason this test used non-stadard slashing parameters (#354)
deployment_parameters[1] = 300
deployment_parameters[3] = 2
algorithm_sha256, base_penalty, *coefficients = deployment_parameters
algorithm_sha256, base_penalty, *coefficients = token_economics.slashing_deployment_parameters
penalty_history_coefficient, percentage_penalty_coefficient, reward_coefficient = coefficients
data_hash, slashing_args = generate_args_for_slashing(mock_ursula_reencrypts, ursula1_with_stamp)
@ -912,7 +900,7 @@ def test_all(testerchain,
adjudicator_v2, _ = deploy_contract(
'Adjudicator',
escrow.address,
*slashing_economics.deployment_parameters)
*token_economics.slashing_deployment_parameters)
adjudicator_secret2 = os.urandom(SECRET_LENGTH)
adjudicator_secret2_hash = testerchain.w3.keccak(adjudicator_secret2)
# Ursula and Alice can't upgrade library, only owner can

View File

@ -36,11 +36,11 @@ def escrow(testerchain, deploy_contract):
@pytest.fixture(params=[False, True])
def adjudicator(testerchain, escrow, request, slashing_economics, deploy_contract):
def adjudicator(testerchain, escrow, request, token_economics, deploy_contract):
contract, _ = deploy_contract(
'Adjudicator',
escrow.address,
*slashing_economics.deployment_parameters)
*token_economics.slashing_deployment_parameters)
if request.param:
secret = os.urandom(DispatcherDeployer.DISPATCHER_SECRET_LENGTH)

View File

@ -40,7 +40,7 @@ secret2 = (654321).to_bytes(32, byteorder='big')
def test_evaluate_cfrag(testerchain,
escrow,
adjudicator,
slashing_economics,
token_economics,
blockchain_ursulas,
mock_ursula_reencrypts
):
@ -56,10 +56,10 @@ def test_evaluate_cfrag(testerchain,
number_of_evaluations = 0
def compute_penalty_and_reward(stake: int, penalty_history: int) -> Tuple[int, int]:
penalty_ = slashing_economics.base_penalty
penalty_ += slashing_economics.penalty_history_coefficient * penalty_history
penalty_ = min(penalty_, stake // slashing_economics.percentage_penalty_coefficient)
reward_ = penalty_ // slashing_economics.reward_coefficient
penalty_ = token_economics.base_penalty
penalty_ += token_economics.penalty_history_coefficient * penalty_history
penalty_ = min(penalty_, stake // token_economics.percentage_penalty_coefficient)
reward_ = penalty_ // token_economics.reward_coefficient
return penalty_, reward_
# Prepare one staker
@ -206,7 +206,7 @@ def test_evaluate_cfrag(testerchain,
previous_penalty = penalty
penalty, reward = compute_penalty_and_reward(worker_stake, worker_penalty_history)
# Penalty was increased because it's the second violation
assert penalty == previous_penalty + slashing_economics.penalty_history_coefficient
assert penalty == previous_penalty + token_economics.penalty_history_coefficient
worker_stake -= penalty
investigator_balance += reward
worker_penalty_history += 1
@ -252,7 +252,7 @@ def test_evaluate_cfrag(testerchain,
penalty, reward = compute_penalty_and_reward(worker_stake, worker_penalty_history)
# Penalty has reached maximum available percentage of value
assert penalty == worker_stake // slashing_economics.percentage_penalty_coefficient
assert penalty == worker_stake // token_economics.percentage_penalty_coefficient
worker_stake -= penalty
investigator_balance += reward
worker_penalty_history += 1

View File

@ -20,6 +20,7 @@ import pytest
from web3.contract import Contract
from eth_utils import keccak
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.eth.token import NU
VALUE_FIELD = 0
@ -28,29 +29,33 @@ secret = (123456).to_bytes(32, byteorder='big')
@pytest.fixture()
def token(testerchain, deploy_contract):
def token_economics():
economics = TokenEconomics(initial_supply=10 ** 9,
total_supply=2 * 10 ** 9,
staking_coefficient=8 * 10 ** 7,
locked_periods_coefficient=4,
maximum_rewarded_periods=4,
hours_per_period=1,
minimum_locked_periods=2,
minimum_allowed_locked=100,
minimum_worker_periods=1)
return economics
@pytest.fixture()
def token(deploy_contract, token_economics):
# Create an ERC20 token
token, _ = deploy_contract('NuCypherToken', _totalSupply=int(NU(2 * 10 ** 9, 'NuNit')))
token, _ = deploy_contract('NuCypherToken', _totalSupply=token_economics.erc20_total_supply)
return token
@pytest.fixture(params=[False, True])
def escrow_contract(testerchain, token, request, deploy_contract):
def escrow_contract(testerchain, token, token_economics, request, deploy_contract):
def make_escrow(max_allowed_locked_tokens):
# Creator deploys the escrow
_staking_coefficient = 2 * 10 ** 7
contract, _ = deploy_contract(
contract_name='StakingEscrow',
_token=token.address,
_hoursPerPeriod=1,
_miningCoefficient=4 * _staking_coefficient,
_lockedPeriodsCoefficient=4,
_rewardedPeriods=4,
_minLockedPeriods=2,
_minAllowableLockedTokens=100,
_maxAllowableLockedTokens=max_allowed_locked_tokens,
_minWorkerPeriods=1
)
deploy_parameters = list(token_economics.staking_deployment_parameters)
deploy_parameters[-2] = max_allowed_locked_tokens
contract, _ = deploy_contract('StakingEscrow', token.address, *deploy_parameters)
if request.param:
secret_hash = keccak(secret)

View File

@ -22,7 +22,8 @@ from web3.contract import Contract
@pytest.mark.slow
def test_mining(testerchain, token, escrow_contract):
def test_mining(testerchain, token, escrow_contract, token_economics):
escrow = escrow_contract(1500)
policy_manager_interface = testerchain.get_contract_factory('PolicyManagerForStakingEscrowMock')
policy_manager = testerchain.client.get_contract(
@ -33,6 +34,13 @@ def test_mining(testerchain, token, escrow_contract):
ursula1 = testerchain.client.accounts[1]
ursula2 = testerchain.client.accounts[2]
current_supply = token_economics.erc20_initial_supply
def calculate_reward(locked, total_locked, locked_periods):
return (token_economics.erc20_total_supply - current_supply) * locked * \
(locked_periods + token_economics.locked_periods_coefficient) // \
(total_locked * token_economics.staking_coefficient)
staking_log = escrow.events.Mined.createFilter(fromBlock='latest')
deposit_log = escrow.events.Deposited.createFilter(fromBlock='latest')
lock_log = escrow.events.Locked.createFilter(fromBlock='latest')
@ -41,7 +49,7 @@ def test_mining(testerchain, token, escrow_contract):
withdraw_log = escrow.events.Withdrawn.createFilter(fromBlock='latest')
# Give Escrow tokens for reward and initialize contract
tx = token.functions.transfer(escrow.address, 10 ** 9).transact({'from': creator})
tx = token.functions.transfer(escrow.address, token_economics.erc20_reward_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.initialize().transact({'from': creator})
testerchain.wait_for_receipt(tx)
@ -49,7 +57,7 @@ def test_mining(testerchain, token, escrow_contract):
# Give Ursula and Ursula(2) some coins
tx = token.functions.transfer(ursula1, 10000).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.transfer(ursula2, 10000).transact({'from': creator})
tx = token.functions.transfer(ursula2, 850).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Ursula can't confirm and mint because no locked tokens
@ -67,13 +75,15 @@ def test_mining(testerchain, token, escrow_contract):
testerchain.wait_for_receipt(tx)
# Ursula and Ursula(2) transfer some tokens to the escrow and lock them
tx = escrow.functions.deposit(1000, 2).transact({'from': ursula1})
ursula1_stake = 1000
ursula2_stake = 500
tx = escrow.functions.deposit(ursula1_stake, 2).transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.setWorker(ursula1).transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.confirmActivity().transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.deposit(500, 2).transact({'from': ursula2})
tx = escrow.functions.deposit(ursula2_stake, 2).transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.setWorker(ursula2).transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
@ -129,8 +139,11 @@ def test_mining(testerchain, token, escrow_contract):
current_period = escrow.functions.getCurrentPeriod().call()
# Check result of mining
assert 1046 == escrow.functions.getAllTokens(ursula1).call()
assert 525 == escrow.functions.getAllTokens(ursula2).call()
total_locked = ursula1_stake + ursula2_stake
ursula1_reward = calculate_reward(500, total_locked, 1) + calculate_reward(500, total_locked, 2)
assert ursula1_stake + ursula1_reward == escrow.functions.getAllTokens(ursula1).call()
ursula2_reward = calculate_reward(500, total_locked, 2)
assert ursula2_stake + ursula2_reward == escrow.functions.getAllTokens(ursula2).call()
# Check that downtime value has not changed
assert 1 == escrow.functions.getPastDowntimeLength(ursula1).call()
assert 1 == escrow.functions.getPastDowntimeLength(ursula2).call()
@ -141,11 +154,11 @@ def test_mining(testerchain, token, escrow_contract):
assert 2 == len(events)
event_args = events[0]['args']
assert ursula1 == event_args['staker']
assert 46 == event_args['value']
assert ursula1_reward == event_args['value']
assert escrow.functions.getCurrentPeriod().call() - 1 == event_args['period']
event_args = events[1]['args']
assert ursula2 == event_args['staker']
assert 25 == event_args['value']
assert ursula2_reward == event_args['value']
assert escrow.functions.getCurrentPeriod().call() - 1 == event_args['period']
# Check parameters in call of the policy manager mock
@ -157,14 +170,17 @@ def test_mining(testerchain, token, escrow_contract):
# Ursula tries to mint again and doesn't receive a reward
# There are no more confirmed periods that are ready to mint
ursula1_stake += ursula1_reward
ursula2_stake += ursula2_reward
tx = escrow.functions.mint().transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
assert 1046 == escrow.functions.getAllTokens(ursula1).call()
assert ursula1_stake == escrow.functions.getAllTokens(ursula1).call()
events = staking_log.get_all_entries()
assert 2 == len(events)
# Ursula can't confirm next period because stake is unlocked in current period
testerchain.time_travel(hours=1)
current_supply += ursula1_reward + ursula2_reward
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.confirmActivity().transact({'from': ursula1})
testerchain.wait_for_receipt(tx)
@ -187,14 +203,16 @@ def test_mining(testerchain, token, escrow_contract):
# But Ursula(2) can't get reward because she did not confirm activity
tx = escrow.functions.mint().transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
assert 1152 == escrow.functions.getAllTokens(ursula1).call()
assert 525 == escrow.functions.getAllTokens(ursula2).call()
ursula1_reward = calculate_reward(500, 1000, 0) + calculate_reward(500, 1000, 1) + calculate_reward(500, 500, 0)
assert ursula1_stake + ursula1_reward == escrow.functions.getAllTokens(ursula1).call()
assert ursula2_stake == escrow.functions.getAllTokens(ursula2).call()
ursula1_stake += ursula1_reward
events = staking_log.get_all_entries()
assert 3 == len(events)
event_args = events[2]['args']
assert ursula1 == event_args['staker']
assert 106 == event_args['value']
assert ursula1_reward == event_args['value']
assert current_period == event_args['period']
assert 4 == policy_manager.functions.getPeriodsLength(ursula1).call()
@ -204,16 +222,19 @@ def test_mining(testerchain, token, escrow_contract):
# Ursula(2) mints tokens
testerchain.time_travel(hours=1)
current_supply += ursula1_reward
tx = escrow.functions.mint().transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
assert 1152 == escrow.functions.getAllTokens(ursula1).call()
assert 575 == escrow.functions.getAllTokens(ursula2).call()
ursula2_reward = calculate_reward(500, 500, 0)
assert ursula1_stake == escrow.functions.getAllTokens(ursula1).call()
assert ursula2_stake + ursula2_reward == escrow.functions.getAllTokens(ursula2).call()
ursula2_stake += ursula2_reward
events = staking_log.get_all_entries()
assert 4 == len(events)
event_args = events[3]['args']
assert ursula2 == event_args['staker']
assert 50 == event_args['value']
assert ursula2_reward == event_args['value']
assert escrow.functions.getCurrentPeriod().call() - 1 == event_args['period']
current_period = escrow.functions.getCurrentPeriod().call() - 1
@ -231,7 +252,7 @@ def test_mining(testerchain, token, escrow_contract):
testerchain.wait_for_receipt(tx)
current_period = escrow.functions.getCurrentPeriod().call()
assert current_period - 2 == escrow.functions.getLastActivePeriod(ursula1).call()
assert 1152 == escrow.functions.getAllTokens(ursula1).call()
assert ursula1_stake == escrow.functions.getAllTokens(ursula1).call()
# Ursula still can't confirm activity
with pytest.raises((TransactionFailed, ValueError)):
tx = escrow.functions.confirmActivity().transact({'from': ursula1})
@ -244,6 +265,7 @@ def test_mining(testerchain, token, escrow_contract):
testerchain.wait_for_receipt(tx)
tx = escrow.functions.lock(500, 2).transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
ursula2_stake += 250
assert 3 == escrow.functions.getPastDowntimeLength(ursula2).call()
downtime = escrow.functions.getPastDowntime(ursula2, 2).call()
@ -252,20 +274,24 @@ def test_mining(testerchain, token, escrow_contract):
# Ursula(2) mints only one period (by using deposit/approveAndCall function)
testerchain.time_travel(hours=5)
current_supply += ursula2_reward
current_period = escrow.functions.getCurrentPeriod().call()
assert current_period - 4 == escrow.functions.getLastActivePeriod(ursula2).call()
tx = token.functions.approveAndCall(escrow.address, 100, testerchain.w3.toBytes(2))\
.transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
ursula2_stake += 100
tx = escrow.functions.confirmActivity().transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
assert 1152 == escrow.functions.getAllTokens(ursula1).call()
assert 1025 == escrow.functions.getAllTokens(ursula2).call()
ursula2_reward = calculate_reward(250, 750, 4) + calculate_reward(500, 750, 4)
assert ursula1_stake == escrow.functions.getAllTokens(ursula1).call()
assert ursula2_stake + ursula2_reward == escrow.functions.getAllTokens(ursula2).call()
assert 4 == escrow.functions.getPastDowntimeLength(ursula2).call()
downtime = escrow.functions.getPastDowntime(ursula2, 3).call()
assert current_period - 3 == downtime[0]
assert current_period == downtime[1]
ursula2_stake += ursula2_reward
assert 4 == policy_manager.functions.getPeriodsLength(ursula2).call()
assert current_period - 4 == policy_manager.functions.getPeriod(ursula2, 3).call()
@ -274,7 +300,7 @@ def test_mining(testerchain, token, escrow_contract):
assert 5 == len(events)
event_args = events[4]['args']
assert ursula2 == event_args['staker']
assert 100 == event_args['value']
assert ursula2_reward == event_args['value']
assert escrow.functions.getCurrentPeriod().call() - 1 == event_args['period']
# Ursula(2) confirms activity for remaining periods
@ -288,17 +314,18 @@ def test_mining(testerchain, token, escrow_contract):
# Ursula(2) withdraws all
testerchain.time_travel(hours=2)
ursula2_stake = escrow.functions.getAllTokens(ursula2).call()
assert 0 == escrow.functions.getLockedTokens(ursula2).call()
tx = escrow.functions.withdraw(1083).transact({'from': ursula2})
tx = escrow.functions.withdraw(ursula2_stake).transact({'from': ursula2})
testerchain.wait_for_receipt(tx)
assert 0 == escrow.functions.getAllTokens(ursula2).call()
assert 10233 == token.functions.balanceOf(ursula2).call()
assert ursula2_stake == token.functions.balanceOf(ursula2).call()
events = withdraw_log.get_all_entries()
assert 1 == len(events)
event_args = events[0]['args']
assert ursula2 == event_args['staker']
assert 1083 == event_args['value']
assert ursula2_stake == event_args['value']
assert 4 == len(deposit_log.get_all_entries())
assert 6 == len(lock_log.get_all_entries())
@ -307,7 +334,7 @@ def test_mining(testerchain, token, escrow_contract):
@pytest.mark.slow
def test_slashing(testerchain, token, escrow_contract, deploy_contract):
def test_slashing(testerchain, token, escrow_contract, token_economics, deploy_contract):
escrow = escrow_contract(1500)
adjudicator, _ = deploy_contract(
'AdjudicatorForStakingEscrowMock', escrow.address
@ -321,7 +348,7 @@ def test_slashing(testerchain, token, escrow_contract, deploy_contract):
slashing_log = escrow.events.Slashed.createFilter(fromBlock='latest')
# Give Escrow tokens for reward and initialize contract
tx = token.functions.transfer(escrow.address, 10 ** 9).transact({'from': creator})
tx = token.functions.transfer(escrow.address, token_economics.erc20_reward_supply).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.initialize().transact({'from': creator})
testerchain.wait_for_receipt(tx)

View File

@ -32,7 +32,7 @@ secret2 = (654321).to_bytes(32, byteorder='big')
@pytest.mark.slow
def test_upgrading(testerchain, token, deploy_contract):
def test_upgrading(testerchain, token, token_economics, deploy_contract):
creator = testerchain.client.accounts[0]
staker = testerchain.client.accounts[1]
@ -41,16 +41,7 @@ def test_upgrading(testerchain, token, deploy_contract):
# Deploy contract
contract_library_v1, _ = deploy_contract(
contract_name='StakingEscrow',
_token=token.address,
_hoursPerPeriod=1,
_miningCoefficient=8*10**7,
_lockedPeriodsCoefficient=4,
_rewardedPeriods=4,
_minLockedPeriods=2,
_minAllowableLockedTokens=100,
_maxAllowableLockedTokens=1500,
_minWorkerPeriods=1
'StakingEscrow', token.address, *token_economics.staking_deployment_parameters
)
dispatcher, _ = deploy_contract('Dispatcher', contract_library_v1.address, secret_hash)
@ -73,7 +64,7 @@ def test_upgrading(testerchain, token, deploy_contract):
abi=contract_library_v2.abi,
address=dispatcher.address,
ContractFactoryClass=Contract)
assert 1500 == contract.functions.maxAllowableLockedTokens().call()
assert token_economics.maximum_allowed_locked == contract.functions.maxAllowableLockedTokens().call()
# Can't call `finishUpgrade` and `verifyState` methods outside upgrade lifecycle
with pytest.raises((TransactionFailed, ValueError)):
@ -116,7 +107,7 @@ def test_upgrading(testerchain, token, deploy_contract):
testerchain.wait_for_receipt(tx)
# Check constructor and storage values
assert contract_library_v2.address == dispatcher.functions.target().call()
assert 1500 == contract.functions.maxAllowableLockedTokens().call()
assert token_economics.maximum_allowed_locked == contract.functions.maxAllowableLockedTokens().call()
assert policy_manager.address == contract.functions.policyManager().call()
assert 2 == contract.functions.valueToCheck().call()
# Check new ABI
@ -248,7 +239,7 @@ def test_re_stake(testerchain, token, escrow_contract):
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(escrow.address, 10000).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
sub_stake = 1000
sub_stake = 100
tx = escrow.functions.deposit(sub_stake, 10).transact({'from': ursula})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.setWorker(ursula).transact({'from': ursula})
@ -398,8 +389,8 @@ def test_re_stake(testerchain, token, escrow_contract):
# To calculate amount of re-stake we can split Ursula1's reward according sub stakes ratio:
# first sub stake is 2/3 of entire stake and second sub stake is 1/3
re_stake_for_first_sub_stake = ursula_reward * 2 // 3
re_stake_for_second_sub_stake = ursula_reward - re_stake_for_first_sub_stake
re_stake_for_second_sub_stake = ursula_reward // 3
re_stake_for_first_sub_stake = ursula_reward - re_stake_for_second_sub_stake
# Check re-stake for Ursula1's sub stakes
assert stake + ursula_reward == escrow.functions.getLockedTokens(ursula).call()
assert sub_stake_1 + re_stake_for_first_sub_stake == escrow.functions.getSubStakeInfo(ursula, 0).call()[3]

View File

@ -83,7 +83,7 @@ def test_rapid_deployment(token_economics, test_registry):
beneficiary_address = acct.address
amount = random.randint(token_economics.minimum_allowed_locked, token_economics.maximum_allowed_locked)
duration = random.randint(token_economics.minimum_locked_periods*ONE_YEAR_IN_SECONDS,
(token_economics.maximum_locked_periods*ONE_YEAR_IN_SECONDS)*3)
(token_economics.maximum_rewarded_periods*ONE_YEAR_IN_SECONDS)*3)
random_allocation = {'beneficiary_address': beneficiary_address, 'amount': amount, 'duration_seconds': duration}
allocation_data.append(random_allocation)

View File

@ -45,8 +45,7 @@ def test_investigator_requests_slashing(testerchain,
test_registry,
session_agency,
mock_ursula_reencrypts,
token_economics,
slashing_economics):
token_economics):
testerchain = testerchain
staker_account = testerchain.staker_account(0)
@ -109,5 +108,5 @@ def test_investigator_requests_slashing(testerchain,
investigator_reward = investigator.token_balance - bobby_old_balance
assert investigator_reward > 0
assert investigator_reward == slashing_economics.base_penalty / slashing_economics.reward_coefficient
assert investigator_reward == token_economics.base_penalty / token_economics.reward_coefficient
assert staker.locked_tokens(periods=1) < locked_tokens

View File

@ -81,7 +81,8 @@ def test_staker_divides_stake(staker, token_economics):
value=yet_another_stake_value,
checksum_address=staker.checksum_address,
index=3,
staking_agent=staker.staking_agent)
staking_agent=staker.staking_agent,
economics=token_economics)
assert 4 == len(staker.stakes), 'A new stake was not added after two stake divisions'
assert expected_old_stake == staker.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'

View File

@ -47,7 +47,6 @@ def test_adjudicator_slashes(agency,
testerchain,
mock_ursula_reencrypts,
token_economics,
slashing_economics,
test_registry):
staker_account = testerchain.staker_account(0)
@ -113,5 +112,5 @@ def test_adjudicator_slashes(agency,
investigator_reward = bobby.token_balance - bobby_old_balance
assert investigator_reward > 0
assert investigator_reward == slashing_economics.base_penalty / slashing_economics.reward_coefficient
assert investigator_reward == token_economics.base_penalty / token_economics.reward_coefficient
assert staker.locked_tokens(periods=1) < locked_tokens

View File

@ -30,7 +30,7 @@ from nucypher.blockchain.eth.deployers import (
@pytest.mark.slow()
def test_adjudicator_deployer(testerchain,
slashing_economics,
token_economics,
deployment_progress,
test_registry):
testerchain = testerchain
@ -61,11 +61,11 @@ def test_adjudicator_deployer(testerchain,
# Check default Adjudicator deployment parameters
assert staking_escrow_deployer.deployer_address != staking_agent.contract_address
assert adjudicator_agent.staking_escrow_contract == staking_agent.contract_address
assert adjudicator_agent.hash_algorithm == slashing_economics.hash_algorithm
assert adjudicator_agent.base_penalty == slashing_economics.base_penalty
assert adjudicator_agent.penalty_history_coefficient == slashing_economics.penalty_history_coefficient
assert adjudicator_agent.percentage_penalty_coefficient == slashing_economics.percentage_penalty_coefficient
assert adjudicator_agent.reward_coefficient == slashing_economics.reward_coefficient
assert adjudicator_agent.hash_algorithm == token_economics.hash_algorithm
assert adjudicator_agent.base_penalty == token_economics.base_penalty
assert adjudicator_agent.penalty_history_coefficient == token_economics.penalty_history_coefficient
assert adjudicator_agent.percentage_penalty_coefficient == token_economics.percentage_penalty_coefficient
assert adjudicator_agent.reward_coefficient == token_economics.reward_coefficient
# Retrieve the AdjudicatorAgent singleton
some_policy_agent = AdjudicatorAgent(registry=test_registry)

View File

@ -74,7 +74,7 @@ def test_deploy_and_allocate(session_agency, user_escrow_proxy, token_economics,
for address, deployer in deployments.items():
assert deployer.deployer_address == origin
deposit_receipt = deployer.initial_deposit(value=allocation, duration_seconds=token_economics.maximum_locked_periods)
deposit_receipt = deployer.initial_deposit(value=allocation, duration_seconds=token_economics.maximum_rewarded_periods)
deposit_receipts.append(deposit_receipt)
beneficiary = random.choice(testerchain.unassigned_accounts)

View File

@ -19,7 +19,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
from decimal import Decimal, localcontext
from math import log
from nucypher.blockchain.economics import TokenEconomics, LOG2
from nucypher.blockchain.economics import LOG2, StandardTokenEconomics
def test_rough_economics():
@ -37,11 +37,11 @@ def test_rough_economics():
where allLockedPeriods == min(T, T1)
"""
e = TokenEconomics(initial_supply=int(1e9),
initial_inflation=1,
halving_delay=2,
reward_saturation=1,
small_stake_multiplier=Decimal(0.5))
e = StandardTokenEconomics(initial_supply=int(1e9),
initial_inflation=1,
halving_delay=2,
reward_saturation=1,
small_stake_multiplier=Decimal(0.5))
assert float(round(e.erc20_total_supply / Decimal(1e9), 2)) == 3.89 # As per economics paper
@ -112,7 +112,7 @@ def test_exact_economics():
# Use same precision as economics class
with localcontext() as ctx:
ctx.prec = TokenEconomics._precision
ctx.prec = StandardTokenEconomics._precision
# Sanity check expected testing outputs
assert Decimal(expected_total_supply) / expected_initial_supply == expected_supply_ratio
@ -134,10 +134,10 @@ def test_exact_economics():
#
# Check creation
e = TokenEconomics()
e = StandardTokenEconomics()
with localcontext() as ctx:
ctx.prec = TokenEconomics._precision
ctx.prec = StandardTokenEconomics._precision
# Check that total_supply calculated correctly
assert Decimal(e.erc20_total_supply) / e.initial_supply == expected_supply_ratio
@ -159,14 +159,35 @@ def test_exact_economics():
assert e.erc20_initial_supply == expected_initial_supply
assert e.erc20_reward_supply == expected_reward_supply
# Additional checks on supply
assert e.token_supply_at_period(period=0) == expected_initial_supply
assert e.cumulative_rewards_at_period(0) == 0
# Last NuNit is mined after 184 years (or 67000 periods).
# That's the year 2203, if token is launched in 2019.
# 23rd century schizoid man!
assert expected_total_supply == e.token_supply_at_period(period=67000)
# After 1 year:
assert 1_845_111_188_584347879497984668 == e.token_supply_at_period(period=365)
assert 845_111_188_584347879497984668 == e.cumulative_rewards_at_period(365)
assert e.erc20_initial_supply + e.cumulative_rewards_at_period(365) == e.token_supply_at_period(period=365)
# Checking that the supply function is monotonic
todays_supply = e.token_supply_at_period(period=0)
for t in range(67000):
tomorrows_supply = e.token_supply_at_period(period=t + 1)
assert tomorrows_supply >= todays_supply
todays_supply = tomorrows_supply
def test_economic_parameter_aliases():
e = TokenEconomics()
e = StandardTokenEconomics()
assert e.locked_periods_coefficient == 365
assert int(e.staking_coefficient) == 768812
assert e.maximum_locked_periods == 365
assert e.maximum_rewarded_periods == 365
deployment_params = e.staking_deployment_parameters
assert isinstance(deployment_params, tuple)

View File

@ -3,7 +3,7 @@ from decimal import InvalidOperation, Decimal
import pytest
from web3 import Web3
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import TokenEconomics, StandardTokenEconomics
from nucypher.blockchain.eth.token import NU, Stake
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
@ -99,7 +99,7 @@ def test_NU(token_economics):
_nan = NU(float('NaN'), 'NU')
def test_stake(testerchain, agency):
def test_stake(testerchain, token_economics, agency):
token_agent, staking_agent, _policy_agent = agency
class FakeUrsula:
@ -110,7 +110,6 @@ def test_stake(testerchain, agency):
staking_agent = staking_agent
token_agent = token_agent
blockchain = testerchain
economics = TokenEconomics()
ursula = FakeUrsula()
stake = Stake(checksum_address=ursula.checksum_address,
@ -118,7 +117,8 @@ def test_stake(testerchain, agency):
final_locked_period=100,
value=NU(100, 'NU'),
index=0,
staking_agent=staking_agent)
staking_agent=staking_agent,
economics=token_economics)
assert stake.value, 'NU' == NU(100, 'NU')

View File

@ -133,7 +133,8 @@ def test_stake_init(click_runner,
stake = Stake.from_stake_info(index=0,
checksum_address=manual_staker,
stake_info=stakes[0],
staking_agent=staking_agent)
staking_agent=staking_agent,
economics=token_economics)
assert stake.value == stake_value
assert stake.duration == token_economics.minimum_locked_periods

View File

@ -32,7 +32,7 @@ from umbral.keys import UmbralPrivateKey
from umbral.signing import Signer
from web3 import Web3
from nucypher.blockchain.economics import TokenEconomics, SlashingEconomics
from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.actors import Staker
from nucypher.blockchain.eth.agents import NucypherTokenAgent
from nucypher.blockchain.eth.clients import NuCypherGethDevProcess
@ -343,13 +343,7 @@ def federated_ursulas(ursula_federated_test_config):
@pytest.fixture(scope='session')
def token_economics():
economics = TokenEconomics()
return economics
@pytest.fixture(scope='session')
def slashing_economics():
economics = SlashingEconomics()
economics = StandardTokenEconomics()
return economics
@ -432,7 +426,7 @@ def _make_agency(testerchain, test_registry):
adjudicator_deployer.deploy(secret_hash=os.urandom(DispatcherDeployer.DISPATCHER_SECRET_LENGTH))
token_agent = token_deployer.make_agent() # 1 Token
staking_agent = staking_escrow_deployer.make_agent() # 2 Miner Escrow
staking_agent = staking_escrow_deployer.make_agent() # 2 Staking Escrow
policy_agent = policy_manager_deployer.make_agent() # 3 Policy Agent
_adjudicator_agent = adjudicator_deployer.make_agent() # 4 Adjudicator
@ -493,7 +487,7 @@ def stakers(testerchain, agency, token_economics, test_registry):
amount = random.randint(min_stake, balance)
# for a random lock duration
min_locktime, max_locktime = token_economics.minimum_locked_periods, token_economics.maximum_locked_periods
min_locktime, max_locktime = token_economics.minimum_locked_periods, token_economics.maximum_rewarded_periods
periods = random.randint(min_locktime, max_locktime)
staker.initialize_stake(amount=amount, lock_periods=periods)

View File

@ -32,7 +32,7 @@ from umbral.keys import UmbralPrivateKey
from umbral.signing import Signer
from zope.interface import provider
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.agents import NucypherTokenAgent, StakingEscrowAgent, PolicyManagerAgent, AdjudicatorAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
@ -46,11 +46,11 @@ from fixtures import _mock_ursula_reencrypts as mock_ursula_reencrypts
ALGORITHM_SHA256 = 1
TOKEN_ECONOMICS = TokenEconomics()
TOKEN_ECONOMICS = StandardTokenEconomics()
MIN_ALLOWED_LOCKED = TOKEN_ECONOMICS.minimum_allowed_locked
MIN_LOCKED_PERIODS = TOKEN_ECONOMICS.minimum_locked_periods
MAX_ALLOWED_LOCKED = TOKEN_ECONOMICS.maximum_allowed_locked
MAX_MINTING_PERIODS = TOKEN_ECONOMICS.maximum_locked_periods
MAX_MINTING_PERIODS = TOKEN_ECONOMICS.maximum_rewarded_periods
class AnalyzeGas:
@ -155,7 +155,13 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
log = Logger(AnalyzeGas.LOG_NAME)
# Blockchain
testerchain, registry = TesterBlockchain.bootstrap_network()
economics = StandardTokenEconomics(
base_penalty=MIN_ALLOWED_LOCKED - 1,
penalty_history_coefficient=0,
percentage_penalty_coefficient=2,
reward_coefficient=2
)
testerchain, registry = TesterBlockchain.bootstrap_network(economics=economics)
web3 = testerchain.w3
# Accounts