mirror of https://github.com/nucypher/nucypher.git
Issuer: two-phase issuance
parent
aac486afbb
commit
c958b52570
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue