From 40d0f26a26d336c652ecca7a6d323b3bbb483803 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 18 Jan 2023 19:27:38 +0000 Subject: [PATCH 1/5] refactor: defer the change tokenomics parameters to the next epoch --- contracts/Tokenomics.sol | 223 ++++++++++++++++++++------------------- 1 file changed, 115 insertions(+), 108 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 8cabf142..2084e4a3 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -134,9 +134,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 +192,23 @@ 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 + uint8 public tokenomicsParametersUpdated; + // Incentive fractions change request flag + uint8 public incentiveFractionsUpdated; // 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,7 +295,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { _locked = 1; epsilonRate = 1e17; veOLASThreshold = 5_000e18; - lockMaxBond = 1; + tokenomicsParametersUpdated = 1; + incentiveFractionsUpdated = 1; // Check that the epoch length has at least a practical minimal value // TODO Decide on the final minimal value @@ -466,36 +478,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 +499,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 +509,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 +528,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; + revert Overflow(MIN_EPOCH_LENGTH, _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 + tokenomicsParametersUpdated = 2; + emit TokenomicsParametersUpdateRequested(eCounter + 1, _devsPerCapital, _epsilonRate, _epochLen, + _veOLASThreshold, _componentWeight, _agentWeight); } /// @dev Sets incentive parameter fractions. @@ -607,34 +594,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 + incentiveFractionsUpdated = 2; + emit IncentiveFractionsUpdateRequested(eCounter, _rewardComponentFraction, _rewardAgentFraction, + _maxBondFraction, _topUpComponentFraction, _topUpAgentFraction); } /// @dev Reserves OLAS amount from the effective bond to be minted during a bond program. @@ -904,17 +880,18 @@ 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; + // Record the current inflation per second + uint256 curInflationPerSecond = inflationPerSecond; // 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; // 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 @@ -929,7 +906,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { inflationPerSecond = uint96(curInflationPerSecond); currentYear = uint8(numYears); // maxBond lock is released and can be changed starting from the new epoch - lockMaxBond = 1; + //lockMaxBond = 1; } else { inflationPerEpoch = inflationPerSecond * diffNumSeconds; } @@ -951,6 +928,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 (incentiveFractionsUpdated == 2) { + // The update has been already performed by the changeIncentiveFractions() function call + incentiveFractionsUpdated = 1; + // 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 + if (tokenomicsParametersUpdated == 2) { + // Update epoch length and set the next value back to zero + curEpochLen = nextEpochLen; + nextEpochLen = 0; + epochLen = uint32(curEpochLen); + // Update veOLAS threshold and set the next value back to zero + veOLASThreshold = nextVeOLASThreshold; + nextVeOLASThreshold = 0; + + // Recalculate maxBond for the next epoch + maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; + + // Set the tokenomics parameters flag back to unchanged + tokenomicsParametersUpdated = 1; + // 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,7 +979,6 @@ 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 @@ -966,13 +986,14 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // 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; + curMaxBond += ((block.timestamp + curEpochLen - yearEndTime) * curInflationPerSecond * 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; + //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 + // This assignment is done again to account for the maxBond value that could have changed if we are currently + // in the epoch with a changing year, or because the maxBondFraction has changed curMaxBond = maxBond; } // Update effectiveBond with the current or updated maxBond value @@ -1014,9 +1035,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 +1054,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; } From 20bd250ba010515dfa5382027634f15aee4c57de Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 20 Jan 2023 18:04:29 +0000 Subject: [PATCH 2/5] fix and test: tokenomics refactor and test adjustment --- contracts/Depository.sol | 4 +- contracts/Tokenomics.sol | 26 ++- contracts/interfaces/IErrorsTokenomics.sol | 8 - test/Dispenser.js | 68 +++++- test/Tokenomics.js | 233 +++++++++++++-------- 5 files changed, 227 insertions(+), 112 deletions(-) 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 2084e4a3..c666ddb0 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -301,7 +301,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // 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 @@ -531,7 +531,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { if (uint32(_epochLen) >= MIN_EPOCH_LENGTH) { nextEpochLen = uint32(_epochLen); } else { - revert Overflow(MIN_EPOCH_LENGTH, _epochLen); + _epochLen = epochLen; } // Adjust veOLAS threshold for the next epoch @@ -938,6 +938,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { nextPoint.epochPoint.devsPerCapital = tp.epochPoint.devsPerCapital; // Update incentive fractions for the next epoch if (incentiveFractionsUpdated == 2) { + // Recalculate maxBond for the next epoch + maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; // The update has been already performed by the changeIncentiveFractions() function call incentiveFractionsUpdated = 1; // Confirm the change of incentive fractions @@ -954,15 +956,19 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Update parameters for the next epoch, if changes were requested by the changeTokenomicsParameters() function if (tokenomicsParametersUpdated == 2) { // Update epoch length and set the next value back to zero - curEpochLen = nextEpochLen; - nextEpochLen = 0; - epochLen = uint32(curEpochLen); - // Update veOLAS threshold and set the next value back to zero - veOLASThreshold = nextVeOLASThreshold; - nextVeOLASThreshold = 0; + if (nextEpochLen > 0) { + curEpochLen = nextEpochLen; + epochLen = uint32(curEpochLen); + nextEpochLen = 0; - // Recalculate maxBond for the next epoch - maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; + // Recalculate maxBond for the next epoch + maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; + } + // Update veOLAS threshold and set the next value back to zero + if (nextVeOLASThreshold > 0) { + veOLASThreshold = nextVeOLASThreshold; + nextVeOLASThreshold = 0; + } // Set the tokenomics parameters flag back to unchanged tokenomicsParametersUpdated = 1; 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/Tokenomics.js b/test/Tokenomics.js index e32d9c9a..122ec35b 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,89 @@ describe("Tokenomics", async () => { }); it("Changing maxBond values", async function () { - // 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); - - 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); - - // Reserve half of the effectiveBond - const halfEffectiveBond = effectiveBond.div(2); - await tokenomics.connect(deployer).reserveAmountForBondProgram(halfEffectiveBond); - - // 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"); - - // 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 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"); - - // 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); - - // 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); + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); - // 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); + const initEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); + const initMaxBond = initEffectiveBond; + const initMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; + console.log(initMaxBondFraction); + console.log("initMaxBond", Number(initMaxBond)); - // 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); + // Changing maxBond fraction to 100% + await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); + 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); + // Check that the next maxBond has been updated correctly in comparison with the initial one + const nextMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; + expect(nextMaxBondFraction).to.equal(100); + const nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); + console.log("nextMaxBond", Number(nextMaxBond)); + expect((nextMaxBond.div(nextMaxBondFraction)).mul(initMaxBondFraction)).to.equal(initMaxBond); + + // const nextEffectiveBond = 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, 0, 0); + // // Increase the time and change the epoch + // await helpers.time.increase(epochLen + 100); + // await tokenomics.checkpoint(); + // + // let effectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); + // // Verify that the effectiveBond increased by a factor of epochLenFactor + // expect(initEffectiveBond.add(initEffectiveBond.mul(epochLenFactor))).to.equal(effectiveBond); + // return; + // + // // Reserve half of the effectiveBond + // const halfEffectiveBond = effectiveBond.div(2); + // await tokenomics.connect(deployer).reserveAmountForBondProgram(halfEffectiveBond); + // + // // 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, 0, 0) + // ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); + // + // // 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, 0, 0); + // + // // 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"); + // + // // 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); + // + // // 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); + // + // // 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, 0, 0); + // + // // 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); + // + // // 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); + + snapshot.restore(); }); }); @@ -666,10 +728,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 +746,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(); From aed85741c83b06d6bba1836cde41caf89b219c1a Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 24 Jan 2023 14:31:23 +0000 Subject: [PATCH 3/5] refactor and test: adjust incentives calculations and fix tests --- contracts/Tokenomics.sol | 107 ++++++++++++++++++--------------------- test/Dispenser.t.sol | 5 +- test/Tokenomics.js | 73 ++++++++++++++++++++++++-- 3 files changed, 122 insertions(+), 63 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index c666ddb0..7b244345 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; @@ -193,9 +190,7 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // This number is enough for the next 255 years uint8 public currentYear; // Tokenomics parameters change request flag - uint8 public tokenomicsParametersUpdated; - // Incentive fractions change request flag - uint8 public incentiveFractionsUpdated; + bytes1 public tokenomicsParametersUpdated; // Reentrancy lock uint8 internal _locked; @@ -295,8 +290,6 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { _locked = 1; epsilonRate = 1e17; veOLASThreshold = 5_000e18; - tokenomicsParametersUpdated = 1; - incentiveFractionsUpdated = 1; // Check that the epoch length has at least a practical minimal value // TODO Decide on the final minimal value @@ -555,8 +548,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { _agentWeight = mapEpochTokenomics[eCounter].unitPoints[1].unitWeight; } - // Set the flag that tokenomics parameters are requested to be updated - tokenomicsParametersUpdated = 2; + // 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); } @@ -607,8 +600,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { tp.unitPoints[0].topUpUnitFraction = uint8(_topUpComponentFraction); tp.unitPoints[1].topUpUnitFraction = uint8(_topUpAgentFraction); - // Set the flag that incentive fractions are requested to be updated - incentiveFractionsUpdated = 2; + // 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); } @@ -658,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; @@ -681,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 @@ -729,6 +721,8 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { if (numServiceUnits == 0) { revert ServiceNeverDeployed(serviceIds[i]); } + // The amount has to be adjusted for the number of units in the service + amount /= uint96(numServiceUnits); // Record amounts data only if at least one incentive unit fraction is not zero if (incentiveFlags[unitType] || incentiveFlags[unitType + 2]) { // Accumulate amounts for each unit Id @@ -750,7 +744,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 @@ -778,6 +771,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]); +// } } } @@ -789,8 +786,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); @@ -860,6 +855,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; } @@ -882,10 +878,6 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { uint256 inflationPerEpoch; // Record the current inflation per second uint256 curInflationPerSecond = inflationPerSecond; - // 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; // Current year uint256 numYears = (block.timestamp - timeLaunch) / ONE_YEAR; // Amounts for the yearly inflation change from year to year, so if the year changes in the middle @@ -900,15 +892,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 @@ -916,6 +907,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, @@ -936,12 +932,9 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { nextPoint.unitPoints[i].unitWeight = tp.unitPoints[i].unitWeight; } nextPoint.epochPoint.devsPerCapital = tp.epochPoint.devsPerCapital; - // Update incentive fractions for the next epoch - if (incentiveFractionsUpdated == 2) { - // Recalculate maxBond for the next epoch - maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; - // The update has been already performed by the changeIncentiveFractions() function call - incentiveFractionsUpdated = 1; + // 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 { @@ -954,30 +947,27 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { nextPoint.epochPoint.maxBondFraction = tp.epochPoint.maxBondFraction; } // Update parameters for the next epoch, if changes were requested by the changeTokenomicsParameters() function - if (tokenomicsParametersUpdated == 2) { + // 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; - - // Recalculate maxBond for the next epoch - maxBond = uint96(curInflationPerSecond * curEpochLen * nextPoint.epochPoint.maxBondFraction) / 100; } + // Update veOLAS threshold and set the next value back to zero if (nextVeOLASThreshold > 0) { veOLASThreshold = nextVeOLASThreshold; nextVeOLASThreshold = 0; } - // Set the tokenomics parameters flag back to unchanged - tokenomicsParametersUpdated = 1; // 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 @@ -987,20 +977,25 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { // Calculate remainder of inflation for the passing year // 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 * nextPoint.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 have changed if we are currently - // in the epoch with a changing year, or because the maxBondFraction has changed - 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; @@ -1014,7 +1009,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) @@ -1243,14 +1238,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/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 122ec35b..8c2eb616 100644 --- a/test/Tokenomics.js +++ b/test/Tokenomics.js @@ -612,22 +612,89 @@ describe("Tokenomics", async () => { const snapshot = await helpers.takeSnapshot(); const initEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); - const initMaxBond = initEffectiveBond; + let initMaxBond = initEffectiveBond; const initMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; console.log(initMaxBondFraction); console.log("initMaxBond", Number(initMaxBond)); + let snapshotInternal = await helpers.takeSnapshot(); // Changing maxBond fraction to 100% await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); await helpers.time.increase(epochLen); await tokenomics.checkpoint(); // Check that the next maxBond has been updated correctly in comparison with the initial one - const nextMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; + let nextMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; expect(nextMaxBondFraction).to.equal(100); - const nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); + let nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); console.log("nextMaxBond", Number(nextMaxBond)); expect((nextMaxBond.div(nextMaxBondFraction)).mul(initMaxBondFraction)).to.equal(initMaxBond); + // Restore the state of the blockchain back to before changing maxBond-related parameters + snapshotInternal.restore(); + + // Change the epoch length + await tokenomics.changeTokenomicsParameters(0, 0, 2 * epochLen, 0, 0, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // 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 + snapshotInternal.restore(); + + // 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, 2 * epochLen, 0, 0, 0); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // 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 before changing maxBond-related parameters + snapshotInternal.restore(); + return; + + // 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 half the epoch length before the year change (0.5 epoch length) + let timeEpochBeforeYearChange = yearChangeTime - epochLen / 2; + await helpers.time.increaseTo(timeEpochBeforeYearChange); + await tokenomics.checkpoint(); + + // Next epoch will be the year change epoch + snapshotInternal = await helpers.takeSnapshot(); + // Calculate the maxBond manually and compare with the tokenomics one + initMaxBond = await tokenomics.maxBond(); + console.log("initMaxBond", initMaxBond); + let inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); + console.log("inflationPerSecond this year", inflationPerSecond); + // Get the part of a max bond before the year change + let manualMaxBond = (ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2); + console.log("half of current year epoch", manualMaxBond.div(49)); + inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); + console.log("inflationPerSecond next year", inflationPerSecond); + manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2)); + console.log("half of current year epoch", ((ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2)).div(49)); + manualMaxBond = (manualMaxBond.mul(initMaxBondFraction)).div(ethers.BigNumber.from(100)); + expect(initMaxBond).to.equal(manualMaxBond); + + snapshotInternal.restore(); + +// let snapshotInternal = await helpers.takeSnapshot(); +// // Try to change the epoch length now such that the next epoch will immediately have the year change +// 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(); // const nextEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); // // Changing the epoch length to 10 From 69f5543ef58aa06d4165c0a056193d0db0a3f989 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 24 Jan 2023 16:29:16 +0000 Subject: [PATCH 4/5] fix: donation amount calculation for each service --- contracts/Tokenomics.sol | 6 ++---- test/Tokenomics.js | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 7b244345..75345913 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -700,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 @@ -721,10 +719,10 @@ contract Tokenomics is TokenomicsConstants, IErrorsTokenomics { if (numServiceUnits == 0) { revert ServiceNeverDeployed(serviceIds[i]); } - // The amount has to be adjusted for the number of units in the service - amount /= uint96(numServiceUnits); // 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 diff --git a/test/Tokenomics.js b/test/Tokenomics.js index 8c2eb616..5f2a7b98 100644 --- a/test/Tokenomics.js +++ b/test/Tokenomics.js @@ -607,7 +607,7 @@ describe("Tokenomics", async () => { expect(topUp).to.greaterThan(0); }); - it("Changing maxBond values", async function () { + it.only("Changing maxBond values", async function () { // Take a snapshot of the current state of the blockchain const snapshot = await helpers.takeSnapshot(); @@ -654,7 +654,6 @@ describe("Tokenomics", async () => { expect((nextMaxBond.div(nextMaxBondFraction).div(2)).mul(initMaxBondFraction)).to.equal(initMaxBond); // Restore the state of the blockchain back to before changing maxBond-related parameters snapshotInternal.restore(); - return; // Move to the epoch before changing the year // OLAS starting time @@ -686,15 +685,15 @@ describe("Tokenomics", async () => { snapshotInternal.restore(); -// let snapshotInternal = await helpers.takeSnapshot(); -// // Try to change the epoch length now such that the next epoch will immediately have the year change -// 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(); + // let snapshotInternal = await helpers.takeSnapshot(); + // // Try to change the epoch length now such that the next epoch will immediately have the year change + // 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(); // const nextEffectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); // // Changing the epoch length to 10 From 7a938097e8225bfdb3cb073d28be5c9586d55e03 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 24 Jan 2023 17:43:54 +0000 Subject: [PATCH 5/5] test: adding tokenomics tests --- test/Tokenomics.js | 196 +++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/test/Tokenomics.js b/test/Tokenomics.js index 5f2a7b98..206f49a5 100644 --- a/test/Tokenomics.js +++ b/test/Tokenomics.js @@ -607,17 +607,14 @@ describe("Tokenomics", async () => { expect(topUp).to.greaterThan(0); }); - it.only("Changing maxBond values", async function () { + it("Changing maxBond values", async function () { // Take a snapshot of the current state of the blockchain - const snapshot = await helpers.takeSnapshot(); + 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; - console.log(initMaxBondFraction); - console.log("initMaxBond", Number(initMaxBond)); - let snapshotInternal = await helpers.takeSnapshot(); // Changing maxBond fraction to 100% await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0); await helpers.time.increase(epochLen); @@ -627,13 +624,14 @@ describe("Tokenomics", async () => { let nextMaxBondFraction = (await tokenomics.mapEpochTokenomics(await tokenomics.epochCounter())).maxBondFraction; expect(nextMaxBondFraction).to.equal(100); let nextMaxBond = ethers.BigNumber.from(await tokenomics.maxBond()); - console.log("nextMaxBond", Number(nextMaxBond)); expect((nextMaxBond.div(nextMaxBondFraction)).mul(initMaxBondFraction)).to.equal(initMaxBond); // Restore the state of the blockchain back to before changing maxBond-related parameters - snapshotInternal.restore(); + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); // Change the epoch length - await tokenomics.changeTokenomicsParameters(0, 0, 2 * epochLen, 0, 0, 0); + let newEpochLen = 2 * epochLen; + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); await helpers.time.increase(epochLen); await tokenomics.checkpoint(); @@ -641,19 +639,21 @@ describe("Tokenomics", async () => { 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 - snapshotInternal.restore(); + 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, 2 * epochLen, 0, 0, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0, 0, 0); await helpers.time.increase(epochLen); await tokenomics.checkpoint(); // 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 before changing maxBond-related parameters - snapshotInternal.restore(); + // Restore the state of the blockchain back to the very beginning of this test + snapshot.restore(); + snapshot = await helpers.takeSnapshot(); // Move to the epoch before changing the year // OLAS starting time @@ -661,100 +661,106 @@ describe("Tokenomics", async () => { // One year time from the launch const yearChangeTime = timeLaunch + oneYear; - // Get to the time of half the epoch length before the year change (0.5 epoch length) - let timeEpochBeforeYearChange = yearChangeTime - epochLen / 2; + // 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(); - // Next epoch will be the year change epoch - snapshotInternal = await helpers.takeSnapshot(); + snapshot = await helpers.takeSnapshot(); + // Calculate the maxBond manually and compare with the tokenomics one initMaxBond = await tokenomics.maxBond(); - console.log("initMaxBond", initMaxBond); let inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); - console.log("inflationPerSecond this year", inflationPerSecond); // Get the part of a max bond before the year change - let manualMaxBond = (ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2); - console.log("half of current year epoch", manualMaxBond.div(49)); + let manualMaxBond = ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond); inflationPerSecond = (await tokenomics.getInflationForYear(1)).div(await tokenomics.ONE_YEAR()); - console.log("inflationPerSecond next year", inflationPerSecond); - manualMaxBond = manualMaxBond.add((ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2)); - console.log("half of current year epoch", ((ethers.BigNumber.from(epochLen).mul(inflationPerSecond)).div(2)).div(49)); + manualMaxBond = manualMaxBond.add(ethers.BigNumber.from(epochLen / 2).mul(inflationPerSecond)); manualMaxBond = (manualMaxBond.mul(initMaxBondFraction)).div(ethers.BigNumber.from(100)); - expect(initMaxBond).to.equal(manualMaxBond); - snapshotInternal.restore(); + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + 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(); + + 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(); - // let snapshotInternal = await helpers.takeSnapshot(); - // // Try to change the epoch length now such that the next epoch will immediately have the year change - // 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(); - - // const nextEffectiveBond = 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, 0, 0); - // // Increase the time and change the epoch - // await helpers.time.increase(epochLen + 100); - // await tokenomics.checkpoint(); - // - // let effectiveBond = ethers.BigNumber.from(await tokenomics.effectiveBond()); - // // Verify that the effectiveBond increased by a factor of epochLenFactor - // expect(initEffectiveBond.add(initEffectiveBond.mul(epochLenFactor))).to.equal(effectiveBond); - // return; - // - // // Reserve half of the effectiveBond - // const halfEffectiveBond = effectiveBond.div(2); - // await tokenomics.connect(deployer).reserveAmountForBondProgram(halfEffectiveBond); - // - // // 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, 0, 0) - // ).to.be.revertedWithCustomError(tokenomics, "RejectMaxBondAdjustment"); - // - // // 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, 0, 0); - // - // // 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"); - // - // // 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); - // - // // 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); - // - // // 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, 0, 0); - // - // // 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); - // - // // 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(); });