diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 5da8d6f7..eeba1909 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -74,10 +74,10 @@ contract Depository is IErrorsTokenomics { address public owner; // Individual bond counter // We assume that the number of bonds will not be bigger than the number of seconds - uint32 bondCounter; + uint32 public bondCounter; // Bond product counter // We assume that the number of products will not be bigger than the number of seconds - uint32 productCounter; + uint32 public productCounter; // Reentrancy lock uint8 internal _locked; diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 8cabf142..75345913 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -39,11 +39,8 @@ import "./interfaces/IVotingEscrow.sol"; */ // Structure for component / agent point with tokenomics-related statistics -// The size of the struct is 96 * 2 + 32 + 8 * 3 = 248 bits (1 full slot) +// The size of the struct is 96 + 32 + 8 * 3 = 152 bits (1 full slot) struct UnitPoint { - // Summation of all the relative ETH donations accumulated by each component / agent in a service - // Even if the ETH inflation rate is 5% per year, it would take 130+ years to reach 2^96 - 1 of ETH total supply - uint96 sumUnitDonationsETH; // Summation of all the relative OLAS top-ups accumulated by each component / agent in a service // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 sumUnitTopUpsOLAS; @@ -134,9 +131,12 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { event DispenserUpdated(address indexed dispenser); event EpochLengthUpdated(uint256 epochLen); event EffectiveBondUpdated(uint256 effectiveBond); - event TokenomicsParametersUpdated(uint256 devsPerCapital, uint256 epsilonRate, uint256 epochLen, uint256 veOLASThreshold); - event IncentiveFractionsUpdated(uint256 rewardComponentFraction, uint256 rewardAgentFraction, - uint256 maxBondFraction, uint256 topUpComponentFraction, uint256 topUpAgentFraction); + event TokenomicsParametersUpdateRequested(uint256 indexed epochNumber, uint256 devsPerCapital, uint256 epsilonRate, + uint256 epochLen, uint256 veOLASThreshold, uint256 componentWeight, uint256 agentWeight); + event TokenomicsParametersUpdated(uint256 indexed epochNumber); + event IncentiveFractionsUpdateRequested(uint256 indexed epochNumber, uint256 rewardComponentFraction, + uint256 rewardAgentFraction, uint256 maxBondFraction, uint256 topUpComponentFraction, uint256 topUpAgentFraction); + event IncentiveFractionsUpdated(uint256 indexed epochNumber); event ComponentRegistryUpdated(address indexed componentRegistry); event AgentRegistryUpdated(address indexed agentRegistry); event ServiceRegistryUpdated(address indexed serviceRegistry); @@ -189,15 +189,21 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Current year number // This number is enough for the next 255 years uint8 public currentYear; - // maxBond-related parameter change locker - uint8 public lockMaxBond; + // Tokenomics parameters change request flag + bytes1 public tokenomicsParametersUpdated; // Reentrancy lock uint8 internal _locked; // Component Registry address public componentRegistry; + // Epoch length in seconds that will be set in the next epoch + // By design, the epoch length cannot be practically bigger than one year, or 31_536_000 seconds + uint32 public nextEpochLen; // Agent Registry address public agentRegistry; + // veOLAS threshold for top-ups that will be set in the next epoch + // This number cannot be practically bigger than the number of OLAS tokens + uint96 public nextVeOLASThreshold; // Service Registry address public serviceRegistry; @@ -284,12 +290,11 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { _locked = 1; epsilonRate = 1e17; veOLASThreshold = 5_000e18; - lockMaxBond = 1; // Check that the epoch length has at least a practical minimal value // TODO Decide on the final minimal value if (uint32(_epochLen) < MIN_EPOCH_LENGTH) { - revert AmountLowerThan(_epochLen, MIN_EPOCH_LENGTH); + revert Overflow(MIN_EPOCH_LENGTH, _epochLen); } // Assign other passed variables @@ -466,36 +471,13 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { emit DonatorBlacklistUpdated(_donatorBlacklist); } - /// @dev Checks if the maxBond update is within allowed limits of the effectiveBond, and adjusts maxBond and effectiveBond. - /// @param nextMaxBond Proposed next epoch maxBond. - function _adjustMaxBond(uint256 nextMaxBond) internal { - uint256 curMaxBond = maxBond; - uint256 curEffectiveBond = effectiveBond; - // If the new epochLen is shorter than the current one, the current maxBond is bigger than the proposed nextMaxBond - if (curMaxBond > nextMaxBond) { - // Get the difference of the maxBond - uint256 delta = curMaxBond - nextMaxBond; - // Update the value for the effectiveBond if there is room for it - if (curEffectiveBond > delta) { - curEffectiveBond -= delta; - } else { - // Otherwise effectiveBond cannot be reduced further, and the current epochLen cannot be shortened - revert RejectMaxBondAdjustment(curEffectiveBond, delta); - } - } else { - // The new epochLen is longer than the current one, and thus we must add the difference to the effectiveBond - curEffectiveBond += nextMaxBond - curMaxBond; - } - // Update maxBond and effectiveBond based on their calculations - maxBond = uint96(nextMaxBond); - effectiveBond = uint96(curEffectiveBond); - } - /// @dev Changes tokenomics parameters. /// @notice Parameter values are not updated for those that are passed as zero. /// @param _devsPerCapital Number of valuable devs can be paid per units of capital per epoch. /// @param _epsilonRate Epsilon rate that contributes to the interest rate value. /// @param _epochLen New epoch length. + /// @param _componentWeight Component weight for code unit calculations. + /// @param _agentWeight Agent weight for code unit calculations. ///#if_succeeds {:msg "ep is correct endTime"} epochCounter > 1 ///==> mapEpochTokenomics[epochCounter - 1].epochPoint.endTime > mapEpochTokenomics[epochCounter - 2].epochPoint.endTime; ///#if_succeeds {:msg "epochLen"} old(_epochLen > MIN_EPOCH_LENGTH && _epochLen <= type(uint32).max && epochLen != _epochLen) ==> epochLen == _epochLen; @@ -510,7 +492,9 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { uint256 _devsPerCapital, uint256 _epsilonRate, uint256 _epochLen, - uint256 _veOLASThreshold + uint256 _veOLASThreshold, + uint256 _componentWeight, + uint256 _agentWeight ) external { // Check for the contract ownership @@ -518,15 +502,18 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { revert OwnerOnly(msg.sender, owner); } + uint256 eCounter = epochCounter; + // devsPerCapital is the part of the IDF calculation and thus its change will be accounted for in the next epoch if (uint32(_devsPerCapital) > 0) { - mapEpochTokenomics[epochCounter].epochPoint.devsPerCapital = uint32(_devsPerCapital); + mapEpochTokenomics[eCounter].epochPoint.devsPerCapital = uint32(_devsPerCapital); } else { // This is done in order not to pass incorrect parameters into the event - _devsPerCapital = mapEpochTokenomics[epochCounter].epochPoint.devsPerCapital; + _devsPerCapital = mapEpochTokenomics[eCounter].epochPoint.devsPerCapital; } // Check the epsilonRate value for idf to fit in its size // 2^64 - 1 < 18.5e18, idf is equal at most 1 + epsilonRate < 18e18, which fits in the variable size + // epsilonRate is the part of the IDF calculation and thus its change will be accounted for in the next epoch if (_epsilonRate > 0 && _epsilonRate <= 17e18) { epsilonRate = uint64(_epsilonRate); } else { @@ -534,44 +521,37 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { } // Check for the epochLen value to change - uint256 oldEpochLen = epochLen; - if (uint32(_epochLen) >= MIN_EPOCH_LENGTH && oldEpochLen != _epochLen) { - // Check if the year change is ongoing in the current epoch, and thus maxBond cannot be changed - if (lockMaxBond == 2) { - revert MaxBondUpdateLocked(); - } - - // Check if the bigger proposed length of the epoch end time results in a scenario when the year changes - if (_epochLen > oldEpochLen) { - // End time of the last epoch - uint256 lastEpochEndTime = mapEpochTokenomics[epochCounter - 1].epochPoint.endTime; - // Actual year of the time when the epoch is going to finish with the proposed epoch length - uint256 numYears = (lastEpochEndTime + _epochLen - timeLaunch) / ONE_YEAR; - // Check if the year is going to change - if (numYears > currentYear) { - revert MaxBondUpdateLocked(); - } - } - - // Calculate next maxBond based on the proposed epochLen - uint256 nextMaxBond = (inflationPerSecond * mapEpochTokenomics[epochCounter].epochPoint.maxBondFraction * _epochLen) / 100; - // Adjust maxBond and effectiveBond, if they are within the allowed limits - _adjustMaxBond(nextMaxBond); - - // Update the epochLen - epochLen = uint32(_epochLen); - emit EpochLengthUpdated(_epochLen); + if (uint32(_epochLen) >= MIN_EPOCH_LENGTH) { + nextEpochLen = uint32(_epochLen); } else { _epochLen = epochLen; } + // Adjust veOLAS threshold for the next epoch if (uint96(_veOLASThreshold) > 0) { - veOLASThreshold = uint96(_veOLASThreshold); + nextVeOLASThreshold = uint96(_veOLASThreshold); } else { _veOLASThreshold = veOLASThreshold; } - emit TokenomicsParametersUpdated(_devsPerCapital, _epsilonRate, _epochLen, _veOLASThreshold); + // Adjust unit weights + // componentWeight is the part of the IDF calculation and thus its change will be accounted for in the next epoch + if (uint8(_componentWeight) > 0) { + mapEpochTokenomics[eCounter].unitPoints[0].unitWeight = uint8(_componentWeight); + } else { + _componentWeight = mapEpochTokenomics[eCounter].unitPoints[0].unitWeight; + } + // agentWeight is the part of the IDF calculation and thus its change will be accounted for in the next epoch + if (uint8(_agentWeight) > 0) { + mapEpochTokenomics[eCounter].unitPoints[1].unitWeight = uint8(_agentWeight); + } else { + _agentWeight = mapEpochTokenomics[eCounter].unitPoints[1].unitWeight; + } + + // Set the flag that tokenomics parameters are requested to be updated (1st bit is set to one) + tokenomicsParametersUpdated = tokenomicsParametersUpdated | 0x01; + emit TokenomicsParametersUpdateRequested(eCounter + 1, _devsPerCapital, _epsilonRate, _epochLen, + _veOLASThreshold, _componentWeight, _agentWeight); } /// @dev Sets incentive parameter fractions. @@ -607,34 +587,23 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { revert WrongAmount(_maxBondFraction + _topUpComponentFraction + _topUpAgentFraction, 100); } - TokenomicsPoint storage tp = mapEpochTokenomics[epochCounter]; + // All the adjustments will be accounted for in the next epoch + uint256 eCounter = epochCounter + 1; + TokenomicsPoint storage tp = mapEpochTokenomics[eCounter]; // 0 stands for components and 1 for agents tp.unitPoints[0].rewardUnitFraction = uint8(_rewardComponentFraction); tp.unitPoints[1].rewardUnitFraction = uint8(_rewardAgentFraction); // Rewards are always distributed in full: the leftovers will be allocated to treasury tp.epochPoint.rewardTreasuryFraction = uint8(100 - _rewardComponentFraction - _rewardAgentFraction); - // Check if the maxBondFraction changes - uint256 oldMaxBondFraction = tp.epochPoint.maxBondFraction; - if (oldMaxBondFraction != _maxBondFraction) { - // Epoch with the year change is ongoing, and maxBond cannot be changed - if (lockMaxBond == 2) { - revert MaxBondUpdateLocked(); - } - - // Calculate next maxBond based on the proposed maxBondFraction - uint256 nextMaxBond = (inflationPerSecond * _maxBondFraction * epochLen) / 100; - // Adjust maxBond and effectiveBond, if they are within the allowed limits - _adjustMaxBond(nextMaxBond); - - // Update the maxBondFraction - tp.epochPoint.maxBondFraction = uint8(_maxBondFraction); - } + tp.epochPoint.maxBondFraction = uint8(_maxBondFraction); tp.unitPoints[0].topUpUnitFraction = uint8(_topUpComponentFraction); tp.unitPoints[1].topUpUnitFraction = uint8(_topUpAgentFraction); - emit IncentiveFractionsUpdated(_rewardComponentFraction, _rewardAgentFraction, _maxBondFraction, - _topUpComponentFraction, _topUpAgentFraction); + // Set the flag that incentive fractions are requested to be updated (2nd bit is set to one) + tokenomicsParametersUpdated = tokenomicsParametersUpdated | 0x02; + emit IncentiveFractionsUpdateRequested(eCounter, _rewardComponentFraction, _rewardAgentFraction, + _maxBondFraction, _topUpComponentFraction, _topUpAgentFraction); } /// @dev Reserves OLAS amount from the effective bond to be minted during a bond program. @@ -682,14 +651,12 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Get the overall amount of component rewards for the component's last epoch // The pendingRelativeReward can be zero if the rewardUnitFraction was zero in the first place // Note that if the rewardUnitFraction is set to zero at the end of epoch, the whole pending reward will be zero - // reward = (pendingRelativeReward * totalDonationsETH * rewardUnitFraction) / (100 * sumUnitDonationsETH) + // reward = (pendingRelativeReward * rewardUnitFraction) / 100 uint256 totalIncentives = mapUnitIncentives[unitType][unitId].pendingRelativeReward; if (totalIncentives > 0) { - totalIncentives *= mapEpochTokenomics[epochNum].epochPoint.totalDonationsETH; totalIncentives *= mapEpochTokenomics[epochNum].unitPoints[unitType].rewardUnitFraction; - uint256 sumUnitIncentives = mapEpochTokenomics[epochNum].unitPoints[unitType].sumUnitDonationsETH * 100; // Add to the final reward for the last epoch - totalIncentives = mapUnitIncentives[unitType][unitId].reward + totalIncentives / sumUnitIncentives; + totalIncentives = mapUnitIncentives[unitType][unitId].reward + totalIncentives / 100; mapUnitIncentives[unitType][unitId].reward = uint96(totalIncentives); // Setting pending reward to zero mapUnitIncentives[unitType][unitId].pendingRelativeReward = 0; @@ -705,6 +672,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { totalIncentives *= mapEpochTokenomics[epochNum].epochPoint.totalTopUpsOLAS; totalIncentives *= mapEpochTokenomics[epochNum].unitPoints[unitType].topUpUnitFraction; uint256 sumUnitIncentives = mapEpochTokenomics[epochNum].unitPoints[unitType].sumUnitTopUpsOLAS * 100; + //uint256 sumUnitIncentives = mapEpochTokenomics[epochNum].epochPoint.sumUnitTopUpsOLAS * 100; totalIncentives = mapUnitIncentives[unitType][unitId].topUp + totalIncentives / sumUnitIncentives; mapUnitIncentives[unitType][unitId].topUp = uint96(totalIncentives); // Setting pending top-up to zero @@ -732,8 +700,6 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { uint256 numServices = serviceIds.length; // Loop over service Ids to calculate their partial UCFu-s for (uint256 i = 0; i < numServices; ++i) { - uint96 amount = uint96(amounts[i]); - // Check if the service owner stakes enough OLAS for its components / agents to get a top-up // If both component and agent owner top-up fractions are zero, there is no need to call external contract // functions to check each service owner veOLAS balance @@ -755,6 +721,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { } // Record amounts data only if at least one incentive unit fraction is not zero if (incentiveFlags[unitType] || incentiveFlags[unitType + 2]) { + // The amount has to be adjusted for the number of units in the service + uint96 amount = uint96(amounts[i] / numServiceUnits); // Accumulate amounts for each unit Id for (uint256 j = 0; j < numServiceUnits; ++j) { // Get the last epoch number the incentives were accumulated for @@ -774,7 +742,6 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Sum the relative amounts for the corresponding components / agents if (incentiveFlags[unitType]) { mapUnitIncentives[unitType][serviceUnitIds[j]].pendingRelativeReward += amount; - mapEpochTokenomics[curEpoch].unitPoints[unitType].sumUnitDonationsETH += amount; } // If eligible, add relative top-up weights in the form of donation amounts. // These weights will represent the fraction of top-ups for each component / agent relative @@ -802,6 +769,10 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { } } } + // TODO Explore this path of spending less gas for the OLAS top-ups computation +// if (topUpEligible) { +// mapEpochTokenomics[curEpoch].epochPoint.sumUnitTopUpsOLAS += uint96(amounts[i]); +// } } } @@ -813,8 +784,6 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { /// @param donationETH Overall service donation amount in ETH. ///if_succeeds {:msg "totalDonationsETH can only increase"} old(mapEpochTokenomics[epochCounter].epochPoint.totalDonationsETH) + donationETH <= type(uint96).max ///==> mapEpochTokenomics[epochCounter].epochPoint.totalDonationsETH == old(mapEpochTokenomics[epochCounter].epochPoint.totalDonationsETH) + donationETH; - ///if_succeeds {:msg "sumUnitDonationsETH for components can only increase"} mapEpochTokenomics[epochCounter].unitPoints[0].sumUnitDonationsETH >= old(mapEpochTokenomics[epochCounter].unitPoints[0].sumUnitDonationsETH); - ///if_succeeds {:msg "sumUnitDonationsETH for agents can only increase"} mapEpochTokenomics[epochCounter].unitPoints[1].sumUnitDonationsETH >= old(mapEpochTokenomics[epochCounter].unitPoints[1].sumUnitDonationsETH); ///if_succeeds {:msg "sumUnitTopUpsOLAS for components can only increase"} mapEpochTokenomics[epochCounter].unitPoints[0].sumUnitTopUpsOLAS >= old(mapEpochTokenomics[epochCounter].unitPoints[0].sumUnitTopUpsOLAS); ///if_succeeds {:msg "sumUnitTopUpsOLAS for agents can only increase"} mapEpochTokenomics[epochCounter].unitPoints[1].sumUnitTopUpsOLAS >= old(mapEpochTokenomics[epochCounter].unitPoints[1].sumUnitTopUpsOLAS); ///#if_succeeds {:msg "numNewOwners can only increase"} mapEpochTokenomics[epochCounter].epochPoint.numNewOwners >= old(mapEpochTokenomics[epochCounter].epochPoint.numNewOwners); @@ -884,6 +853,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { uint256 prevEpochTime = mapEpochTokenomics[epochCounter - 1].epochPoint.endTime; uint256 diffNumSeconds = block.timestamp - prevEpochTime; uint256 curEpochLen = epochLen; + // Check if the time passed since the last epoch end time is bigger than the specified epoch length if (diffNumSeconds < curEpochLen) { return false; } @@ -904,17 +874,14 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // The actual inflation per epoch considering that it is settled not in the exact epochLen time, but a bit later uint256 inflationPerEpoch; - // Get the maxBond that was credited to effectiveBond during this settled epoch - // If the year changes, the maxBond for the next epoch is updated in the condition below and will be used - // later when the effectiveBond is updated for the next epoch - uint256 curMaxBond = maxBond; + // Record the current inflation per second + uint256 curInflationPerSecond = inflationPerSecond; // Current year uint256 numYears = (block.timestamp - timeLaunch) / ONE_YEAR; - // There amounts for the yearly inflation change from year to year, so if the year changes in the middle + // Amounts for the yearly inflation change from year to year, so if the year changes in the middle // of the epoch, it is necessary to adjust the epoch inflation numbers to account for the year change if (numYears > currentYear) { // Calculate remainder of inflation for the passing year - uint256 curInflationPerSecond = inflationPerSecond; // End of the year timestamp uint256 yearEndTime = timeLaunch + numYears * ONE_YEAR; // Initial inflation per epoch during the end of the year minus previous epoch timestamp @@ -923,15 +890,14 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { curInflationPerSecond = getInflationForYear(numYears) / ONE_YEAR; // Add the remainder of inflation amount for this epoch based on a new inflation per second ratio inflationPerEpoch += (block.timestamp - yearEndTime) * curInflationPerSecond; - // Update the maxBond value for the next epoch after the year changes - maxBond = uint96(curInflationPerSecond * curEpochLen * tp.epochPoint.maxBondFraction) / 100; // Updating state variables inflationPerSecond = uint96(curInflationPerSecond); currentYear = uint8(numYears); - // maxBond lock is released and can be changed starting from the new epoch - lockMaxBond = 1; + // Set the tokenomics parameters flag such that the maxBond is correctly updated below (3rd bit is set to one) + tokenomicsParametersUpdated = tokenomicsParametersUpdated | 0x04; } else { - inflationPerEpoch = inflationPerSecond * diffNumSeconds; + // Inflation per epoch is equal to the inflation per second multiplied by the actual time of the epoch + inflationPerEpoch = curInflationPerSecond * diffNumSeconds; } // Bonding and top-ups in OLAS are recalculated based on the inflation schedule per epoch @@ -939,6 +905,11 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { tp.epochPoint.totalTopUpsOLAS = uint96(inflationPerEpoch); incentives[4] = (inflationPerEpoch * tp.epochPoint.maxBondFraction) / 100; + // Get the maxBond that was credited to effectiveBond during this settled epoch + // If the year changes, the maxBond for the next epoch is updated in the condition below and will be used + // later when the effectiveBond is updated for the next epoch + uint256 curMaxBond = maxBond; + // Effective bond accumulates bonding leftovers from previous epochs (with the last max bond value set) // It is given the value of the maxBond for the next epoch as a credit // The difference between recalculated max bond per epoch and maxBond value must be reflected in effectiveBond, @@ -951,6 +922,50 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { effectiveBond = uint96(incentives[4]); } + // Get the tokenomics point of the next epoch + TokenomicsPoint storage nextPoint = mapEpochTokenomics[eCounter + 1]; + // TODO unit weights and devsPerCapital move to just contract state variables + // Copy unit weights to the next epoch point + for (uint256 i = 0; i < 2; ++i) { + nextPoint.unitPoints[i].unitWeight = tp.unitPoints[i].unitWeight; + } + nextPoint.epochPoint.devsPerCapital = tp.epochPoint.devsPerCapital; + // Update incentive fractions for the next epoch if they were requested by the changeIncentiveFractions() function + // Check if the second bit is set to one + if (tokenomicsParametersUpdated & 0x02 == 0x02) { + // Confirm the change of incentive fractions + emit IncentiveFractionsUpdated(eCounter + 1); + } else { + // Copy current tokenomics point into the next one such that it has necessary tokenomics parameters + for (uint256 i = 0; i < 2; ++i) { + nextPoint.unitPoints[i].topUpUnitFraction = tp.unitPoints[i].topUpUnitFraction; + nextPoint.unitPoints[i].rewardUnitFraction = tp.unitPoints[i].rewardUnitFraction; + } + nextPoint.epochPoint.rewardTreasuryFraction = tp.epochPoint.rewardTreasuryFraction; + nextPoint.epochPoint.maxBondFraction = tp.epochPoint.maxBondFraction; + } + // Update parameters for the next epoch, if changes were requested by the changeTokenomicsParameters() function + // Check if the second bit is set to one + if (tokenomicsParametersUpdated & 0x01 == 0x01) { + // Update epoch length and set the next value back to zero + if (nextEpochLen > 0) { + curEpochLen = nextEpochLen; + epochLen = uint32(curEpochLen); + nextEpochLen = 0; + } + + // Update veOLAS threshold and set the next value back to zero + if (nextVeOLASThreshold > 0) { + veOLASThreshold = nextVeOLASThreshold; + nextVeOLASThreshold = 0; + } + + // Confirm the change of tokenomics parameters + emit TokenomicsParametersUpdated(eCounter + 1); + } + // Record settled epoch timestamp + tp.epochPoint.endTime = uint32(block.timestamp); + // Adjust max bond value if the next epoch is going to be the year change epoch // Note that this computation happens before the epoch that is triggered in the next epoch (the code above) when // the actual year will change @@ -958,22 +973,27 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Account for the year change to adjust the max bond if (numYears > currentYear) { // Calculate remainder of inflation for the passing year - uint256 curInflationPerSecond = inflationPerSecond; // End of the year timestamp uint256 yearEndTime = timeLaunch + numYears * ONE_YEAR; - // Calculate the max bond value until the end of the year - curMaxBond = ((yearEndTime - block.timestamp) * curInflationPerSecond * tp.epochPoint.maxBondFraction) / 100; + // Calculate the inflation per epoch value until the end of the year + inflationPerEpoch = (yearEndTime - block.timestamp) * curInflationPerSecond; // Recalculate the inflation per second based on the new inflation for the current year curInflationPerSecond = getInflationForYear(numYears) / ONE_YEAR; - // Add the remainder of max bond amount for the next epoch based on a new inflation per second ratio - curMaxBond += ((block.timestamp + curEpochLen - yearEndTime) * curInflationPerSecond * tp.epochPoint.maxBondFraction) / 100; + // Add the remainder of the inflation for the next epoch based on a new inflation per second ratio + inflationPerEpoch += (block.timestamp + curEpochLen - yearEndTime) * curInflationPerSecond; + // Calculate the max bond value + curMaxBond = (inflationPerEpoch * nextPoint.epochPoint.maxBondFraction) / 100; + // Update state maxBond value maxBond = uint96(curMaxBond); - // maxBond lock is set and cannot be changed until the next epoch with the year change passes - lockMaxBond = 2; - } else { - // This assignment is done again to account for the maxBond value that could change if we are currently - // in the epoch with a changing year - curMaxBond = maxBond; + // Reset the tokenomics parameters update flag + tokenomicsParametersUpdated = 0; + } else if (tokenomicsParametersUpdated > 0) { + // Since tokenomics parameters have been updated, maxBond has to be recalculated + curMaxBond = (curEpochLen * curInflationPerSecond * nextPoint.epochPoint.maxBondFraction) / 100; + // Update state maxBond value + maxBond = uint96(curMaxBond); + // Reset the tokenomics parameters update flag + tokenomicsParametersUpdated = 0; } // Update effectiveBond with the current or updated maxBond value curMaxBond += effectiveBond; @@ -987,7 +1007,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // codeUnits = (weightAgent * numComponents + weightComponent * numAgents) / (weightComponent * weightAgent) // (weightComponent * weightAgent) will be divided by when assigning to another variable uint256 codeUnits = (tp.unitPoints[1].unitWeight * tp.unitPoints[0].numNewUnits + - tp.unitPoints[0].unitWeight * tp.unitPoints[1].numNewUnits); + tp.unitPoints[0].unitWeight * tp.unitPoints[1].numNewUnits); // f(K(e), D(e)) = d * k * K(e) + d * D(e) // fKD = codeUnits * devsPerCapital * treasuryRewards + codeUnits * newOwners; // Convert all the necessary values to fixed-point numbers considering OLAS decimals (18 by default) @@ -1014,9 +1034,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // 1 + fKD in the system where 1e18 is equal to a whole unit (18 decimals) idf += uint64(fKD); } - - // Record settled epoch timestamp - tp.epochPoint.endTime = uint32(block.timestamp); + // Update the IDF value for the next epoch + nextPoint.epochPoint.idf = idf; // Cumulative incentives uint256 accountRewards = incentives[2] + incentives[3]; @@ -1034,25 +1053,12 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Emit settled epoch written to the last economics point emit EpochSettled(eCounter, incentives[1], accountRewards, accountTopUps); // Start new epoch - eCounter++; - epochCounter = uint32(eCounter); + epochCounter = uint32(eCounter + 1); } else { // If the treasury rebalance was not executed correctly, the new epoch does not start revert TreasuryRebalanceFailed(eCounter); } - // Copy current tokenomics point into the next one such that it has necessary tokenomics parameters - TokenomicsPoint storage nextPoint = mapEpochTokenomics[eCounter]; - for (uint256 i = 0; i < 2; ++i) { - nextPoint.unitPoints[i].topUpUnitFraction = tp.unitPoints[i].topUpUnitFraction; - nextPoint.unitPoints[i].rewardUnitFraction = tp.unitPoints[i].rewardUnitFraction; - nextPoint.unitPoints[i].unitWeight = tp.unitPoints[i].unitWeight; - } - nextPoint.epochPoint.rewardTreasuryFraction = tp.epochPoint.rewardTreasuryFraction; - nextPoint.epochPoint.maxBondFraction = tp.epochPoint.maxBondFraction; - nextPoint.epochPoint.devsPerCapital = tp.epochPoint.devsPerCapital; - nextPoint.epochPoint.idf = idf; - return true; } @@ -1230,14 +1236,12 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Calculate rewards and top-ups if there were pending ones from the previous epoch if (lastEpoch > 0 && lastEpoch < curEpoch) { // Get the overall amount of component rewards for the component's last epoch - // reward = (pendingRelativeReward * totalDonationsETH * rewardUnitFraction) / (100 * sumUnitDonationsETH) + // reward = (pendingRelativeReward * rewardUnitFraction) / 100 uint256 totalIncentives = mapUnitIncentives[unitTypes[i]][unitIds[i]].pendingRelativeReward; if (totalIncentives > 0) { - totalIncentives *= mapEpochTokenomics[lastEpoch].epochPoint.totalDonationsETH; totalIncentives *= mapEpochTokenomics[lastEpoch].unitPoints[unitTypes[i]].rewardUnitFraction; - uint256 sumUnitIncentives = mapEpochTokenomics[lastEpoch].unitPoints[unitTypes[i]].sumUnitDonationsETH * 100; // Accumulate to the final reward for the last epoch - reward += totalIncentives / sumUnitIncentives; + reward += totalIncentives / 100; } // Add the final top-up for the last epoch totalIncentives = mapUnitIncentives[unitTypes[i]][unitIds[i]].pendingRelativeTopUp; diff --git a/contracts/interfaces/IErrorsTokenomics.sol b/contracts/interfaces/IErrorsTokenomics.sol index 2c211eed..415d74ea 100644 --- a/contracts/interfaces/IErrorsTokenomics.sol +++ b/contracts/interfaces/IErrorsTokenomics.sol @@ -102,14 +102,6 @@ interface IErrorsTokenomics { /// @dev Caught reentrancy violation. error ReentrancyGuard(); - /// @dev maxBond parameter is locked and cannot be updated. - error MaxBondUpdateLocked(); - - /// @dev Rejects the max bond adjustment. - /// @param maxBondAmount Max bond amount available at the moment. - /// @param delta Delta bond amount to be subtracted from the maxBondAmount. - error RejectMaxBondAdjustment(uint256 maxBondAmount, uint256 delta); - /// @dev Failure of treasury re-balance during the reward allocation. /// @param epochNumber Epoch number. error TreasuryRebalanceFailed(uint256 epochNumber); diff --git a/test/Dispenser.js b/test/Dispenser.js index a63f6f2c..9bdf53fd 100644 --- a/test/Dispenser.js +++ b/test/Dispenser.js @@ -482,6 +482,10 @@ describe("Dispenser", async () => { // Change the component and agent fractions to zero await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 40, 40, 20); + // Changes will take place in the next epoch, need to move more than one epoch in time + await helpers.time.increase(epochLen + 10); + // Start new epoch + await tokenomics.connect(deployer).checkpoint(); // Send donations to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], @@ -575,6 +579,10 @@ describe("Dispenser", async () => { // Change the component fractions to zero await tokenomics.connect(deployer).changeIncentiveFractions(0, 100, 40, 0, 60); + // Changes will take place in the next epoch, need to move more than one epoch in time + await helpers.time.increase(epochLen + 10); + // Start new epoch + await tokenomics.connect(deployer).checkpoint(); // Send donations to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], @@ -668,6 +676,10 @@ describe("Dispenser", async () => { // Change the component and agent to-up fractions to zero await tokenomics.connect(deployer).changeIncentiveFractions(50, 30, 100, 0, 0); + // Changes will take place in the next epoch, need to move more than one epoch in time + await helpers.time.increase(epochLen + 10); + // Start new epoch + await tokenomics.connect(deployer).checkpoint(); // Send donations to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], @@ -762,7 +774,7 @@ describe("Dispenser", async () => { // Send donations to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], {value: twoRegDepositFromServices}); - // Change the fractions such that rewards and top-ups are now zero + // Change the fractions such that rewards and top-ups are now zero. However, they will be updated for the next epoch only await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); // Move more than one epoch in time await helpers.time.increase(epochLen + 10); @@ -770,11 +782,11 @@ describe("Dispenser", async () => { await tokenomics.connect(deployer).checkpoint(); // Get the last settled epoch counter - const lastPoint = Number(await tokenomics.epochCounter()) - 1; + let lastPoint = Number(await tokenomics.epochCounter()) - 1; // Get the epoch point of the last epoch - const ep = await tokenomics.getEpochPoint(lastPoint); + let ep = await tokenomics.getEpochPoint(lastPoint); // Get the unit points of the last epoch - const up = [await tokenomics.getUnitPoint(lastPoint, 0), await tokenomics.getUnitPoint(lastPoint, 1)]; + let up = [await tokenomics.getUnitPoint(lastPoint, 0), await tokenomics.getUnitPoint(lastPoint, 1)]; // Calculate rewards based on the points information const percentFraction = ethers.BigNumber.from(100); let rewards = [ @@ -789,8 +801,8 @@ describe("Dispenser", async () => { ethers.BigNumber.from(ep.totalTopUpsOLAS).mul(ethers.BigNumber.from(up[1].topUpUnitFraction)).div(percentFraction) ]; let accountTopUps = topUps[1].add(topUps[2]); - expect(accountRewards).to.equal(0); - expect(accountTopUps).to.equal(0); + expect(accountRewards).to.greaterThan(0); + expect(accountTopUps).to.greaterThan(0); // Check for the incentive balances such that their pending relative incentives are not zero let incentiveBalances = await tokenomics.getIncentiveBalances(0, 1); @@ -810,6 +822,46 @@ describe("Dispenser", async () => { expect(Math.abs(Number(accountRewards.sub(checkedReward)))).to.lessThan(delta); expect(Math.abs(Number(accountTopUps.sub(checkedTopUp)))).to.lessThan(delta); + // Claim rewards and top-ups + const balanceBeforeTopUps = ethers.BigNumber.from(await olas.balanceOf(deployer.address)); + await dispenser.connect(deployer).claimOwnerIncentives([0, 1], [1, 1]); + const balanceAfterTopUps = ethers.BigNumber.from(await olas.balanceOf(deployer.address)); + + // Check the OLAS balance after receiving incentives + const balance = balanceAfterTopUps.sub(balanceBeforeTopUps); + expect(balance).to.lessThanOrEqual(accountTopUps); + expect(Math.abs(Number(accountTopUps.sub(balance)))).to.lessThan(delta); + + // Send donations to services for the next epoch where all the fractions are zero + await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], + {value: twoRegDepositFromServices}); + // Move more than one epoch in time + await helpers.time.increase(epochLen + 10); + // Start new epoch and calculate tokenomics parameters and rewards + await tokenomics.connect(deployer).checkpoint(); + + // Get the last settled epoch counter + lastPoint = Number(await tokenomics.epochCounter()) - 1; + // Get the epoch point of the last epoch + ep = await tokenomics.getEpochPoint(lastPoint); + // Get the unit points of the last epoch + up = [await tokenomics.getUnitPoint(lastPoint, 0), await tokenomics.getUnitPoint(lastPoint, 1)]; + // Calculate rewards based on the points information + rewards = [ + ethers.BigNumber.from(ep.totalDonationsETH).mul(ethers.BigNumber.from(up[0].rewardUnitFraction)).div(percentFraction), + ethers.BigNumber.from(ep.totalDonationsETH).mul(ethers.BigNumber.from(up[1].rewardUnitFraction)).div(percentFraction) + ]; + accountRewards = rewards[0].add(rewards[1]); + // Calculate top-ups based on the points information + topUps = [ + ethers.BigNumber.from(ep.totalTopUpsOLAS).mul(ethers.BigNumber.from(ep.maxBondFraction)).div(percentFraction), + ethers.BigNumber.from(ep.totalTopUpsOLAS).mul(ethers.BigNumber.from(up[0].topUpUnitFraction)).div(percentFraction), + ethers.BigNumber.from(ep.totalTopUpsOLAS).mul(ethers.BigNumber.from(up[1].topUpUnitFraction)).div(percentFraction) + ]; + accountTopUps = topUps[1].add(topUps[2]); + expect(accountRewards).to.equal(0); + expect(accountTopUps).to.equal(0); + // Try to claim rewards and top-ups for owners which are essentially zero as all the fractions were set to zero await expect( dispenser.connect(deployer).claimOwnerIncentives([0, 1], [1, 1]) @@ -837,6 +889,10 @@ describe("Dispenser", async () => { // Change the fractions such that rewards and top-ups are not zero await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); + // Changes will take place in the next epoch, need to move more than one epoch in time + await helpers.time.increase(epochLen + 10); + // Start new epoch + await tokenomics.connect(deployer).checkpoint(); // Send donations to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], diff --git a/test/Dispenser.t.sol b/test/Dispenser.t.sol index 6c000f00..34456d6f 100644 --- a/test/Dispenser.t.sol +++ b/test/Dispenser.t.sol @@ -227,10 +227,9 @@ contract DispenserTest is BaseSetup { // Calculate maxBond uint256 calculatedMaxBond = (ep.totalTopUpsOLAS * ep.maxBondFraction) / 100; // Compare it with the max bond calculated from the fraction and the total OLAS inflation for the epoch - // Do not compare directly if it is the current or next epoch of the year change - uint256 numYearsCurEpoch = (block.timestamp - tokenomics.timeLaunch()) / tokenomics.ONE_YEAR(); + // Do not compare directly if the next epoch is the epoch where the year changes uint256 numYearsNextEpoch = (block.timestamp + tokenomics.epochLen() - tokenomics.timeLaunch()) / tokenomics.ONE_YEAR(); - if (numYearsCurEpoch == curYear && numYearsNextEpoch == curYear) { + if (numYearsNextEpoch == curYear) { assertEq(tokenomics.maxBond(), calculatedMaxBond); } // Effective bond must be the previous effective bond plus the actual maxBond diff --git a/test/Tokenomics.js b/test/Tokenomics.js index e32d9c9a..206f49a5 100644 --- a/test/Tokenomics.js +++ b/test/Tokenomics.js @@ -232,32 +232,58 @@ describe("Tokenomics", async () => { }); it("Changing tokenomics parameters", async function () { - const cutEpochLen = epochLen * 2; + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); + + const lessThanMinEpochLen = Number(await tokenomics.MIN_EPOCH_LENGTH()) - 1; // Trying to change tokenomics parameters from a non-owner account address await expect( - tokenomics.connect(signers[1]).changeTokenomicsParameters(10, 10, cutEpochLen, 10) + tokenomics.connect(signers[1]).changeTokenomicsParameters(10, 10, epochLen * 2, 10, 10, 10) ).to.be.revertedWithCustomError(tokenomics, "OwnerOnly"); - await tokenomics.changeTokenomicsParameters(10, 10, cutEpochLen, 10); + // Trying to set epoch length smaller than the minimum allowed value + await tokenomics.changeTokenomicsParameters(10, 10, lessThanMinEpochLen, 10, 10, 10); + // Move one epoch in time and finish the epoch + await helpers.time.increase(epochLen + 100); + await tokenomics.checkpoint(); + // Make sure the epoch lenght didn't change + expect(await tokenomics.epochLen()).to.equal(epochLen); + + // Change epoch length to a bigger number + await tokenomics.changeTokenomicsParameters(10, 10, epochLen * 2, 10, 10, 10); + // The change will take effect in the next epoch + expect(await tokenomics.epochLen()).to.equal(epochLen); + // Move one epoch in time and finish the epoch + await helpers.time.increase(epochLen + 100); + await tokenomics.checkpoint(); + expect(await tokenomics.epochLen()).to.equal(epochLen * 2); + // Change epoch len to a smaller value - await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10, 10, 10); + // The change will take effect in the next epoch + expect(await tokenomics.epochLen()).to.equal(epochLen * 2); + // Move one epoch in time and finish the epoch + await helpers.time.increase(epochLen * 2 + 100); + await tokenomics.checkpoint(); + expect(await tokenomics.epochLen()).to.equal(epochLen); + // Leave the epoch length untouched - await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10, 10, 10); // And then change back to the bigger one - await tokenomics.changeTokenomicsParameters(10, 10, epochLen + 100, 10); - // Try to set the epochLen to a time where it fails due to effectiveBond going below zero - // since all the current effectiveBond is already reserved - await tokenomics.reserveAmountForBondProgram(await tokenomics.effectiveBond()); - await expect( - tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10) - ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen + 100, 10, 10, 10); + // The change will take effect in the next epoch + expect(await tokenomics.epochLen()).to.equal(epochLen); + // Move one epoch in time and finish the epoch + await helpers.time.increase(epochLen + 100); + await tokenomics.checkpoint(); + expect(await tokenomics.epochLen()).to.equal(epochLen + 100); // Trying to set epsilonRate bigger than 17e18 - await tokenomics.changeTokenomicsParameters(10, "171"+"0".repeat(17), 10, 10); + await tokenomics.changeTokenomicsParameters(0, "171"+"0".repeat(17), 0, 0, 0, 0); expect(await tokenomics.epsilonRate()).to.equal(10); // Trying to set all zeros - await tokenomics.changeTokenomicsParameters(0, 0, 0, 0); + await tokenomics.changeTokenomicsParameters(0, 0, 0, 0, 0, 0); // Check that parameters were not changed expect(await tokenomics.epsilonRate()).to.equal(10); expect(await tokenomics.epochLen()).to.equal(epochLen + 100); @@ -268,6 +294,12 @@ describe("Tokenomics", async () => { // Get the epoch point of the current epoch const ep = await tokenomics.getEpochPoint(curPoint); expect(await ep.devsPerCapital).to.equal(10); + const up = [await tokenomics.getUnitPoint(curPoint, 0), await tokenomics.getUnitPoint(curPoint, 1)]; + expect(up[0].unitWeight).to.equal(10); + expect(up[1].unitWeight).to.equal(10); + + // Restore to the state of the snapshot + await snapshot.restore(); }); it("Changing reward fractions", async function () { @@ -380,13 +412,16 @@ describe("Tokenomics", async () => { context("Tokenomics calculation", async function () { it("Checkpoint without any revenues", async () => { // Skip the number of blocks within the epoch + const epochCounter = await tokenomics.epochCounter(); await ethers.provider.send("evm_mine"); await tokenomics.connect(deployer).checkpoint(); + let updatedEpochCounter = await tokenomics.epochCounter(); + expect(updatedEpochCounter).to.equal(epochCounter); // Try to run checkpoint while the epoch length is not yet reached - await tokenomics.changeTokenomicsParameters(10, 10, 10, 10); await tokenomics.connect(deployer).checkpoint(); - + updatedEpochCounter = await tokenomics.epochCounter(); + expect(updatedEpochCounter).to.equal(epochCounter); }); it("Checkpoint with revenues", async () => { @@ -418,6 +453,9 @@ describe("Tokenomics", async () => { it("Checkpoint with inability to re-balance treasury rewards", async () => { // Change tokenomics factors such that all the rewards are given to the treasury await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 20, 50, 30); + // Move more than one epoch in time and move to the next epoch + await helpers.time.increase(epochLen + 10); + await tokenomics.checkpoint(); // Send the revenues to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], {value: twoRegDepositFromServices}); @@ -443,8 +481,7 @@ describe("Tokenomics", async () => { await treasury.connect(deployer).depositServiceDonationsETH(accounts, [regDepositFromServices, regDepositFromServices], {value: twoRegDepositFromServices}); // Start new epoch and calculate tokenomics parameters and rewards - await tokenomics.changeTokenomicsParameters(10, 10, 10, 10); - await helpers.time.increase(10); + await helpers.time.increase(epochLen + 10); await tokenomics.connect(deployer).checkpoint(); // Get IDF @@ -571,64 +608,161 @@ describe("Tokenomics", async () => { }); it("Changing maxBond values", async function () { + // Take a snapshot of the current state of the blockchain + let snapshot = await helpers.takeSnapshot(); + + const initEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); + let initMaxBond = initEffectiveBond; + const initMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; + // Changing maxBond fraction to 100% await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); - const initEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); - // Changing the epoch length to 10 - let epochLenFactor = 10; - let newEpochLen = epochLen * epochLenFactor; - await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); - let effectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); - // Verify that the effectiveBond increased by a factor of epochLenFactor - expect(initEffectiveBond.mul(epochLenFactor)).to.equal(effectiveBond); + // Check that the next maxBond has been updated correctly in comparison with the initial one + let nextMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; + expect(nextMaxBondFraction).to.equal(100); + let nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); + expect((nextMaxBond.div(nextMaxBondFraction)).mul(initMaxBondFraction)).to.equal(initMaxBond); + // Restore the state of the blockchain back to before changing maxBond-related parameters + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); + + // Change the epoch length + let newEpochLen = 2 * epochLen; + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); - // Reserve half of the effectiveBond - const halfEffectiveBond = effectiveBond.div(2); - await tokenomics.connect(deployer).reserveAmountForBondProgram(halfEffectiveBond); + // Check the new maxBond + nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); + expect(nextMaxBond.div(2)).to.equal(initMaxBond); + // Restore the state of the blockchain back to before changing maxBond-related parameters + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); - // Check that the epoch length cannot be reduced by a half or more - newEpochLen = newEpochLen / 2; - await expect( - tokenomics.connect(deployer).changeTokenomicsParameters(0, 0, newEpochLen, 0) - ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); + // Change now maxBondFraction and epoch length at the same time + await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); - // Check in a static call that the change on a bigger value is fine - await tokenomics.connect(deployer).callStatic.changeTokenomicsParameters(0, 0, newEpochLen + 1, 0); + // Check the new maxBond + nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); + expect((nextMaxBond.div(nextMaxBondFraction).div(2)).mul(initMaxBondFraction)).to.equal(initMaxBond); + // Restore the state of the blockchain back to the very beginning of this test + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); - // Check that the maxBond fraction cannot be reduced by a half or more - await expect( - tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 50, 0, 0) - ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); + // Move to the epoch before changing the year + // OLAS starting time + const timeLaunch = Number(await tokenomics.timeLaunch()); + // One year time from the launch + const yearChangeTime = timeLaunch + oneYear; + + // Get to the time of one and a half the epoch length before the year change (1.5 epoch length) + let timeEpochBeforeYearChange = yearChangeTime - epochLen - epochLen / 2; + await helpers.time.increaseTo(timeEpochBeforeYearChange); + await tokenomics.checkpoint(); - // Check in a static call that the change on a bigger maxBond fraction value is fine - await tokenomics.connect(deployer).callStatic.changeIncentiveFractions(0, 0, 51, 0, 0); + snapshot = await helpers.takeSnapshot(); - // Check that the reserve amount can go maximum to the effectiveBond == 0 - let result = await tokenomics.connect(deployer).callStatic.reserveAmountForBondProgram(halfEffectiveBond); - expect(result).to.equal(true); - result = await tokenomics.connect(deployer).callStatic.reserveAmountForBondProgram(halfEffectiveBond.add(1)); - expect(result).to.equal(false); + // Calculate the maxBond manually and compare with the tokenomics one + initMaxBond = await tokenomics.maxBond(); + let inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); + // Get the part of a max bond before the year change + let manualMaxBond = ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond); + inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); + manualMaxBond = manualMaxBond.add(ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond)); + manualMaxBond = (manualMaxBond.mul(initMaxBondFraction)).div(ethers.BigNumber.from(100)); - // Increase the epoch length by 10 (was x1, then x10, then x5 (not executed), now will be x15) - newEpochLen += epochLen * 10; - await tokenomics.connect(deployer).changeTokenomicsParameters(0, 0, newEpochLen, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); - // Now we should be able to reserve of the amount of the effectiveBond, since we increased by half of the original - // Since we reserved half, we can now go no lower than one third - // EffectiveBond was 100, we reserved 50, it became 100 - 50 = 50. We then added 50 more. The effectiveBond is 100. - // The total effectiveBond if we returned the reserved one would be 150. So we can reduce the effectiveBond - // by a maximum of 100 out of 150, which is 66%. - await expect( - tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 33, 0, 0) - ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); - await tokenomics.connect(deployer).callStatic.changeIncentiveFractions(0, 0, 34, 0, 0); + let updatedMaxBond = await tokenomics.maxBond(); + expect(updatedMaxBond).to.greaterThan(manualMaxBond); + // Add more inflation to the manual maxBond since the round-off is within one block + manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(12).mul(inflationPerSecond))); + expect(manualMaxBond).to.greaterThan(updatedMaxBond); + + // Now the maxBond will be calculated fully dependent on the inflation for the next year + manualMaxBond = ethers.BigNumber.from(epochLen).mul(inflationPerSecond); + manualMaxBond = (manualMaxBond.mul(initMaxBondFraction)).div(ethers.BigNumber.from(100)); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + updatedMaxBond = await tokenomics.maxBond(); + expect(updatedMaxBond).to.equal(manualMaxBond); + + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); + + // Changing maxBond fraction to 100% + await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); + // Calculate the maxBond manually and compare with the tokenomics one + initMaxBond = await tokenomics.maxBond(); + inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); + // Get the part of a max bond before the year change + manualMaxBond = ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond); + inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); + manualMaxBond = manualMaxBond.add(ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond)); + + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); - // Since 50 was reserved, the maximum we can reserve now is 100 (out of 150), or the full effectiveBond - result = await tokenomics.connect(deployer).callStatic.reserveAmountForBondProgram(effectiveBond); - expect(result).to.equal(true); - result = await tokenomics.connect(deployer).callStatic.reserveAmountForBondProgram(effectiveBond.add(1)); - expect(result).to.equal(false); + updatedMaxBond = await tokenomics.maxBond(); + expect(updatedMaxBond).to.greaterThan(manualMaxBond); + // Add more inflation to the manual maxBond since the round-off is within one block + manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(12).mul(inflationPerSecond))); + expect(manualMaxBond).to.greaterThan(updatedMaxBond); + + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); + + // Change the epoch length + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); + // Calculate the maxBond manually and compare with the tokenomics one + initMaxBond = await tokenomics.maxBond(); + inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); + // Get the part of a max bond before the year change + manualMaxBond = ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond); + inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); + manualMaxBond = manualMaxBond.add(ethers.BigNumber.from(newEpochLen - epochLen / 2).mul(inflationPerSecond)); + manualMaxBond = (manualMaxBond.mul(initMaxBondFraction)).div(ethers.BigNumber.from(100)); + + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + updatedMaxBond = await tokenomics.maxBond(); + expect(updatedMaxBond).to.greaterThan(manualMaxBond); + // Add more inflation to the manual maxBond since the round-off is within one block + manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(12).mul(inflationPerSecond))); + expect(manualMaxBond).to.greaterThan(updatedMaxBond); + + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); + + // Change now maxBondFraction and epoch length at the same time + await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); + // Calculate the maxBond manually and compare with the tokenomics one + initMaxBond = await tokenomics.maxBond(); + inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); + // Get the part of a max bond before the year change + manualMaxBond = ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond); + inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); + manualMaxBond = manualMaxBond.add(ethers.BigNumber.from(newEpochLen - epochLen / 2).mul(inflationPerSecond)); + + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + updatedMaxBond = await tokenomics.maxBond(); + expect(updatedMaxBond).to.greaterThan(manualMaxBond); + // Add more inflation to the manual maxBond since the round-off is within one block + manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(12).mul(inflationPerSecond))); + expect(manualMaxBond).to.greaterThan(updatedMaxBond); + + snapshot.restore(); }); }); @@ -666,10 +800,16 @@ describe("Tokenomics", async () => { let timeEpochBeforeYearChange = yearChangeTime - epochLen - epochLen / 2; await helpers.time.increaseTo(timeEpochBeforeYearChange); await tokenomics.checkpoint(); + + let snapshotInternal = await helpers.takeSnapshot(); // Try to change the epoch length now such that the next epoch will immediately have the year change - await expect( - tokenomics.changeTokenomicsParameters(1, 1, 2 * epochLen, 1) - ).to.be.revertedWithCustomError(tokenomics, "MaxBondUpdateLocked"); + await tokenomics.changeTokenomicsParameters(0, 0, 2 * epochLen, 0, 0, 0); + // Move to the end of epoch and check the updated epoch length + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + expect(await tokenomics.epochLen()).to.equal(2 * epochLen); + // Restore the state of the blockchain back to the time half of the epoch before one epoch left for the current year + snapshotInternal.restore(); // Get to the time of the half epoch length before the year change // Meaning that the year does not change yet during the current epoch, but it will during the next one @@ -678,21 +818,14 @@ describe("Tokenomics", async () => { await tokenomics.checkpoint(); // The maxBond lock flag must be set to true, now try to change the epochLen - await expect( - tokenomics.changeTokenomicsParameters(1, 1, epochLen + 100, 1) - ).to.be.revertedWithCustomError(tokenomics, "MaxBondUpdateLocked"); + await tokenomics.changeTokenomicsParameters(0, 0, epochLen + 100, 0, 0, 0); // Try to change the maxBondFraction as well - await expect( - tokenomics.changeIncentiveFractions(30, 40, 60, 40, 0) - ).to.be.revertedWithCustomError(tokenomics, "MaxBondUpdateLocked"); + await tokenomics.changeIncentiveFractions(30, 40, 60, 40, 0); // Now skip one epoch await helpers.time.increaseTo(timeEpochBeforeYearChange + epochLen); await tokenomics.checkpoint(); - - // Change parameters now - await tokenomics.changeTokenomicsParameters(1, 1, epochLen, 1); - await tokenomics.changeIncentiveFractions(30, 40, 50, 50, 0); + expect(await tokenomics.epochLen()).to.equal(epochLen + 100); // Restore to the state of the snapshot await snapshot.restore();