diff --git a/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol b/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol index 7978997ce..1f41ecbce 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol @@ -10,7 +10,7 @@ import "zeppelin/token/ERC20/SafeERC20.sol"; /** * @notice Contract for calculation of issued tokens -* @dev |v3.1.1| +* @dev |v3.2.1| */ abstract contract Issuer is Upgradeable { using SafeERC20 for NuCypherToken; @@ -25,13 +25,20 @@ abstract contract Issuer is Upgradeable { NuCypherToken public immutable token; uint128 public immutable totalSupply; - uint256 public immutable miningCoefficient; - uint256 public immutable lockedPeriodsCoefficient; + // k2 * k3 + uint256 public immutable mintingCoefficient; + // k1 + uint256 public immutable lockingDurationCoefficient1; + // k3 + uint256 public immutable lockingDurationCoefficient2; uint32 public immutable secondsPerPeriod; - uint16 public immutable rewardedPeriods; + uint16 public immutable maxRewardedPeriods; + + uint256 public immutable maxFirstPhaseReward; + uint256 public immutable firstPhaseTotalSupply; /** - * Current supply is used in the mining formula and is stored to prevent different calculation + * Current supply is used in the minting formula and is stored to prevent different calculation * for stakers which get reward in the same period. There are two values - * supply for previous period (used in formula) and supply for current period which accumulates value * before end of period. @@ -41,47 +48,68 @@ abstract contract Issuer is Upgradeable { uint16 public currentMintingPeriod; /** - * @notice Constructor sets address of token contract and coefficients for mining - * @dev Mining formula for one stake in one period - (totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k2 - if allLockedPeriods > rewardedPeriods then allLockedPeriods = rewardedPeriods + * @notice Constructor sets address of token contract and coefficients for minting + * @dev Minting formula for one sub-stake in one period for the first phase + maxFirstPhaseReward * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k3 + * @dev Minting formula for one sub-stake in one period for the seconds phase + (totalSupply - currentSupply) / k2 * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k3 + if allLockedPeriods > maxRewardedPeriods then allLockedPeriods = maxRewardedPeriods * @param _token Token contract * @param _hoursPerPeriod Size of period in hours - * @param _miningCoefficient Mining coefficient (k2) - * @param _lockedPeriodsCoefficient Locked periods coefficient (k1) - * @param _rewardedPeriods Max periods that will be additionally rewarded + * @param _secondPhaseMintingCoefficient Minting coefficient for the second phase (k2) + * @param _lockingDurationCoefficient1 Numerator of he locking duration coefficient (k1) + * @param _lockingDurationCoefficient2 Denominator of the locking duration coefficient (k3) + * @param _maxRewardedPeriods Max periods that will be additionally rewarded + * @param _firstPhaseTotalSupply Total supply for the first phase + * @param _maxFirstPhaseReward Max possible reward for one period for all stakers in the first phase */ constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward ) public { uint256 localTotalSupply = _token.totalSupply(); require(localTotalSupply > 0 && - _miningCoefficient != 0 && + _secondPhaseMintingCoefficient != 0 && _hoursPerPeriod != 0 && - _lockedPeriodsCoefficient != 0 && - _rewardedPeriods != 0); + _lockingDurationCoefficient1 != 0 && + _lockingDurationCoefficient2 != 0 && + _maxRewardedPeriods != 0); require(localTotalSupply <= uint256(MAX_UINT128), "Token contract has supply more than supported"); - uint256 maxLockedPeriods = _rewardedPeriods + _lockedPeriodsCoefficient; - require(maxLockedPeriods > _rewardedPeriods && - _miningCoefficient >= maxLockedPeriods && - // worst case for `totalLockedValue * k2`, when totalLockedValue == totalSupply - localTotalSupply * _miningCoefficient / localTotalSupply == _miningCoefficient && + + uint256 maxLockingDurationCoefficient = _maxRewardedPeriods + _lockingDurationCoefficient1; + uint256 localMintingCoefficient = _secondPhaseMintingCoefficient * _lockingDurationCoefficient2; + require(maxLockingDurationCoefficient > _maxRewardedPeriods && + localMintingCoefficient / _secondPhaseMintingCoefficient == _lockingDurationCoefficient2 && + // worst case for `totalLockedValue * k2 * k3`, when totalLockedValue == totalSupply + localTotalSupply * localMintingCoefficient / localTotalSupply == localMintingCoefficient && // worst case for `(totalSupply - currentSupply) * lockedValue * (k1 + allLockedPeriods)`, // when currentSupply == 0, lockedValue == totalSupply - localTotalSupply * localTotalSupply * maxLockedPeriods / localTotalSupply / localTotalSupply == maxLockedPeriods, + localTotalSupply * localTotalSupply * maxLockingDurationCoefficient / localTotalSupply / localTotalSupply == + maxLockingDurationCoefficient, "Specified parameters cause overflow"); + + require(maxLockingDurationCoefficient <= _lockingDurationCoefficient2, + "Resulting locking duration coefficient must be less than 1"); + require(_firstPhaseTotalSupply <= localTotalSupply, "Too many tokens for the first phase"); + require(_maxFirstPhaseReward <= _firstPhaseTotalSupply, "Reward for the first phase is too high"); + token = _token; - miningCoefficient = _miningCoefficient; secondsPerPeriod = _hoursPerPeriod.mul32(1 hours); - lockedPeriodsCoefficient = _lockedPeriodsCoefficient; - rewardedPeriods = _rewardedPeriods; + lockingDurationCoefficient1 = _lockingDurationCoefficient1; + lockingDurationCoefficient2 = _lockingDurationCoefficient2; + maxRewardedPeriods = _maxRewardedPeriods; + firstPhaseTotalSupply = _firstPhaseTotalSupply; + maxFirstPhaseReward = _maxFirstPhaseReward; totalSupply = uint128(localTotalSupply); + mintingCoefficient = localMintingCoefficient; } /** @@ -105,10 +133,12 @@ abstract contract Issuer is Upgradeable { */ function initialize(uint256 _reservedReward) external onlyOwner { require(currentMintingPeriod == 0); - token.safeTransferFrom(msg.sender, address(this), _reservedReward); + // Reserved reward must be sufficient for at least one period of the first phase + require(maxFirstPhaseReward <= _reservedReward); currentMintingPeriod = getCurrentPeriod(); currentPeriodSupply = totalSupply - uint128(_reservedReward); previousPeriodSupply = currentPeriodSupply; + token.safeTransferFrom(msg.sender, address(this), _reservedReward); emit Initialized(_reservedReward); } @@ -136,15 +166,29 @@ abstract contract Issuer is Upgradeable { previousPeriodSupply = currentPeriodSupply; currentMintingPeriod = _currentPeriod; } - uint128 currentReward = totalSupply - previousPeriodSupply; - //(totalSupply - currentSupply) * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k2) + uint256 currentReward; + uint256 coefficient; + + // first phase + // maxFirstPhaseReward * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k3) + if (previousPeriodSupply + maxFirstPhaseReward <= firstPhaseTotalSupply) { + currentReward = maxFirstPhaseReward; + coefficient = lockingDurationCoefficient2; + // second phase + // (totalSupply - currentSupply) * lockedValue * (k1 + allLockedPeriods) / (totalLockedValue * k2 * k3) + } else { + currentReward = totalSupply - previousPeriodSupply; + coefficient = mintingCoefficient; + } + uint256 allLockedPeriods = - AdditionalMath.min16(_allLockedPeriods, rewardedPeriods) + lockedPeriodsCoefficient; + AdditionalMath.min16(_allLockedPeriods, maxRewardedPeriods) + lockingDurationCoefficient1; amount = (uint256(currentReward) * _lockedValue * allLockedPeriods) / - (_totalLockedValue * miningCoefficient); + (_totalLockedValue * coefficient); // rounding the last reward + // TODO optimize uint256 maxReward = getReservedReward(); if (amount == 0) { amount = 1; diff --git a/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol b/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol index 1aba15ce5..353d3f7d3 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/StakingEscrow.sol @@ -37,7 +37,7 @@ interface WorkLockInterface { /** * @notice Contract holds and locks stakers tokens. * Each staker that locks their tokens will receive some compensation -* @dev |v4.1.1| +* @dev |v4.2.1| */ contract StakingEscrow is Issuer, IERC900History { @@ -143,10 +143,13 @@ contract StakingEscrow is Issuer, IERC900History { * @notice Constructor sets address of token contract and coefficients for mining * @param _token Token contract * @param _hoursPerPeriod Size of period in hours - * @param _miningCoefficient Mining coefficient + * @param _secondPhaseMintingCoefficient Minting coefficient for the second phase (k2) + * @param _lockingDurationCoefficient1 Numerator of he locking duration coefficient (k1) + * @param _lockingDurationCoefficient2 Denominator of the locking duration coefficient (k3) + * @param _maxRewardedPeriods Max periods that will be additionally rewarded + * @param _firstPhaseTotalSupply Total supply for the first phase + * @param _maxFirstPhaseReward Max possible reward for one period for all stakers in the first phase * @param _minLockedPeriods Min amount of periods during which tokens can be locked - * @param _lockedPeriodsCoefficient Locked blocks coefficient - * @param _rewardedPeriods Max periods that will be additionally rewarded * @param _minAllowableLockedTokens Min amount of tokens that can be locked * @param _maxAllowableLockedTokens Max amount of tokens that can be locked * @param _minWorkerPeriods Min amount of periods while a worker can't be changed @@ -155,9 +158,12 @@ contract StakingEscrow is Issuer, IERC900History { constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods, + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward, uint16 _minLockedPeriods, uint256 _minAllowableLockedTokens, uint256 _maxAllowableLockedTokens, @@ -168,9 +174,12 @@ contract StakingEscrow is Issuer, IERC900History { Issuer( _token, _hoursPerPeriod, - _miningCoefficient, - _lockedPeriodsCoefficient, - _rewardedPeriods + _secondPhaseMintingCoefficient, + _lockingDurationCoefficient1, + _lockingDurationCoefficient2, + _maxRewardedPeriods, + _firstPhaseTotalSupply, + _maxFirstPhaseReward ) { // constant `1` in the expression `_minLockedPeriods > 1` uses to simplify the `lock` method diff --git a/tests/blockchain/eth/contracts/base/test_issuer.py b/tests/blockchain/eth/contracts/base/test_issuer.py index a216ac790..8d8a76f05 100644 --- a/tests/blockchain/eth/contracts/base/test_issuer.py +++ b/tests/blockchain/eth/contracts/base/test_issuer.py @@ -40,35 +40,41 @@ def test_issuer(testerchain, token, deploy_contract): locked_periods_coefficient=10 ** 4, maximum_rewarded_periods=10 ** 4, hours_per_period=1) + locking_duration_coefficient_1 = economics.maximum_rewarded_periods + locking_duration_coefficient_2 = 2 * economics.maximum_rewarded_periods + first_phase_total_supply = INITIAL_SUPPLY + 1000 + max_first_phase_reward = (first_phase_total_supply - INITIAL_SUPPLY) // 3 - def calculate_reward(locked, total_locked, locked_periods): - return economics.erc20_reward_supply * locked * \ - (locked_periods + economics.locked_periods_coefficient) // \ + def calculate_first_phase_reward(locked, total_locked, locked_periods): + return max_first_phase_reward * locked * \ + (locked_periods + locking_duration_coefficient_1) // \ + (total_locked * locking_duration_coefficient_2) + + def calculate_second_phase_reward(locked, total_locked, locked_periods): + return (economics.erc20_reward_supply - INITIAL_SUPPLY) * locked * \ + (locked_periods + locking_duration_coefficient_1) // \ (total_locked * economics.staking_coefficient) creator = testerchain.client.accounts[0] - ursula = testerchain.client.accounts[1] + staker = testerchain.client.accounts[1] # Only token contract is allowed in Issuer constructor + # TODO update base economics + bad_args = dict(_token=staker, + _hoursPerPeriod=economics.hours_per_period, + _secondPhaseMintingCoefficient=economics.staking_coefficient // locking_duration_coefficient_2, + _lockingDurationCoefficient1=locking_duration_coefficient_1, + _lockingDurationCoefficient2=locking_duration_coefficient_2, + _maxRewardedPeriods=economics.maximum_rewarded_periods, + _firstPhaseTotalSupply=first_phase_total_supply, + _maxFirstPhaseReward=max_first_phase_reward) with pytest.raises((TransactionFailed, ValueError)): - 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 - ) + deploy_contract(contract_name='IssuerMock', **bad_args) # Creator deploys the issuer - 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 - ) + args = bad_args + args.update(_token=token.address) + issuer, _ = deploy_contract(contract_name='IssuerMock', **args) events = issuer.events.Initialized.createFilter(fromBlock='latest') # Give staker tokens for reward and initialize contract @@ -82,7 +88,7 @@ def test_issuer(testerchain, token, deploy_contract): # Only owner can initialize with pytest.raises((TransactionFailed, ValueError)): - tx = issuer.functions.initialize(0).transact({'from': ursula}) + tx = issuer.functions.initialize(0).transact({'from': staker}) testerchain.wait_for_receipt(tx) tx = issuer.functions.initialize(economics.erc20_reward_supply).transact({'from': creator}) testerchain.wait_for_receipt(tx) @@ -97,39 +103,194 @@ def test_issuer(testerchain, token, deploy_contract): tx = issuer.functions.initialize(0).transact({'from': creator}) testerchain.wait_for_receipt(tx) + # First phase + # Check result of minting tokens - tx = issuer.functions.testMint(0, 1000, 2000, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(0, 1000, 2000, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - reward = calculate_reward(1000, 2000, 0) - assert reward == token.functions.balanceOf(ursula).call() + reward = calculate_first_phase_reward(1000, 2000, 0) + assert calculate_second_phase_reward(1000, 2000, 0) != reward + assert token.functions.balanceOf(staker).call() == reward + assert token.functions.balanceOf(issuer.address).call() == balance - reward + + # The result must be more because of a different proportion of lockedValue and totalLockedValue + tx = issuer.functions.testMint(0, 500, 500, 0).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + reward += calculate_first_phase_reward(500, 500, 0) + assert token.functions.balanceOf(staker).call() == reward + assert token.functions.balanceOf(issuer.address).call() == balance - reward + + # The result must be more because of bigger value of allLockedPeriods + tx = issuer.functions.testMint(0, 500, 500, 10 ** 4).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + reward += calculate_first_phase_reward(500, 500, 10 ** 4) + assert token.functions.balanceOf(staker).call() == reward + assert token.functions.balanceOf(issuer.address).call() == balance - reward + + # The result is the same because allLockedPeriods more then specified coefficient _rewardedPeriods + period = issuer.functions.getCurrentPeriod().call() + tx = issuer.functions.testMint(period, 500, 500, 2 * 10 ** 4).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + reward += calculate_first_phase_reward(500, 500, 10 ** 4) + assert token.functions.balanceOf(staker).call() == reward + assert token.functions.balanceOf(issuer.address).call() == balance - reward + + # Still the first phase because minting period didn't change + assert issuer.functions.previousPeriodSupply().call() + max_first_phase_reward < first_phase_total_supply + assert issuer.functions.currentPeriodSupply().call() < first_phase_total_supply + assert issuer.functions.currentPeriodSupply().call() + max_first_phase_reward >= first_phase_total_supply + + tx = issuer.functions.testMint(0, 100, 500, 10 ** 4).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + reward += calculate_first_phase_reward(100, 500, 10 ** 4) + assert token.functions.balanceOf(staker).call() == reward + assert token.functions.balanceOf(issuer.address).call() == balance - reward + + # Second phase + assert issuer.functions.previousPeriodSupply().call() + max_first_phase_reward < first_phase_total_supply + assert issuer.functions.currentPeriodSupply().call() < first_phase_total_supply + assert issuer.functions.currentPeriodSupply().call() + max_first_phase_reward >= first_phase_total_supply + + # Check result of minting tokens + tx = issuer.functions.testMint(period + 1, 1000, 2000, 0).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + current_reward = calculate_second_phase_reward(1000, 2000, 0) + assert calculate_first_phase_reward(500, 500, 10 ** 4) != current_reward + reward += current_reward + assert reward == token.functions.balanceOf(staker).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}) + tx = issuer.functions.testMint(1, 500, 500, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - reward += calculate_reward(500, 500, 0) - assert reward == token.functions.balanceOf(ursula).call() + reward += calculate_second_phase_reward(500, 500, 0) + assert reward == token.functions.balanceOf(staker).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}) + tx = issuer.functions.testMint(1, 500, 500, 10 ** 4).transact({'from': staker}) testerchain.wait_for_receipt(tx) - reward += calculate_reward(500, 500, 10 ** 4) - assert reward == token.functions.balanceOf(ursula).call() + reward += calculate_second_phase_reward(500, 500, 10 ** 4) + assert reward == token.functions.balanceOf(staker).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}) + tx = issuer.functions.testMint(1, 500, 500, 2 * 10 ** 4).transact({'from': staker}) testerchain.wait_for_receipt(tx) - reward += calculate_reward(500, 500, 10 ** 4) - assert reward == token.functions.balanceOf(ursula).call() + reward += calculate_second_phase_reward(500, 500, 10 ** 4) + assert reward == token.functions.balanceOf(staker).call() assert balance - reward == token.functions.balanceOf(issuer.address).call() @pytest.mark.slow -def test_inflation_rate(testerchain, token, deploy_contract): +def test_issuance_first_phase(testerchain, token, deploy_contract): """ - Check decreasing of inflation rate after minting. + Checks stable issuance in the first phase + """ + + economics = BaseEconomics(initial_supply=INITIAL_SUPPLY, + total_supply=TOTAL_SUPPLY, + staking_coefficient=2 * 10 ** 35, + locked_periods_coefficient=1, + maximum_rewarded_periods=1, + hours_per_period=1) + + creator = testerchain.client.accounts[0] + staker = testerchain.client.accounts[1] + + # Creator deploys the contract + # TODO update base economics + first_phase_total_supply = INITIAL_SUPPLY + 1000 + max_first_phase_reward = (first_phase_total_supply - INITIAL_SUPPLY) // 5 + args = dict(_token=token.address, + _hoursPerPeriod=economics.hours_per_period, + _secondPhaseMintingCoefficient=economics.staking_coefficient // ( + 2 * economics.maximum_rewarded_periods), + _lockingDurationCoefficient1=economics.maximum_rewarded_periods, + _lockingDurationCoefficient2=2 * economics.maximum_rewarded_periods, + _maxRewardedPeriods=economics.maximum_rewarded_periods, + _firstPhaseTotalSupply=first_phase_total_supply, + _maxFirstPhaseReward=max_first_phase_reward) + issuer, _ = deploy_contract(contract_name='IssuerMock', **args) + + # Give staker tokens for reward and initialize contract + tx = token.functions.approve(issuer.address, economics.erc20_reward_supply).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + tx = issuer.functions.initialize(economics.erc20_reward_supply).transact({'from': creator}) + testerchain.wait_for_receipt(tx) + reward = issuer.functions.getReservedReward().call() + + # Mint some tokens and save result of minting + period = issuer.functions.getCurrentPeriod().call() + tx = issuer.functions.testMint(period + 1, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + one_period = token.functions.balanceOf(staker).call() + assert one_period == max_first_phase_reward + + # Inflation rate must be the same in all periods of the first phase + # Mint more tokens in the same period + tx = issuer.functions.testMint(period + 1, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 2 * one_period == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + + # Mint tokens in the next period + tx = issuer.functions.testMint(period + 2, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 3 * one_period == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + + # Mint tokens in the first period again + tx = issuer.functions.testMint(period + 1, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 4 * one_period == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + + # Mint tokens in the next period + tx = issuer.functions.testMint(period + 3, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 5 * one_period == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + + # Switch to the second phase + assert issuer.functions.previousPeriodSupply().call() < first_phase_total_supply + assert issuer.functions.currentPeriodSupply().call() == first_phase_total_supply + + tx = issuer.functions.testMint(period + 4, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 6 * one_period > token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + minted_amount_second_phase = token.functions.balanceOf(staker).call() - 5 * one_period + + # Return some tokens as a reward + # balance = token.functions.balanceOf(staker).call() TODO + reward = issuer.functions.getReservedReward().call() + amount_to_burn = 4 * one_period + minted_amount_second_phase + tx = token.functions.approve(issuer.address, amount_to_burn).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + tx = issuer.functions.burn(amount_to_burn).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert reward + amount_to_burn == issuer.functions.getReservedReward().call() + assert one_period == token.functions.balanceOf(staker).call() + + events = issuer.events.Burnt.createFilter(fromBlock=0).get_all_entries() + assert 1 == len(events) + event_args = events[0]['args'] + assert staker == event_args['sender'] + assert amount_to_burn == event_args['value'] + + # Switch back to the first phase + reward += amount_to_burn + tx = issuer.functions.testMint(period + 5, 1, 1, economics.maximum_rewarded_periods).transact({'from': staker}) + testerchain.wait_for_receipt(tx) + assert 2 * one_period == token.functions.balanceOf(staker).call() + assert reward - one_period == issuer.functions.getReservedReward().call() + + +@pytest.mark.slow +def test_issuance_second_phase(testerchain, token, deploy_contract): + """ + Check for decreasing of issuance after minting in the second phase. During one period inflation rate must be the same """ @@ -141,17 +302,20 @@ def test_inflation_rate(testerchain, token, deploy_contract): hours_per_period=1) creator = testerchain.client.accounts[0] - ursula = testerchain.client.accounts[1] + staker = testerchain.client.accounts[1] # Creator deploys the contract - 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 - ) + # TODO update base economics + args = dict(_token=token.address, + _hoursPerPeriod=economics.hours_per_period, + _secondPhaseMintingCoefficient=economics.staking_coefficient // ( + 2 * economics.maximum_rewarded_periods), + _lockingDurationCoefficient1=economics.maximum_rewarded_periods, + _lockingDurationCoefficient2=2 * economics.maximum_rewarded_periods, + _maxRewardedPeriods=economics.maximum_rewarded_periods, + _firstPhaseTotalSupply=0, + _maxFirstPhaseReward=0) + issuer, _ = deploy_contract(contract_name='IssuerMock', **args) # Give staker tokens for reward and initialize contract tx = token.functions.approve(issuer.address, economics.erc20_reward_supply).transact({'from': creator}) @@ -162,58 +326,58 @@ def test_inflation_rate(testerchain, token, deploy_contract): # Mint some tokens and save result of minting period = issuer.functions.getCurrentPeriod().call() - tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - one_period = token.functions.balanceOf(ursula).call() + one_period = token.functions.balanceOf(staker).call() # Mint more tokens in the same period, inflation rate must be the same as in previous minting - tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - assert 2 * one_period == token.functions.balanceOf(ursula).call() - assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() + assert 2 * one_period == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() # Mint tokens in the next period, inflation rate must be lower than in previous minting - tx = issuer.functions.testMint(period + 2, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 2, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - assert 3 * one_period > token.functions.balanceOf(ursula).call() - assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() - minted_amount = token.functions.balanceOf(ursula).call() - 2 * one_period + assert 3 * one_period > token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() + minted_amount = token.functions.balanceOf(staker).call() - 2 * one_period # Mint tokens in the first period again, inflation rate must be the same as in previous minting # but can't be equals as in first minting because rate can't be increased - tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - assert 2 * one_period + 2 * minted_amount == token.functions.balanceOf(ursula).call() - assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() + assert 2 * one_period + 2 * minted_amount == token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() # Mint tokens in the next period, inflation rate must be lower than in previous minting - tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - assert 2 * one_period + 3 * minted_amount > token.functions.balanceOf(ursula).call() - assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() + assert 2 * one_period + 3 * minted_amount > token.functions.balanceOf(staker).call() + assert reward - token.functions.balanceOf(staker).call() == issuer.functions.getReservedReward().call() # Return some tokens as a reward - balance = token.functions.balanceOf(ursula).call() + balance = token.functions.balanceOf(staker).call() reward = issuer.functions.getReservedReward().call() amount_to_burn = 2 * one_period + 2 * minted_amount - tx = token.functions.transfer(ursula, amount_to_burn).transact({'from': creator}) + tx = token.functions.transfer(staker, amount_to_burn).transact({'from': creator}) testerchain.wait_for_receipt(tx) - tx = token.functions.approve(issuer.address, amount_to_burn).transact({'from': ursula}) + tx = token.functions.approve(issuer.address, amount_to_burn).transact({'from': staker}) testerchain.wait_for_receipt(tx) - tx = issuer.functions.burn(amount_to_burn).transact({'from': ursula}) + tx = issuer.functions.burn(amount_to_burn).transact({'from': staker}) testerchain.wait_for_receipt(tx) assert reward + amount_to_burn == issuer.functions.getReservedReward().call() events = issuer.events.Burnt.createFilter(fromBlock=0).get_all_entries() assert 1 == len(events) event_args = events[0]['args'] - assert ursula == event_args['sender'] + assert staker == event_args['sender'] assert amount_to_burn == event_args['value'] # Rate will be increased because some tokens were returned - tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': ursula}) + tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': staker}) testerchain.wait_for_receipt(tx) - assert balance + one_period == token.functions.balanceOf(ursula).call() + assert balance + one_period == token.functions.balanceOf(staker).call() assert reward + one_period + 2 * minted_amount == issuer.functions.getReservedReward().call() @@ -226,9 +390,12 @@ def test_upgrading(testerchain, token, deploy_contract): contract_name='IssuerMock', _token=token.address, _hoursPerPeriod=1, - _miningCoefficient=2, - _lockedPeriodsCoefficient=1, - _rewardedPeriods=1 + _secondPhaseMintingCoefficient=1, + _lockingDurationCoefficient1=1, + _lockingDurationCoefficient2=2, + _maxRewardedPeriods=1, + _firstPhaseTotalSupply=1, + _maxFirstPhaseReward=1 ) dispatcher, _ = deploy_contract('Dispatcher', contract_library_v1.address) @@ -237,9 +404,12 @@ def test_upgrading(testerchain, token, deploy_contract): contract_name='IssuerV2Mock', _token=token.address, _hoursPerPeriod=2, - _miningCoefficient=4, - _lockedPeriodsCoefficient=2, - _rewardedPeriods=2 + _secondPhaseMintingCoefficient=2, + _lockingDurationCoefficient1=2, + _lockingDurationCoefficient2=4, + _maxRewardedPeriods=2, + _firstPhaseTotalSupply=2, + _maxFirstPhaseReward=2 ) contract = testerchain.client.get_contract( abi=contract_library_v2.abi, @@ -262,14 +432,17 @@ def test_upgrading(testerchain, token, deploy_contract): # Upgrade to the second version, check new and old values of variables period = contract.functions.currentMintingPeriod().call() - assert 2 == contract.functions.miningCoefficient().call() + assert 2 == contract.functions.mintingCoefficient().call() tx = dispatcher.functions.upgrade(contract_library_v2.address).transact({'from': creator}) testerchain.wait_for_receipt(tx) assert contract_library_v2.address == dispatcher.functions.target().call() - assert 4 == contract.functions.miningCoefficient().call() + assert 8 == contract.functions.mintingCoefficient().call() assert 2 * 3600 == contract.functions.secondsPerPeriod().call() - assert 2 == contract.functions.lockedPeriodsCoefficient().call() - assert 2 == contract.functions.rewardedPeriods().call() + assert 2 == contract.functions.lockingDurationCoefficient1().call() + assert 4 == contract.functions.lockingDurationCoefficient2().call() + assert 2 == contract.functions.maxRewardedPeriods().call() + assert 2 == contract.functions.firstPhaseTotalSupply().call() + assert 2 == contract.functions.maxFirstPhaseReward().call() assert period == contract.functions.currentMintingPeriod().call() assert TOTAL_SUPPLY == contract.functions.totalSupply().call() # Check method from new ABI @@ -298,10 +471,13 @@ def test_upgrading(testerchain, token, deploy_contract): testerchain.wait_for_receipt(tx) # Check old values assert contract_library_v1.address == dispatcher.functions.target().call() - assert 2 == contract.functions.miningCoefficient().call() + assert 2 == contract.functions.mintingCoefficient().call() assert 3600 == contract.functions.secondsPerPeriod().call() - assert 1 == contract.functions.lockedPeriodsCoefficient().call() - assert 1 == contract.functions.rewardedPeriods().call() + assert 1 == contract.functions.lockingDurationCoefficient1().call() + assert 2 == contract.functions.lockingDurationCoefficient2().call() + assert 1 == contract.functions.maxRewardedPeriods().call() + assert 1 == contract.functions.firstPhaseTotalSupply().call() + assert 1 == contract.functions.maxFirstPhaseReward().call() assert period == contract.functions.currentMintingPeriod().call() assert TOTAL_SUPPLY == contract.functions.totalSupply().call() # After rollback can't use new ABI diff --git a/tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol b/tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol index db3382019..3754d6ce2 100644 --- a/tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol +++ b/tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol @@ -14,17 +14,23 @@ contract IssuerMock is Issuer { constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward ) public Issuer( _token, _hoursPerPeriod, - _miningCoefficient, - _lockedPeriodsCoefficient, - _rewardedPeriods + _secondPhaseMintingCoefficient, + _lockingDurationCoefficient1, + _lockingDurationCoefficient2, + _maxRewardedPeriods, + _firstPhaseTotalSupply, + _maxFirstPhaseReward ) { } @@ -70,17 +76,23 @@ contract IssuerV2Mock is Issuer { constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward ) public Issuer( _token, _hoursPerPeriod, - _miningCoefficient, - _lockedPeriodsCoefficient, - _rewardedPeriods + _secondPhaseMintingCoefficient, + _lockingDurationCoefficient1, + _lockingDurationCoefficient2, + _maxRewardedPeriods, + _firstPhaseTotalSupply, + _maxFirstPhaseReward ) { } diff --git a/tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol b/tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol index 957d960e1..5fe1359fa 100644 --- a/tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol +++ b/tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol @@ -13,9 +13,12 @@ contract StakingEscrowBad is StakingEscrow { constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods, + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward, uint16 _minLockedPeriods, uint256 _minAllowableLockedTokens, uint256 _maxAllowableLockedTokens, @@ -26,9 +29,12 @@ contract StakingEscrowBad is StakingEscrow { StakingEscrow( _token, _hoursPerPeriod, - _miningCoefficient, - _lockedPeriodsCoefficient, - _rewardedPeriods, + _secondPhaseMintingCoefficient, + _lockingDurationCoefficient1, + _lockingDurationCoefficient2, + _maxRewardedPeriods, + _firstPhaseTotalSupply, + _maxFirstPhaseReward, _minLockedPeriods, _minAllowableLockedTokens, _maxAllowableLockedTokens, @@ -53,9 +59,12 @@ contract StakingEscrowV2Mock is StakingEscrow { constructor( NuCypherToken _token, uint32 _hoursPerPeriod, - uint256 _miningCoefficient, - uint256 _lockedPeriodsCoefficient, - uint16 _rewardedPeriods, + uint256 _secondPhaseMintingCoefficient, + uint256 _lockingDurationCoefficient1, + uint256 _lockingDurationCoefficient2, + uint16 _maxRewardedPeriods, + uint256 _firstPhaseTotalSupply, + uint256 _maxFirstPhaseReward, uint16 _minLockedPeriods, uint256 _minAllowableLockedTokens, uint256 _maxAllowableLockedTokens, @@ -67,9 +76,12 @@ contract StakingEscrowV2Mock is StakingEscrow { StakingEscrow( _token, _hoursPerPeriod, - _miningCoefficient, - _lockedPeriodsCoefficient, - _rewardedPeriods, + _secondPhaseMintingCoefficient, + _lockingDurationCoefficient1, + _lockingDurationCoefficient2, + _maxRewardedPeriods, + _firstPhaseTotalSupply, + _maxFirstPhaseReward, _minLockedPeriods, _minAllowableLockedTokens, _maxAllowableLockedTokens,