From a0737ee2387476dbeffb7d4b3a954865a6606a08 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 10 Jul 2024 18:32:57 +0100 Subject: [PATCH 01/17] refactor: initial additions of aip-1 specs --- contracts/Depository.sol | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/contracts/Depository.sol b/contracts/Depository.sol index f9c7d64c..3e61df8e 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -54,6 +54,7 @@ struct Product { // Supply of remaining OLAS tokens // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 supply; + // Supply } /// @title Bond Depository - Smart contract for OLAS Bond Depository @@ -66,7 +67,7 @@ contract Depository is IErrorsTokenomics { event BondCalculatorUpdated(address indexed bondCalculator); event CreateBond(address indexed token, uint256 indexed productId, address indexed owner, uint256 bondId, uint256 amountOLAS, uint256 tokenAmount, uint256 maturity); - event RedeemBond(uint256 indexed productId, address indexed owner, uint256 bondId); + event RedeemBond(uint256 indexed productId, address indexed owner, uint256 bondId, uint256 payout); event CreateProduct(address indexed token, uint256 indexed productId, uint256 supply, uint256 priceLP, uint256 vesting); event CloseProduct(address indexed token, uint256 indexed productId, uint256 supply); @@ -74,16 +75,18 @@ contract Depository is IErrorsTokenomics { // Minimum bond vesting value uint256 public constant MIN_VESTING = 1 days; // Depository version number - string public constant VERSION = "1.0.1"; + string public constant VERSION = "1.1.0"; // Owner address address public owner; // Individual bond counter // We assume that the number of bonds will not be bigger than the number of seconds - uint32 public bondCounter; + uint256 public bondCounter; // Bond product counter // We assume that the number of products will not be bigger than the number of seconds - uint32 public productCounter; + uint256 public productCounter; + // Minimum amount of supply that is given to the + uint256 public minOLASLeftoverAmount; // OLAS token address address public immutable olas; @@ -326,13 +329,19 @@ contract Depository is IErrorsTokenomics { // Decrease the supply for the amount of payout supply -= payout; + // Adjust payout and set supply to zero if supply drops below the min defined value + if (supply < minOLASLeftoverAmount) { + payout += supply; + supply = 0; + } product.supply = uint96(supply); // Create and add a new bond, update the bond counter bondId = bondCounter; mapUserBonds[bondId] = Bond(msg.sender, uint96(payout), uint32(maturity), uint32(productId)); - bondCounter = uint32(bondId + 1); + bondCounter = bondId + 1; + // TODO OLAS balance check before and after // Deposit that token amount to mint OLAS tokens in exchange ITreasury(treasury).depositTokenForOLAS(msg.sender, tokenAmount, token, payout); @@ -377,7 +386,7 @@ contract Depository is IErrorsTokenomics { // Delete the Bond struct and release the gas delete mapUserBonds[bondIds[i]]; - emit RedeemBond(productId, msg.sender, bondIds[i]); + emit RedeemBond(productId, msg.sender, bondIds[i], pay); } // Check for the non-zero payout From e092d4ee1cf87465d10ac897d03ada3fe5abb3bb Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 11 Jul 2024 15:05:31 +0100 Subject: [PATCH 02/17] feat: enhancing dynamic idf calculation --- contracts/BondCalculator.sol | 176 +++++++++++++++++++++++++++++++++++ contracts/Depository.sol | 7 +- contracts/Tokenomics.sol | 74 ++------------- 3 files changed, 189 insertions(+), 68 deletions(-) create mode 100644 contracts/BondCalculator.sol diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol new file mode 100644 index 00000000..2ee02b17 --- /dev/null +++ b/contracts/BondCalculator.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {mulDiv} from "@prb/math/src/Common.sol"; +import {IVotingEscrow} from "../interfaces/IVotingEscrow.sol"; + +interface ITokenomics { + /// @dev Gets number of new units that were donated in the last epoch. + /// @return Number of new units. + function getLastEpochNumNewUnits() external view returns (uint256); +} + +/// @dev Value overflow. +/// @param provided Overflow value. +/// @param max Maximum possible value. +error Overflow(uint256 provided, uint256 max); + +/// @dev Provided zero address. +error ZeroAddress(); + +// Struct for discount factor params +// The size of the struct is 96 + 32 + 64 = 192 (1 slot) +struct DiscountParams { + uint96 targetVotingPower; + uint32 targetNewUnits; + uint16[4] weightFactors; +} + +// The size of the struct is 160 + 32 + 160 + 96 = 256 + 192 (2 slots) +struct Product { + // priceLP (reserve0 / totalSupply or reserve1 / totalSupply) with 18 additional decimals + // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) + // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced) + uint160 priceLP; + // Bond vesting time + // 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 + uint32 vesting; + // Token to accept as a payment + address token; + // Supply of remaining OLAS tokens + // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 + uint96 supply; + // Current OLAS payout + // This value is bound by the initial total supply + uint96 payout; +} + +/// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. +/// @dev The bond calculation mechanism is based on the UniswapV2Pair contract. +/// @author AL +/// @author Aleksandr Kuperman - +contract GenericBondCalculator { + event OwnerUpdated(address indexed owner); + + // Maximum sum of discount factor weights + uint256 public constant MAX_SUM_WEIGHTS = 10_000; + // OLAS contract address + address public immutable olas; + // veOLAS contract address + address public immutable ve; + // Tokenomics contract address + address public immutable tokenomics; + + // Contract owner + address public owner; + // Discount params + DiscountParams public discountParams; + + + /// @dev Generic Bond Calcolator constructor + /// @param _olas OLAS contract address. + /// @param _tokenomics Tokenomics contract address. + constructor(address _olas, address _ve, address _tokenomics, DiscountParams memory _discountParams) { + // Check for at least one zero contract address + if (_olas == address(0) || _ve == address(0)|| _tokenomics == address(0)) { + revert ZeroAddress(); + } + + olas = _olas; + ve = _ve; + tokenomics = _tokenomics; + owner = msg.sender; + } + + /// @dev Changes contract owner address. + /// @param newOwner Address of a new owner. + function changeOwner(address newOwner) external { + // Check for the contract ownership + if (msg.sender != owner) { + revert OwnerOnly(msg.sender, owner); + } + + // Check for the zero address + if (newOwner == address(0)) { + revert ZeroAddress(); + } + + owner = newOwner; + emit OwnerUpdated(newOwner); + } + + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. + /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. + /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. + /// @param tokenAmount LP token amount. + /// @param priceLP LP token price. + /// @return amountOLAS Resulting amount of OLAS tokens. + /// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max; + function calculatePayoutOLAS(address account, uint256 tokenAmount, uint256 vestingTime, Product memory product) external view + returns (uint256 amountOLAS) + { + // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation + // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; + // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) + // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced); + // tokenAmount is of the order of sqrt(r0*r1) ~ 104 bits (if balanced) or sqrt(96) ~ 10 bits (if max unbalanced); + // overall: 64 + 53 + 104 = 221 < 256 - regular case if LP is balanced, and 64 + 147 + 10 = 221 < 256 if unbalanced + // mulDiv will correctly fit the total amount up to the value of max uint256, i.e., max of priceLP and max of tokenAmount, + // however their multiplication can not be bigger than the max of uint192 + uint256 totalTokenValue = mulDiv(priceLP, tokenAmount, 1); + // Check for the cumulative LP tokens value limit + if (totalTokenValue > type(uint192).max) { + revert Overflow(totalTokenValue, type(uint192).max); + } + + // Calculate the dynamic inverse discount factor + uint256 idf = calculateIDF(); + + // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 + // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 + amountOLAS = idf * totalTokenValue / 1e36; + } + + function calculateIDF( + address account, + uint256 vestingTime, + Product memory product + ) public view returns (uint256 idf) { + // Get the copy of the discount params + DiscountParams memory localParams = discountParams; + uint256 discountBooster; + + // Check the number of new units coming from tokenomics vs the target number of new units + if (localParams.targetNewUnits > 0) { + uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits(); + + // If the number of new units exceeds the target, bound by the target number + if (numNewUnits >= localParams.targetNewUnits) { + numNewUnits = localParams.targetNewUnits; + } + discountBooster = (localParams.weightFactors[0] * numNewUnits * 1e18) / localParams.targetNewUnits; + } + + // Add vesting time discount booster + discountBooster += (localParams.weightFactors[1] * vestingTime * 1e18) / product.vesting; + + // Add product supply discount booster + uint256 productSupply = product.payout + product.supply; + discountBooster += localParams.weightFactors[2] * (1e18 - ((product.payout * 1e18) / productSupply)); + + // Check the veOLAS balance of a bonding account + if (localParams.targetVotingPower > 0) { + uint256 vPower = IVotingEscrow(ve).getVotes(account); + + // If the number of new units exceeds the target, bound by the target number + if (vPower >= localParams.targetVotingPower) { + vPower = localParams.targetVotingPower; + } + discountBooster += (localParams.weightFactors[3] * vPower * 1e18) / localParams.targetVotingPower; + } + + discountBooster /= MAX_SUM_WEIGHTS; + + idf = 1e18 + discountBooster; + } +} diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 3e61df8e..ee12af56 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -54,7 +54,9 @@ struct Product { // Supply of remaining OLAS tokens // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 supply; - // Supply + // Current OLAS payout + // This value is bound by the initial total supply + uint96 payout; } /// @title Bond Depository - Smart contract for OLAS Bond Depository @@ -232,7 +234,7 @@ contract Depository is IErrorsTokenomics { // Push newly created bond product into the list of products productId = productCounter; - mapBondProducts[productId] = Product(uint160(priceLP), uint32(vesting), token, uint96(supply)); + mapBondProducts[productId] = Product(uint160(priceLP), uint32(vesting), token, uint96(supply), 0); // Even if we create a bond product every second, 2^32 - 1 is enough for the next 136 years productCounter = uint32(productId + 1); emit CreateProduct(token, productId, supply, priceLP, vesting); @@ -335,6 +337,7 @@ contract Depository is IErrorsTokenomics { supply = 0; } product.supply = uint96(supply); + product.payout += uint96(payout); // Create and add a new bond, update the bond counter bondId = bondCounter; diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 6416cdd5..cb5b410f 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import {convert, UD60x18} from "@prb/math/src/UD60x18.sol"; import {TokenomicsConstants} from "./TokenomicsConstants.sol"; import {IDonatorBlacklist} from "./interfaces/IDonatorBlacklist.sol"; import {IErrorsTokenomics} from "./interfaces/IErrorsTokenomics.sol"; @@ -182,6 +181,7 @@ struct EpochPoint { // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 totalTopUpsOLAS; // Inverse of the discount factor + // NOTE: This is a legacy parameter now and not used throughout the tokenomics logic // IDF is bound by a factor of 18, since (2^64 - 1) / 10^18 > 18 // IDF uses a multiplier of 10^18 by default, since it is a rational number and must be accounted for divisions // The IDF depends on the epsilonRate value, idf = 1 + epsilonRate, and epsilonRate is bound by 17 with 18 decimals @@ -259,9 +259,8 @@ contract Tokenomics is TokenomicsConstants { event EpochLengthUpdated(uint256 epochLen); event EffectiveBondUpdated(uint256 indexed epochNumber, uint256 effectiveBond); event StakingRefunded(uint256 indexed epochNumber, uint256 amount); - event IDFUpdated(uint256 idf); event TokenomicsParametersUpdateRequested(uint256 indexed epochNumber, uint256 devsPerCapital, uint256 codePerDev, - uint256 epsilonRate, uint256 epochLen, uint256 veOLASThreshold); + uint256 epochLen, uint256 veOLASThreshold); event TokenomicsParametersUpdated(uint256 indexed epochNumber); event IncentiveFractionsUpdateRequested(uint256 indexed epochNumber, uint256 rewardComponentFraction, uint256 rewardAgentFraction, uint256 maxBondFraction, uint256 topUpComponentFraction, @@ -318,6 +317,7 @@ contract Tokenomics is TokenomicsConstants { // Component Registry address public componentRegistry; // Default epsilon rate that contributes to the interest rate: 10% or 0.1 + // NOTE: This is a legacy parameter now and not used throughout the tokenomics logic // We assume that for the IDF calculation epsilonRate must be lower than 17 (with 18 decimals) // (2^64 - 1) / 10^18 > 18, however IDF = 1 + epsilonRate, thus we limit epsilonRate by 17 with 18 decimals at most uint64 public epsilonRate; @@ -627,19 +627,16 @@ contract Tokenomics is TokenomicsConstants { /// @notice Parameter values are not updated for those that are passed as zero or out of defined bounds. /// @param _devsPerCapital Number of valuable devs can be paid per units of capital per epoch. /// @param _codePerDev Number of units of useful code that can be built by a developer during one epoch. - /// @param _epsilonRate Epsilon rate that contributes to the interest rate value. /// @param _epochLen New epoch length. /// #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 <= ONE_YEAR && epochLen != _epochLen) ==> nextEpochLen == _epochLen; /// #if_succeeds {:msg "devsPerCapital"} _devsPerCapital > MIN_PARAM_VALUE && _devsPerCapital <= type(uint72).max ==> devsPerCapital == _devsPerCapital; /// #if_succeeds {:msg "codePerDev"} _codePerDev > MIN_PARAM_VALUE && _codePerDev <= type(uint72).max ==> codePerDev == _codePerDev; - /// #if_succeeds {:msg "epsilonRate"} _epsilonRate > 0 && _epsilonRate < 17e18 ==> epsilonRate == _epsilonRate; /// #if_succeeds {:msg "veOLASThreshold"} _veOLASThreshold > 0 && _veOLASThreshold <= type(uint96).max ==> nextVeOLASThreshold == _veOLASThreshold; function changeTokenomicsParameters( uint256 _devsPerCapital, uint256 _codePerDev, - uint256 _epsilonRate, uint256 _epochLen, uint256 _veOLASThreshold ) external { @@ -664,15 +661,6 @@ contract Tokenomics is TokenomicsConstants { _codePerDev = codePerDev; } - // 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 { - _epsilonRate = epsilonRate; - } - // Check for the epochLen value to change if (uint32(_epochLen) >= MIN_EPOCH_LENGTH && uint32(_epochLen) <= MAX_EPOCH_LENGTH) { nextEpochLen = uint32(_epochLen); @@ -1026,41 +1014,6 @@ contract Tokenomics is TokenomicsConstants { lastDonationBlockNumber = uint32(block.number); } - /// @dev Gets the inverse discount factor value. - /// @param treasuryRewards Treasury rewards. - /// @param numNewOwners Number of new owners of components / agents registered during the epoch. - /// @return idf IDF value. - function _calculateIDF(uint256 treasuryRewards, uint256 numNewOwners) internal view returns (uint256 idf) { - idf = 0; - // Calculate the inverse discount factor based on the tokenomics parameters and values of units per epoch - // df = 1 / (1 + iterest_rate), idf = (1 + iterest_rate) >= 1.0 - // Calculate IDF from epsilon rate and f(K,D) - // f(K(e), D(e)) = d * k * K(e) + d * D(e), - // where d corresponds to codePerDev and k corresponds to devPerCapital - // codeUnits (codePerDev) is the estimated value of the code produced by a single developer for epoch - UD60x18 codeUnits = UD60x18.wrap(codePerDev); - // fKD = codeUnits * devsPerCapital * treasuryRewards + codeUnits * newOwners; - // Convert all the necessary values to fixed-point numbers considering OLAS decimals (18 by default) - UD60x18 fp = UD60x18.wrap(treasuryRewards); - // Convert devsPerCapital - UD60x18 fpDevsPerCapital = UD60x18.wrap(devsPerCapital); - fp = fp.mul(fpDevsPerCapital); - UD60x18 fpNumNewOwners = convert(numNewOwners); - fp = fp.add(fpNumNewOwners); - fp = fp.mul(codeUnits); - // fp = fp / 100 - calculate the final value in fixed point - fp = fp.div(UD60x18.wrap(100e18)); - // fKD in the state that is comparable with epsilon rate - uint256 fKD = UD60x18.unwrap(fp); - - // Compare with epsilon rate and choose the smallest one - if (fKD > epsilonRate) { - fKD = epsilonRate; - } - // 1 + fKD in the system where 1e18 is equal to a whole unit (18 decimals) - idf = 1e18 + fKD; - } - /// @dev Record global data with a new checkpoint. /// @notice Note that even though a specific epoch can last longer than the epochLen, it is practically /// not valid not to call a checkpoint for longer than a year. Thus, the function will return false otherwise. @@ -1072,7 +1025,6 @@ contract Tokenomics is TokenomicsConstants { /// #if_succeeds {:msg "when the year is the same, the adjusted maxBond (incentives[4]) will never be lower than the epoch maxBond"} ///$result == true && (block.timestamp - timeLaunch) / ONE_YEAR == old(currentYear) /// ==> old((inflationPerSecond * (block.timestamp - mapEpochTokenomics[epochCounter - 1].epochPoint.endTime) * mapEpochTokenomics[epochCounter].epochPoint.maxBondFraction) / 100) >= old(maxBond); - /// #if_succeeds {:msg "idf check"} $result == true ==> mapEpochTokenomics[epochCounter].epochPoint.idf >= 1e18 && mapEpochTokenomics[epochCounter].epochPoint.idf <= 18e18; /// #if_succeeds {:msg "devsPerCapital check"} $result == true ==> devsPerCapital > MIN_PARAM_VALUE; /// #if_succeeds {:msg "codePerDev check"} $result == true ==> codePerDev > MIN_PARAM_VALUE; /// #if_succeeds {:msg "sum of reward fractions must result in 100"} $result == true @@ -1270,17 +1222,6 @@ contract Tokenomics is TokenomicsConstants { curMaxBond += effectiveBond; effectiveBond = uint96(curMaxBond); - // Update the IDF value for the next epoch or assign a default one if there are no ETH donations - if (incentives[0] > 0) { - // Calculate IDF based on the incoming donations - uint256 idf = _calculateIDF(incentives[1], tp.epochPoint.numNewOwners); - nextEpochPoint.epochPoint.idf = uint64(idf); - emit IDFUpdated(idf); - } else { - // Assign a default IDF value - nextEpochPoint.epochPoint.idf = 1e18; - } - // Treasury contract rebalances ETH funds depending on the treasury rewards if (incentives[1] == 0 || ITreasury(treasury).rebalanceTreasury(incentives[1])) { // Emit settled epoch written to the last economics point @@ -1467,10 +1408,11 @@ contract Tokenomics is TokenomicsConstants { return mapEpochTokenomics[epoch].unitPoints[unitType]; } - /// @dev Gets inverse discount factor with the multiple of 1e18 of the last epoch. - /// @return Discount factor with the multiple of 1e18. - function getLastIDF() external view returns (uint256) { - return mapEpochTokenomics[epochCounter].epochPoint.idf; + /// @dev Gets number of new units that were donated in the last epoch. + /// @return Number of new units. + function getLastEpochNumNewUnits() external view returns (uint256) { + uint256 eCounter = epochCounter - 1; + return mapEpochTokenomics[eCounter].unitPoints[0] + mapEpochTokenomics[eCounter].unitPoints[1]; } /// @dev Gets epoch end time. From 8280452979d71aae7b8b379e04c5fe72145b13f0 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 11 Jul 2024 16:13:28 +0100 Subject: [PATCH 03/17] feat: enhancing dynamic idf calculation --- contracts/BondCalculator.sol | 42 ++++++++++++++++++---------- contracts/Depository.sol | 54 ++++++++++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index 2ee02b17..f3308701 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -32,17 +32,17 @@ struct Product { // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced) uint160 priceLP; - // Bond vesting time - // 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 - uint32 vesting; - // Token to accept as a payment - address token; // Supply of remaining OLAS tokens // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 supply; + // Token to accept as a payment + address token; // Current OLAS payout // This value is bound by the initial total supply uint96 payout; + // Max bond vesting time + // 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 + uint32 vesting; } /// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. @@ -104,11 +104,21 @@ contract GenericBondCalculator { /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. /// @param tokenAmount LP token amount. /// @param priceLP LP token price. + /// @param bondVestingTime Bond vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. /// @return amountOLAS Resulting amount of OLAS tokens. /// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max; - function calculatePayoutOLAS(address account, uint256 tokenAmount, uint256 vestingTime, Product memory product) external view - returns (uint256 amountOLAS) - { + function calculatePayoutOLAS( + address account, + uint256 tokenAmount, + uint256 priceLP, + uint256 bondVestingTime, + uint256 productMaxVestingTime, + uint256 productSupply, + uint256 productPayout + ) external view returns (uint256 amountOLAS) { // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) @@ -124,17 +134,19 @@ contract GenericBondCalculator { } // Calculate the dynamic inverse discount factor - uint256 idf = calculateIDF(); + uint256 idf = calculateIDF(account, bondVestingTime, productMaxVestingTime, productSupply, productPayout); // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 - amountOLAS = idf * totalTokenValue / 1e36; + amountOLAS = (idf * totalTokenValue) / 1e36; } function calculateIDF( address account, - uint256 vestingTime, - Product memory product + uint256 bondVestingTime, + uint256 productMaxVestingTime, + uint256 productSupply, + uint256 productPayout ) public view returns (uint256 idf) { // Get the copy of the discount params DiscountParams memory localParams = discountParams; @@ -152,11 +164,11 @@ contract GenericBondCalculator { } // Add vesting time discount booster - discountBooster += (localParams.weightFactors[1] * vestingTime * 1e18) / product.vesting; + discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; // Add product supply discount booster - uint256 productSupply = product.payout + product.supply; - discountBooster += localParams.weightFactors[2] * (1e18 - ((product.payout * 1e18) / productSupply)); + productSupply = productSupply + productPayout; + discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); // Check the veOLAS balance of a bonding account if (localParams.targetVotingPower > 0) { diff --git a/contracts/Depository.sol b/contracts/Depository.sol index ee12af56..b9af97b8 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -2,11 +2,32 @@ pragma solidity ^0.8.20; import {IErrorsTokenomics} from "./interfaces/IErrorsTokenomics.sol"; -import {IGenericBondCalculator} from "./interfaces/IGenericBondCalculator.sol"; import {IToken} from "./interfaces/IToken.sol"; import {ITokenomics} from "./interfaces/ITokenomics.sol"; import {ITreasury} from "./interfaces/ITreasury.sol"; +interface IBondCalculator { + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. + /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. + /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. + /// @param tokenAmount LP token amount. + /// @param priceLP LP token price. + /// @param bondVestingTime Bond vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. + /// @return amountOLAS Resulting amount of OLAS tokens. + function calculatePayoutOLAS( + address account, + uint256 tokenAmount, + uint256 priceLP, + uint256 bondVestingTime, + uint256 productMaxVestingTime, + uint256 productSupply, + uint256 productPayout + ) external view returns (uint256 amountOLAS); +} + /* * In this contract we consider OLAS tokens. The initial numbers will be as follows: * - For the first 10 years there will be the cap of 1 billion (1e27) tokens; @@ -40,23 +61,23 @@ struct Bond { uint32 productId; } -// The size of the struct is 160 + 32 + 160 + 96 = 256 + 192 (2 slots) +// The size of the struct is 160 + 96 + 160 + 96 + 32 = 2 * 256 + 32 (3 slots) struct Product { // priceLP (reserve0 / totalSupply or reserve1 / totalSupply) with 18 additional decimals // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced) uint160 priceLP; - // Bond vesting time - // 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 - uint32 vesting; - // Token to accept as a payment - address token; // Supply of remaining OLAS tokens // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 supply; + // Token to accept as a payment + address token; // Current OLAS payout // This value is bound by the initial total supply uint96 payout; + // Max bond vesting time + // 2^32 - 1 is enough to count 136 years starting from the year of 1970. This counter is safe until the year of 2106 + uint32 vesting; } /// @title Bond Depository - Smart contract for OLAS Bond Depository @@ -234,9 +255,9 @@ contract Depository is IErrorsTokenomics { // Push newly created bond product into the list of products productId = productCounter; - mapBondProducts[productId] = Product(uint160(priceLP), uint32(vesting), token, uint96(supply), 0); + mapBondProducts[productId] = Product(uint160(priceLP), uint96(supply), token, 0, uint32(vesting)); // Even if we create a bond product every second, 2^32 - 1 is enough for the next 136 years - productCounter = uint32(productId + 1); + productCounter = productId + 1; emit CreateProduct(token, productId, supply, priceLP, vesting); } @@ -293,7 +314,7 @@ contract Depository is IErrorsTokenomics { /// #if_succeeds {:msg "bond Id"} bondCounter == old(bondCounter) + 1 && bondCounter <= type(uint32).max; /// #if_succeeds {:msg "payout"} old(mapBondProducts[productId].supply) == mapBondProducts[productId].supply + payout; /// #if_succeeds {:msg "OLAS balances"} IToken(mapBondProducts[productId].token).balanceOf(treasury) == old(IToken(mapBondProducts[productId].token).balanceOf(treasury)) + tokenAmount; - function deposit(uint256 productId, uint256 tokenAmount) external + function deposit(uint256 productId, uint256 tokenAmount, uint256 bondVestingTime) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // Check the token amount @@ -310,8 +331,16 @@ contract Depository is IErrorsTokenomics { revert ProductClosed(productId); } + uint256 productMaxVestingTime = product.vesting; + // Calculate vesting limits + if (bondVestingTime < MIN_VESTING) { + revert LowerThan(bondVestingTime, MIN_VESTING); + } + if (vestingTime > productMaxVestingTime) { + revert Overflow(vestingTime, productMaxVestingTime); + } // Calculate the bond maturity based on its vesting time - maturity = block.timestamp + product.vesting; + maturity = block.timestamp + productMaxVestingTime; // Check for the time limits if (maturity > type(uint32).max) { revert Overflow(maturity, type(uint32).max); @@ -322,7 +351,8 @@ contract Depository is IErrorsTokenomics { // Calculate the payout in OLAS tokens based on the LP pair with the discount factor (DF) calculation // Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created - payout = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); + payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP, bondVestingTime, + productMaxVestingTime, supply, product.payout); // Check for the sufficient supply if (payout > supply) { From dc5a55179e53ebdefc334404da99753f80aab451 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 11 Jul 2024 17:54:52 +0100 Subject: [PATCH 04/17] feat: enhancing dynamic idf calculation --- contracts/BondCalculator.sol | 79 +++++++++++++++++++++++++++++-- contracts/Depository.sol | 31 +++++++----- contracts/Tokenomics.sol | 5 +- contracts/test/TestTokenomics.sol | 12 ++--- 4 files changed, 102 insertions(+), 25 deletions(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index f3308701..56b1031d 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.25; import {mulDiv} from "@prb/math/src/Common.sol"; -import {IVotingEscrow} from "../interfaces/IVotingEscrow.sol"; +import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; interface ITokenomics { /// @dev Gets number of new units that were donated in the last epoch. @@ -10,6 +10,11 @@ interface ITokenomics { function getLastEpochNumNewUnits() external view returns (uint256); } +/// @dev Only `owner` has a privilege, but the `sender` was provided. +/// @param sender Sender address. +/// @param owner Required sender address as an owner. +error OwnerOnly(address sender, address owner); + /// @dev Value overflow. /// @param provided Overflow value. /// @param max Maximum possible value. @@ -19,10 +24,16 @@ error Overflow(uint256 provided, uint256 max); error ZeroAddress(); // Struct for discount factor params -// The size of the struct is 96 + 32 + 64 = 192 (1 slot) +// The size of the struct is 96 + 64 + 64 = 224 (1 slot) struct DiscountParams { + // DAO set voting power limit for the bonding account + // This value is bound by the veOLAS total voting power uint96 targetVotingPower; - uint32 targetNewUnits; + // DAO set number of new units per epoch limit + // This number is bound by the total number of possible components and agents + uint64 targetNewUnits; + // DAO set weight factors + // The sum of factors cannot exceed the value of 10_000 (100% with a 0.01% step) uint16[4] weightFactors; } @@ -51,6 +62,7 @@ struct Product { /// @author Aleksandr Kuperman - contract GenericBondCalculator { event OwnerUpdated(address indexed owner); + event DiscountParamsUpdated(DiscountParams newDiscountParams); // Maximum sum of discount factor weights uint256 public constant MAX_SUM_WEIGHTS = 10_000; @@ -72,7 +84,7 @@ contract GenericBondCalculator { /// @param _tokenomics Tokenomics contract address. constructor(address _olas, address _ve, address _tokenomics, DiscountParams memory _discountParams) { // Check for at least one zero contract address - if (_olas == address(0) || _ve == address(0)|| _tokenomics == address(0)) { + if (_olas == address(0) || _ve == address(0) || _tokenomics == address(0)) { revert ZeroAddress(); } @@ -80,6 +92,23 @@ contract GenericBondCalculator { ve = _ve; tokenomics = _tokenomics; owner = msg.sender; + + // Check discount factor limits + if (_discountParams.targetVotingPower > type(uint96).max) { + revert Overflow(_discountParams.targetVotingPower, type(uint96).max); + } + if (_discountParams.targetNewUnits > type(uint64).max) { + revert Overflow(_discountParams.targetNewUnits, type(uint64).max); + } + // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) + uint256 sumWeights; + for (uint256 i = 0; i < _discountParams.weightFactors.length; ++i) { + sumWeights += _discountParams.weightFactors[i]; + } + if (sumWeights > MAX_SUM_WEIGHTS) { + revert Overflow(sumWeights, MAX_SUM_WEIGHTS); + } + discountParams = _discountParams; } /// @dev Changes contract owner address. @@ -99,6 +128,35 @@ contract GenericBondCalculator { emit OwnerUpdated(newOwner); } + /// @dev Changed inverse discount factor parameters. + /// @param newDiscountParams Struct of new discount parameters. + function changeDiscountParams(DiscountParams memory newDiscountParams) external { + // Check for the contract ownership + if (msg.sender != owner) { + revert OwnerOnly(msg.sender, owner); + } + + // Check discount factor limits + if (newDiscountParams.targetVotingPower > type(uint96).max) { + revert Overflow(newDiscountParams.targetVotingPower, type(uint96).max); + } + if (newDiscountParams.targetNewUnits > type(uint64).max) { + revert Overflow(newDiscountParams.targetNewUnits, type(uint64).max); + } + // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) + uint256 sumWeights; + for (uint256 i = 0; i < newDiscountParams.weightFactors.length; ++i) { + sumWeights += newDiscountParams.weightFactors[i]; + } + if (sumWeights > MAX_SUM_WEIGHTS) { + revert Overflow(sumWeights, MAX_SUM_WEIGHTS); + } + + discountParams = newDiscountParams; + + emit DiscountParamsUpdated(newDiscountParams); + } + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. @@ -141,6 +199,13 @@ contract GenericBondCalculator { amountOLAS = (idf * totalTokenValue) / 1e36; } + /// @dev Calculated inverse discount factor based on bonding and account parameters. + /// @param account Bonding account address. + /// @param bondVestingTime Bonding desired vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. + /// @return idf Inverse discount factor in 18 decimals format. function calculateIDF( address account, uint256 bondVestingTime, @@ -152,6 +217,7 @@ contract GenericBondCalculator { DiscountParams memory localParams = discountParams; uint256 discountBooster; + // First discount booster: booster = k1 * NumNewUnits(previous epoch) / TargetNewUnits(previous epoch) // Check the number of new units coming from tokenomics vs the target number of new units if (localParams.targetNewUnits > 0) { uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits(); @@ -163,13 +229,16 @@ contract GenericBondCalculator { discountBooster = (localParams.weightFactors[0] * numNewUnits * 1e18) / localParams.targetNewUnits; } + // Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime // Add vesting time discount booster discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; + // Third discount booster: booster += k3 * (1 - productPayout(at bonding time) / productSupply) // Add product supply discount booster productSupply = productSupply + productPayout; discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); + // Fourth discount booster: booster += k4 * getVotes(bonding account) / targetVotingPower // Check the veOLAS balance of a bonding account if (localParams.targetVotingPower > 0) { uint256 vPower = IVotingEscrow(ve).getVotes(account); @@ -181,8 +250,10 @@ contract GenericBondCalculator { discountBooster += (localParams.weightFactors[3] * vPower * 1e18) / localParams.targetVotingPower; } + // Normalize discount booster by the max sum of weights discountBooster /= MAX_SUM_WEIGHTS; + // IDF = 1 + normalized booster idf = 1e18 + discountBooster; } } diff --git a/contracts/Depository.sol b/contracts/Depository.sol index b9af97b8..af20edaf 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.25; import {IErrorsTokenomics} from "./interfaces/IErrorsTokenomics.sol"; import {IToken} from "./interfaces/IToken.sol"; @@ -28,6 +28,11 @@ interface IBondCalculator { ) external view returns (uint256 amountOLAS); } +/// @dev Wrong amount received / provided. +/// @param provided Provided amount. +/// @param expected Expected amount. +error WrongAmount(uint256 provided, uint256 expected); + /* * In this contract we consider OLAS tokens. The initial numbers will be as follows: * - For the first 10 years there will be the cap of 1 billion (1e27) tokens; @@ -336,8 +341,8 @@ contract Depository is IErrorsTokenomics { if (bondVestingTime < MIN_VESTING) { revert LowerThan(bondVestingTime, MIN_VESTING); } - if (vestingTime > productMaxVestingTime) { - revert Overflow(vestingTime, productMaxVestingTime); + if (bondVestingTime > productMaxVestingTime) { + revert Overflow(bondVestingTime, productMaxVestingTime); } // Calculate the bond maturity based on its vesting time maturity = block.timestamp + productMaxVestingTime; @@ -351,8 +356,8 @@ contract Depository is IErrorsTokenomics { // Calculate the payout in OLAS tokens based on the LP pair with the discount factor (DF) calculation // Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created - payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP, bondVestingTime, - productMaxVestingTime, supply, product.payout); + payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, + bondVestingTime, productMaxVestingTime, supply, product.payout); // Check for the sufficient supply if (payout > supply) { @@ -374,10 +379,17 @@ contract Depository is IErrorsTokenomics { mapUserBonds[bondId] = Bond(msg.sender, uint96(payout), uint32(maturity), uint32(productId)); bondCounter = bondId + 1; - // TODO OLAS balance check before and after + uint256 olasBalance = IToken(olas).balanceOf(address(this)); // Deposit that token amount to mint OLAS tokens in exchange ITreasury(treasury).depositTokenForOLAS(msg.sender, tokenAmount, token, payout); + // Check the balance after the OLAS mint + olasBalance = IToken(olas).balanceOf(address(this)) - olasBalance; + + if (olasBalance != payout) { + revert WrongAmount(olasBalance, payout); + } + // Close the product if the supply becomes zero if (supply == 0) { delete mapBondProducts[productId]; @@ -526,11 +538,4 @@ contract Depository is IErrorsTokenomics { matured = block.timestamp >= mapUserBonds[bondId].maturity; } } - - /// @dev Gets current reserves of OLAS / totalSupply of LP tokens. - /// @param token Token address. - /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. - function getCurrentPriceLP(address token) external view returns (uint256 priceLP) { - return IGenericBondCalculator(bondCalculator).getCurrentPriceLP(token); - } } diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index cb5b410f..76eb6f51 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -677,7 +677,7 @@ contract Tokenomics is TokenomicsConstants { // Set the flag that tokenomics parameters are requested to be updated (1st bit is set to one) tokenomicsParametersUpdated = tokenomicsParametersUpdated | 0x01; - emit TokenomicsParametersUpdateRequested(epochCounter + 1, _devsPerCapital, _codePerDev, _epsilonRate, _epochLen, + emit TokenomicsParametersUpdateRequested(epochCounter + 1, _devsPerCapital, _codePerDev, _epochLen, _veOLASThreshold); } @@ -1412,7 +1412,8 @@ contract Tokenomics is TokenomicsConstants { /// @return Number of new units. function getLastEpochNumNewUnits() external view returns (uint256) { uint256 eCounter = epochCounter - 1; - return mapEpochTokenomics[eCounter].unitPoints[0] + mapEpochTokenomics[eCounter].unitPoints[1]; + return mapEpochTokenomics[eCounter].unitPoints[0].numNewUnits + + mapEpochTokenomics[eCounter].unitPoints[1].numNewUnits; } /// @dev Gets epoch end time. diff --git a/contracts/test/TestTokenomics.sol b/contracts/test/TestTokenomics.sol index dd138848..5272c48a 100644 --- a/contracts/test/TestTokenomics.sol +++ b/contracts/test/TestTokenomics.sol @@ -120,7 +120,7 @@ contract TestTokenomics { // Enable LP token in treasury treasury.enableToken(pair); - priceLP = depository.getCurrentPriceLP(pair); + priceLP = genericBondCalculator.getCurrentPriceLP(pair); // Give a large approval for treasury ZuniswapV2Pair(pair).approve(address(treasury), largeApproval); @@ -129,7 +129,7 @@ contract TestTokenomics { productId = depository.create(pair, priceLP, supplyProductOLAS, vesting); // Deposit to one bond - (, , bondId) = depository.deposit(productId, 1_000 ether); + (, , bondId) = depository.deposit(productId, 1_000 ether, vesting); } @@ -172,25 +172,25 @@ contract TestTokenomics { /// @dev Deposit LP token to the bond product with the max of uint96 tokenAmount. function depositBond96Id0(uint96 tokenAmount) external { if (tokenAmount < ZuniswapV2Pair(pair).balanceOf(address(this))) { - depository.deposit(0, tokenAmount); + depository.deposit(0, tokenAmount, vesting); } } /// @dev Deposit LP token to the bond product with the max of uint96 tokenAmount. function depositBond96(uint96 tokenAmount) external { if (tokenAmount < ZuniswapV2Pair(pair).balanceOf(address(this))) { - depository.deposit(productId, tokenAmount); + depository.deposit(productId, tokenAmount, vesting); } } /// @dev Deposit LP token to the bond product. function depositBond256Id0(uint256 tokenAmount) external { - depository.deposit(0, tokenAmount); + depository.deposit(0, tokenAmount, vesting); } /// @dev Deposit LP token to the bond product. function depositBond256(uint256 tokenAmount) external { - depository.deposit(productId, tokenAmount); + depository.deposit(productId, tokenAmount, vesting); } /// @dev Redeem OLAS from the bond program. From 2ef7e3ad66d0d13417258fffa160c72713641595 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 11 Jul 2024 19:30:45 +0100 Subject: [PATCH 05/17] refactor: make bonds ERC721 tokens --- contracts/Depository.sol | 86 ++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/contracts/Depository.sol b/contracts/Depository.sol index af20edaf..7ec5f6ef 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; +import {ERC721} from "../lib/solmate/src/tokens/ERC721.sol"; import {IErrorsTokenomics} from "./interfaces/IErrorsTokenomics.sol"; import {IToken} from "./interfaces/IToken.sol"; import {ITokenomics} from "./interfaces/ITokenomics.sol"; @@ -51,10 +52,8 @@ error WrongAmount(uint256 provided, uint256 expected); * In conclusion, this contract is only safe to use until 2106. */ -// The size of the struct is 160 + 96 + 32 * 2 = 256 + 64 (2 slots) +// The size of the struct is 96 + 32 * 2 = 160 (1 slot) struct Bond { - // Account address - address account; // OLAS remaining to be paid out // After 10 years, the OLAS inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 uint96 payout; @@ -88,7 +87,7 @@ struct Product { /// @title Bond Depository - Smart contract for OLAS Bond Depository /// @author AL /// @author Aleksandr Kuperman - -contract Depository is IErrorsTokenomics { +contract Depository is ERC721, IErrorsTokenomics { event OwnerUpdated(address indexed owner); event TokenomicsUpdated(address indexed tokenomics); event TreasuryUpdated(address indexed treasury); @@ -104,12 +103,13 @@ contract Depository is IErrorsTokenomics { uint256 public constant MIN_VESTING = 1 days; // Depository version number string public constant VERSION = "1.1.0"; - + // Base URI + string public baseURI; // Owner address address public owner; // Individual bond counter // We assume that the number of bonds will not be bigger than the number of seconds - uint256 public bondCounter; + uint256 public totalSupply; // Bond product counter // We assume that the number of products will not be bigger than the number of seconds uint256 public productCounter; @@ -131,21 +131,39 @@ contract Depository is IErrorsTokenomics { mapping(uint256 => Product) public mapBondProducts; /// @dev Depository constructor. + /// @param _name Service contract name. + /// @param _symbol Agent contract symbol. + /// @param _baseURI Agent registry token base URI. /// @param _olas OLAS token address. /// @param _treasury Treasury address. /// @param _tokenomics Tokenomics address. - constructor(address _olas, address _tokenomics, address _treasury, address _bondCalculator) + constructor( + string memory _name, + string memory _symbol, + string memory _baseURI, + address _olas, + address _tokenomics, + address _treasury, + address _bondCalculator + ) + ERC721(_name, _symbol) { - owner = msg.sender; - // Check for at least one zero contract address if (_olas == address(0) || _tokenomics == address(0) || _treasury == address(0) || _bondCalculator == address(0)) { revert ZeroAddress(); } + + // Check for base URI zero value + if (bytes(_baseURI).length == 0) { + revert ZeroValue(); + } + olas = _olas; tokenomics = _tokenomics; treasury = _treasury; bondCalculator = _bondCalculator; + baseURI = _baseURI; + owner = msg.sender; } /// @dev Changes the owner address. @@ -316,7 +334,7 @@ contract Depository is IErrorsTokenomics { /// #if_succeeds {:msg "token is valid"} mapBondProducts[productId].token != address(0); /// #if_succeeds {:msg "input supply is non-zero"} old(mapBondProducts[productId].supply) > 0 && mapBondProducts[productId].supply <= type(uint96).max; /// #if_succeeds {:msg "vesting is non-zero"} mapBondProducts[productId].vesting > 0 && mapBondProducts[productId].vesting + block.timestamp <= type(uint32).max; - /// #if_succeeds {:msg "bond Id"} bondCounter == old(bondCounter) + 1 && bondCounter <= type(uint32).max; + /// #if_succeeds {:msg "bond Id"} totalSupply == old(totalSupply) + 1 && totalSupply <= type(uint32).max; /// #if_succeeds {:msg "payout"} old(mapBondProducts[productId].supply) == mapBondProducts[productId].supply + payout; /// #if_succeeds {:msg "OLAS balances"} IToken(mapBondProducts[productId].token).balanceOf(treasury) == old(IToken(mapBondProducts[productId].token).balanceOf(treasury)) + tokenAmount; function deposit(uint256 productId, uint256 tokenAmount, uint256 bondVestingTime) external @@ -345,7 +363,7 @@ contract Depository is IErrorsTokenomics { revert Overflow(bondVestingTime, productMaxVestingTime); } // Calculate the bond maturity based on its vesting time - maturity = block.timestamp + productMaxVestingTime; + maturity = block.timestamp + bondVestingTime; // Check for the time limits if (maturity > type(uint32).max) { revert Overflow(maturity, type(uint32).max); @@ -374,10 +392,14 @@ contract Depository is IErrorsTokenomics { product.supply = uint96(supply); product.payout += uint96(payout); - // Create and add a new bond, update the bond counter - bondId = bondCounter; - mapUserBonds[bondId] = Bond(msg.sender, uint96(payout), uint32(maturity), uint32(productId)); - bondCounter = bondId + 1; + // Create and mint a new bond + bondId = totalSupply; + // Safe mint is needed since contracts can create bonds as well + _safeMint(msg.sender, bondId); + mapUserBonds[bondId] = Bond(uint96(payout), uint32(maturity), uint32(productId)); + + // Increase bond total supply + totalSupply = bondId + 1; uint256 olasBalance = IToken(olas).balanceOf(address(this)); // Deposit that token amount to mint OLAS tokens in exchange @@ -403,8 +425,8 @@ contract Depository is IErrorsTokenomics { /// @param bondIds Bond Ids to redeem. /// @return payout Total payout sent in OLAS tokens. /// #if_succeeds {:msg "payout > 0"} payout > 0; - /// #if_succeeds {:msg "msg.sender is the only owner"} old(forall (uint k in bondIds) mapUserBonds[bondIds[k]].account == msg.sender); - /// #if_succeeds {:msg "accounts deleted"} forall (uint k in bondIds) mapUserBonds[bondIds[k]].account == address(0); + /// #if_succeeds {:msg "msg.sender is the only owner"} old(forall (uint k in bondIds) _ownerOf[bondIds[k]] == msg.sender); + /// #if_succeeds {:msg "accounts deleted"} forall (uint k in bondIds) _ownerOf[bondIds[k]].account == address(0); /// #if_succeeds {:msg "payouts are zeroed"} forall (uint k in bondIds) mapUserBonds[bondIds[k]].payout == 0; /// #if_succeeds {:msg "maturities are zeroed"} forall (uint k in bondIds) mapUserBonds[bondIds[k]].maturity == 0; function redeem(uint256[] memory bondIds) external returns (uint256 payout) { @@ -419,8 +441,9 @@ contract Depository is IErrorsTokenomics { } // Check that the msg.sender is the owner of the bond - if (mapUserBonds[bondIds[i]].account != msg.sender) { - revert OwnerOnly(msg.sender, mapUserBonds[bondIds[i]].account); + address bondOwner = _ownerOf[bondIds[i]]; + if (bondOwner != msg.sender) { + revert OwnerOnly(msg.sender, bondOwner); } // Increase the payout @@ -429,6 +452,9 @@ contract Depository is IErrorsTokenomics { // Get the productId uint256 productId = mapUserBonds[bondIds[i]].productId; + // Burn the bond NFT + _burn(bondIds[i]); + // Delete the Bond struct and release the gas delete mapUserBonds[bondIds[i]]; emit RedeemBond(productId, msg.sender, bondIds[i], pay); @@ -496,13 +522,13 @@ contract Depository is IErrorsTokenomics { uint256 numAccountBonds; // Calculate the number of pending bonds - uint256 numBonds = bondCounter; + uint256 numBonds = totalSupply; bool[] memory positions = new bool[](numBonds); // Record the bond number if it belongs to the account address and was not yet redeemed for (uint256 i = 0; i < numBonds; ++i) { // Check if the bond belongs to the account // If not and the address is zero, the bond was redeemed or never existed - if (mapUserBonds[i].account == account) { + if (_ownerOf[i] == account) { // Check if requested bond is not matured but owned by the account address if (!matured || // Or if the requested bond is matured, i.e., the bond maturity timestamp passed @@ -538,4 +564,22 @@ contract Depository is IErrorsTokenomics { matured = block.timestamp >= mapUserBonds[bondId].maturity; } } + + /// @dev Gets the valid bond Id from the provided index. + /// @param id Bond counter. + /// @return Bond Id. + function tokenByIndex(uint256 id) external view returns (uint256) { + if (id >= totalSupply) { + revert Overflow(id, totalSupply - 1); + } + + return id; + } + + /// @dev Returns bond token URI. + /// @param bondId Bond Id. + /// @return Bond token URI string. + function tokenURI(uint256 bondId) public view override returns (string memory) { + return string(abi.encodePacked(baseURI, bondId)); + } } From 0c254b1470e5ec59e661027e80f3adee3abe3d2d Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 11 Jul 2024 19:35:34 +0100 Subject: [PATCH 06/17] refactor: simplifying bond checks --- contracts/BondCalculator.sol | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index 56b1031d..8879e0a7 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -92,14 +92,7 @@ contract GenericBondCalculator { ve = _ve; tokenomics = _tokenomics; owner = msg.sender; - - // Check discount factor limits - if (_discountParams.targetVotingPower > type(uint96).max) { - revert Overflow(_discountParams.targetVotingPower, type(uint96).max); - } - if (_discountParams.targetNewUnits > type(uint64).max) { - revert Overflow(_discountParams.targetNewUnits, type(uint64).max); - } + // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) uint256 sumWeights; for (uint256 i = 0; i < _discountParams.weightFactors.length; ++i) { @@ -136,13 +129,6 @@ contract GenericBondCalculator { revert OwnerOnly(msg.sender, owner); } - // Check discount factor limits - if (newDiscountParams.targetVotingPower > type(uint96).max) { - revert Overflow(newDiscountParams.targetVotingPower, type(uint96).max); - } - if (newDiscountParams.targetNewUnits > type(uint64).max) { - revert Overflow(newDiscountParams.targetNewUnits, type(uint64).max); - } // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) uint256 sumWeights; for (uint256 i = 0; i < newDiscountParams.weightFactors.length; ++i) { From 21bf1d9349836caa78d43ce4473c29a800b92cdf Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 12 Jul 2024 12:18:16 +0100 Subject: [PATCH 07/17] refactor and test: making bond calculator inherit the generic one, correcting tests --- contracts/BondCalculator.sol | 97 +++++++++++++++-------------- contracts/Depository.sol | 34 +++++----- contracts/GenericBondCalculator.sol | 23 ++++--- contracts/test/DepositAttacker.sol | 11 ++-- test/Depository.js | 51 +++++++-------- test/Depository2.js | 45 ++++++------- 6 files changed, 135 insertions(+), 126 deletions(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index 8879e0a7..f478cb9c 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.25; import {mulDiv} from "@prb/math/src/Common.sol"; +import {GenericBondCalculator} from "./GenericBondCalculator.sol"; import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; interface ITokenomics { @@ -23,6 +24,9 @@ error Overflow(uint256 provided, uint256 max); /// @dev Provided zero address. error ZeroAddress(); +/// @dev Provided zero value. +error ZeroValue(); + // Struct for discount factor params // The size of the struct is 96 + 64 + 64 = 224 (1 slot) struct DiscountParams { @@ -56,22 +60,18 @@ struct Product { uint32 vesting; } -/// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. -/// @dev The bond calculation mechanism is based on the UniswapV2Pair contract. -/// @author AL +/// @title BondCalculator - Smart contract for bond calculation payout in exchange for OLAS tokens based on dynamic IDF. /// @author Aleksandr Kuperman - -contract GenericBondCalculator { +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - +contract BondCalculator is GenericBondCalculator { event OwnerUpdated(address indexed owner); event DiscountParamsUpdated(DiscountParams newDiscountParams); // Maximum sum of discount factor weights uint256 public constant MAX_SUM_WEIGHTS = 10_000; - // OLAS contract address - address public immutable olas; // veOLAS contract address address public immutable ve; - // Tokenomics contract address - address public immutable tokenomics; // Contract owner address public owner; @@ -79,20 +79,26 @@ contract GenericBondCalculator { DiscountParams public discountParams; - /// @dev Generic Bond Calcolator constructor + /// @dev Bond Calculator constructor. /// @param _olas OLAS contract address. /// @param _tokenomics Tokenomics contract address. - constructor(address _olas, address _ve, address _tokenomics, DiscountParams memory _discountParams) { - // Check for at least one zero contract address - if (_olas == address(0) || _ve == address(0) || _tokenomics == address(0)) { + /// @param _ve veOLAS contract address. + /// @param _discountParams Discount factor parameters. + constructor(address _olas, address _tokenomics, address _ve, DiscountParams memory _discountParams) + GenericBondCalculator(_olas, _tokenomics) + { + // Check for zero address + if (_ve == address(0)) { revert ZeroAddress(); } - olas = _olas; ve = _ve; - tokenomics = _tokenomics; owner = msg.sender; - + + // Check for zero values + if (_discountParams.targetNewUnits == 0 || _discountParams.targetVotingPower == 0) { + revert ZeroValue(); + } // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) uint256 sumWeights; for (uint256 i = 0; i < _discountParams.weightFactors.length; ++i) { @@ -129,6 +135,10 @@ contract GenericBondCalculator { revert OwnerOnly(msg.sender, owner); } + // Check for zero values + if (newDiscountParams.targetNewUnits == 0 || newDiscountParams.targetVotingPower == 0) { + revert ZeroValue(); + } // Check the sum of factors that cannot exceed the value of 10_000 (100% with a 0.01% step) uint256 sumWeights; for (uint256 i = 0; i < newDiscountParams.weightFactors.length; ++i) { @@ -143,26 +153,16 @@ contract GenericBondCalculator { emit DiscountParamsUpdated(newDiscountParams); } - /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. - /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. - /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. /// @param tokenAmount LP token amount. /// @param priceLP LP token price. - /// @param bondVestingTime Bond vesting time. - /// @param productMaxVestingTime Product max vesting time. - /// @param productSupply Current product supply. - /// @param productPayout Current product payout. + /// @param data Custom data that is used to calculate the IDF. /// @return amountOLAS Resulting amount of OLAS tokens. - /// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max; function calculatePayoutOLAS( - address account, uint256 tokenAmount, uint256 priceLP, - uint256 bondVestingTime, - uint256 productMaxVestingTime, - uint256 productSupply, - uint256 productPayout - ) external view returns (uint256 amountOLAS) { + bytes memory data + ) external view override returns (uint256 amountOLAS) { // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) @@ -178,7 +178,7 @@ contract GenericBondCalculator { } // Calculate the dynamic inverse discount factor - uint256 idf = calculateIDF(account, bondVestingTime, productMaxVestingTime, productSupply, productPayout); + uint256 idf = calculateIDF(data); // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 @@ -186,26 +186,25 @@ contract GenericBondCalculator { } /// @dev Calculated inverse discount factor based on bonding and account parameters. - /// @param account Bonding account address. - /// @param bondVestingTime Bonding desired vesting time. - /// @param productMaxVestingTime Product max vesting time. - /// @param productSupply Current product supply. - /// @param productPayout Current product payout. + /// @param data Custom data that is used to calculate the IDF: + /// - account Account address. + /// - bondVestingTime Bond vesting time. + /// - productMaxVestingTime Product max vesting time. + /// - productSupply Current product supply. + /// - productPayout Current product payout. /// @return idf Inverse discount factor in 18 decimals format. - function calculateIDF( - address account, - uint256 bondVestingTime, - uint256 productMaxVestingTime, - uint256 productSupply, - uint256 productPayout - ) public view returns (uint256 idf) { + function calculateIDF(bytes memory data) public view virtual returns (uint256 idf) { + // Decode the required data + (address account, uint256 bondVestingTime, uint256 productMaxVestingTime, uint256 productSupply, + uint256 productPayout) = abi.decode(data, (address, uint256, uint256, uint256, uint256)); + // Get the copy of the discount params DiscountParams memory localParams = discountParams; uint256 discountBooster; // First discount booster: booster = k1 * NumNewUnits(previous epoch) / TargetNewUnits(previous epoch) // Check the number of new units coming from tokenomics vs the target number of new units - if (localParams.targetNewUnits > 0) { + if (localParams.weightFactors[0] > 0) { uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits(); // If the number of new units exceeds the target, bound by the target number @@ -217,16 +216,20 @@ contract GenericBondCalculator { // Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime // Add vesting time discount booster - discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; + if (localParams.weightFactors[1] > 0) { + discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; + } // Third discount booster: booster += k3 * (1 - productPayout(at bonding time) / productSupply) // Add product supply discount booster - productSupply = productSupply + productPayout; - discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); + if (localParams.weightFactors[2] > 0) { + productSupply = productSupply + productPayout; + discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); + } // Fourth discount booster: booster += k4 * getVotes(bonding account) / targetVotingPower // Check the veOLAS balance of a bonding account - if (localParams.targetVotingPower > 0) { + if (localParams.weightFactors[3] > 0) { uint256 vPower = IVotingEscrow(ve).getVotes(account); // If the number of new units exceeds the target, bound by the target number diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 7ec5f6ef..459acf73 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -8,25 +8,21 @@ import {ITokenomics} from "./interfaces/ITokenomics.sol"; import {ITreasury} from "./interfaces/ITreasury.sol"; interface IBondCalculator { - /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. - /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. - /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. /// @param tokenAmount LP token amount. /// @param priceLP LP token price. - /// @param bondVestingTime Bond vesting time. - /// @param productMaxVestingTime Product max vesting time. - /// @param productSupply Current product supply. - /// @param productPayout Current product payout. + /// @param data Custom data that is used to calculate the IDF. /// @return amountOLAS Resulting amount of OLAS tokens. function calculatePayoutOLAS( - address account, uint256 tokenAmount, uint256 priceLP, - uint256 bondVestingTime, - uint256 productMaxVestingTime, - uint256 productSupply, - uint256 productPayout + bytes memory data ) external view returns (uint256 amountOLAS); + + /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. + /// @param token Token address. + /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. + function getCurrentPriceLP(address token) external view returns (uint256 priceLP); } /// @dev Wrong amount received / provided. @@ -372,10 +368,11 @@ contract Depository is ERC721, IErrorsTokenomics { // Get the LP token address address token = product.token; - // Calculate the payout in OLAS tokens based on the LP pair with the discount factor (DF) calculation + // Calculate the payout in OLAS tokens based on the LP pair with the inverse discount factor (IDF) calculation // Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created - payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, - bondVestingTime, productMaxVestingTime, supply, product.payout); + payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP, + // Encode parameters required for the IDF calculation + abi.encode(msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout)); // Check for the sufficient supply if (payout > supply) { @@ -565,6 +562,13 @@ contract Depository is ERC721, IErrorsTokenomics { } } + /// @dev Gets current reserves of OLAS / totalSupply of Uniswap L2-like LP tokens. + /// @param token Token address. + /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. + function getCurrentPriceLP(address token) external view returns (uint256 priceLP) { + return IBondCalculator(bondCalculator).getCurrentPriceLP(token); + } + /// @dev Gets the valid bond Id from the provided index. /// @param id Bond counter. /// @return Bond Id. diff --git a/contracts/GenericBondCalculator.sol b/contracts/GenericBondCalculator.sol index 4a0286df..a51c23cc 100644 --- a/contracts/GenericBondCalculator.sol +++ b/contracts/GenericBondCalculator.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.25; import {mulDiv} from "@prb/math/src/Common.sol"; -import "./interfaces/ITokenomics.sol"; import "./interfaces/IUniswapV2Pair.sol"; /// @dev Value overflow. @@ -13,17 +12,17 @@ error Overflow(uint256 provided, uint256 max); /// @dev Provided zero address. error ZeroAddress(); -/// @title GenericBondSwap - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. -/// @dev The bond calculation mechanism is based on the UniswapV2Pair contract. -/// @author AL +/// @title GenericBondCalculator - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. /// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - contract GenericBondCalculator { // OLAS contract address address public immutable olas; // Tokenomics contract address address public immutable tokenomics; - /// @dev Generic Bond Calcolator constructor + /// @dev Generic Bond Calculator constructor /// @param _olas OLAS contract address. /// @param _tokenomics Tokenomics contract address. constructor(address _olas, address _tokenomics) { @@ -43,7 +42,7 @@ contract GenericBondCalculator { /// @param priceLP LP token price. /// @return amountOLAS Resulting amount of OLAS tokens. /// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max; - function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP) external view + function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP, bytes memory) external view virtual returns (uint256 amountOLAS) { // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation @@ -60,15 +59,15 @@ contract GenericBondCalculator { revert Overflow(totalTokenValue, type(uint192).max); } // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 - // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 - amountOLAS = ITokenomics(tokenomics).getLastIDF() * totalTokenValue / 1e36; + // Note IDF in Tokenomics is deprecated, and can be assumed as equal to 1e18 by default + amountOLAS = totalTokenValue / 1e18; } - /// @dev Gets current reserves of OLAS / totalSupply of LP tokens. + /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. + /// @notice The price LP calculation is based on the UniswapV2Pair contract. /// @param token Token address. /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. - function getCurrentPriceLP(address token) external view returns (uint256 priceLP) - { + function getCurrentPriceLP(address token) external view virtual returns (uint256 priceLP) { IUniswapV2Pair pair = IUniswapV2Pair(token); uint256 totalSupply = pair.totalSupply(); if (totalSupply > 0) { diff --git a/contracts/test/DepositAttacker.sol b/contracts/test/DepositAttacker.sol index b475842e..8d4ffa9d 100644 --- a/contracts/test/DepositAttacker.sol +++ b/contracts/test/DepositAttacker.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.25; +import {ERC721TokenReceiver} from "../../lib/solmate/src/tokens/ERC721.sol"; import "../interfaces/IToken.sol"; import "../interfaces/IUniswapV2Pair.sol"; interface IDepository { - function deposit(uint256 productId, uint256 tokenAmount) external + function deposit(uint256 productId, uint256 tokenAmount, uint256 vestingTime) external returns (uint256 payout, uint256 expiry, uint256 bondId); } @@ -29,7 +30,7 @@ interface IZRouter { } /// @title DepositAttacker - Smart contract to prove that the deposit attack via price manipulation is not possible -contract DepositAttacker { +contract DepositAttacker is ERC721TokenReceiver { uint256 public constant LARGE_APPROVAL = 1_000_000 * 1e18; constructor() {} @@ -80,7 +81,7 @@ contract DepositAttacker { // console.log("AttackDeposit ## OLAS reserved after swap", balanceOLA); // console.log("AttackDeposit ## DAI reserved before swap", balanceDAI); - (payout, , ) = IDepository(depository).deposit(bid, amountTo); + (payout, , ) = IDepository(depository).deposit(bid, amountTo, 1 weeks); // DAI approve IToken(path[1]).approve(swapRouter, LARGE_APPROVAL); @@ -145,7 +146,7 @@ contract DepositAttacker { // console.log("AttackDeposit ## OLAS reserved after swap", balanceOLA); // console.log("AttackDeposit ## DAI reserved before swap", balanceDAI); - (payout, , ) = IDepository(depository).deposit(bid, amountTo); + (payout, , ) = IDepository(depository).deposit(bid, amountTo, 1 weeks); // DAI approve IToken(path[1]).approve(swapRouter, LARGE_APPROVAL); diff --git a/test/Depository.js b/test/Depository.js index 1d848aa4..c575ffd9 100644 --- a/test/Depository.js +++ b/test/Depository.js @@ -11,6 +11,7 @@ describe("Depository LP", async () => { const initialMint = "1" + "0".repeat(6) + decimals; const AddressZero = "0x" + "0".repeat(40); const oneWeek = 86400 * 7; + const baseURI = "https://localhost/depository/"; let deployer, alice, bob; let erc20Token; @@ -78,8 +79,8 @@ describe("Depository LP", async () => { genericBondCalculator = await GenericBondCalculator.deploy(olas.address, tokenomics.address); await genericBondCalculator.deployed(); // Deploy depository contract - depository = await depositoryFactory.deploy(olas.address, tokenomics.address, treasury.address, - genericBondCalculator.address); + depository = await depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, + tokenomics.address, treasury.address, genericBondCalculator.address); // Deploy Attack example attackDeposit = await attackDepositFactory.deploy(); @@ -345,7 +346,7 @@ describe("Depository LP", async () => { await helpers.time.increaseTo(Number(maxUint32) - 100); await expect( - depository.connect(bob).deposit(productId, 1) + depository.connect(bob).deposit(productId, 1, vesting) ).to.be.revertedWithCustomError(depository, "Overflow"); // Restore to the state of the snapshot @@ -734,12 +735,12 @@ describe("Depository LP", async () => { // Try to deposit zero amount of LP tokens await expect( - depository.connect(bob).deposit(productId, 0) + depository.connect(bob).deposit(productId, 0, vesting) ).to.be.revertedWithCustomError(depository, "ZeroValue"); // Get the full amount of LP tokens and deposit them const bamount = (await pairODAI.balanceOf(bob.address)); - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); expect(Array(await depository.callStatic.getBonds(bob.address, false)).length).to.equal(1); const res = await depository.getBondStatus(0); // The default IDF without any incentivized coefficient or epsilon rate is 1 @@ -759,18 +760,18 @@ describe("Depository LP", async () => { const product = await depository.mapBondProducts(0); const e18 = ethers.BigNumber.from("1" + decimals); const numLP = (ethers.BigNumber.from(product.supply).mul(e18)).div(priceLP); - await depository.connect(bob).deposit(productId, numLP); + await depository.connect(bob).deposit(productId, numLP, vesting); // Trying to supply more to the depleted product await expect( - depository.connect(bob).deposit(productId, 1) + depository.connect(bob).deposit(productId, 1, vesting) ).to.be.revertedWithCustomError(depository, "ProductClosed"); }); it("Should not allow a deposit with insufficient allowance", async () => { let amount = (await pairODAI.balanceOf(bob.address)); await expect( - depository.connect(deployer).deposit(productId, amount) + depository.connect(deployer).deposit(productId, amount, vesting) ).to.be.revertedWithCustomError(treasury, "InsufficientAllowance"); }); @@ -781,14 +782,14 @@ describe("Depository LP", async () => { await pairODAI.connect(deployer).approve(treasury.address, LARGE_APPROVAL); await expect( - depository.connect(deployer).deposit(productId, amount) + depository.connect(deployer).deposit(productId, amount, vesting) ).to.be.revertedWithCustomError(depository, "ProductSupplyLow"); }); it("Should fail a deposit with the priceLP * tokenAmount overflow", async () => { await expect( // maxUint96 + maxUint96 in string will give a value of more than max of uint192 - depository.connect(deployer).deposit(productId, maxUint96 + maxUint96) + depository.connect(deployer).deposit(productId, maxUint96 + maxUint96, vesting) ).to.be.revertedWithCustomError(treasury, "Overflow"); }); }); @@ -798,7 +799,7 @@ describe("Depository LP", async () => { let balance = await olas.balanceOf(bob.address); let bamount = (await pairODAI.balanceOf(bob.address)); // console.log("bob LP:%s depoist:%s",bamount,amount); - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); await expect( depository.connect(bob).redeem([0]) ).to.be.revertedWithCustomError(depository, "BondNotRedeemable"); @@ -820,9 +821,9 @@ describe("Depository LP", async () => { // Deposit LP tokens let amount = (await pairODAI.balanceOf(bob.address)); - let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, amount); + let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, amount, vesting); // console.log("[expectedPayout, expiry, index]:",[expectedPayout, expiry, index]); - await depository.connect(bob).deposit(productId, amount); + await depository.connect(bob).deposit(productId, amount, vesting); // Increase the time to a half vesting await helpers.time.increase(vesting / 2); @@ -868,7 +869,7 @@ describe("Depository LP", async () => { await pairODAI.connect(deployer).transfer(bob.address, amountTo); // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); await depository.close([productId]); }); @@ -879,7 +880,7 @@ describe("Depository LP", async () => { // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); // The product is now closed as its supply has been depleted expect(await depository.isActiveProduct(productId)).to.equal(false); @@ -900,8 +901,8 @@ describe("Depository LP", async () => { // Deposit for the full amount of OLAS const bamount = "1" + "0".repeat(3) + decimals; - let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount); - await depository.connect(bob).deposit(productId, bamount); + let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount, vesting); + await depository.connect(bob).deposit(productId, bamount, vesting); // The product is still open, let's close it expect(await depository.isActiveProduct(productId)).to.equal(true); @@ -937,8 +938,8 @@ describe("Depository LP", async () => { // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount); - await depository.connect(bob).deposit(productId, bamount); + let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount, vesting); + await depository.connect(bob).deposit(productId, bamount, vesting); // Close the product right away await depository.close([productId]); @@ -982,13 +983,13 @@ describe("Depository LP", async () => { // Try to create bond with expired products for (let i = 0; i < 2; i++) { await expect( - depository.connect(bob).deposit(i, 10) + depository.connect(bob).deposit(i, 10, vesting) ).to.be.revertedWithCustomError(depository, "ProductClosed"); } // Create bond let bamount = (await pairODAI.balanceOf(bob.address)); const productId = 2; - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); // Redeem created bond await helpers.time.increase(2 * vesting); @@ -1009,13 +1010,13 @@ describe("Depository LP", async () => { const amounts = [amount.add(deviation), amount, amount.add(deviation).add(deviation)]; // Deposit a bond for bob (bondId == 0) - await depository.connect(bob).deposit(productId, amounts[0]); + await depository.connect(bob).deposit(productId, amounts[0], vesting); // Transfer LP tokens from bob to alice - await pairODAI.connect(bob).transfer(alice.address, amount); + await pairODAI.connect(bob).transfer(alice.address, amount, vesting); // Deposit from alice to the same product (bondId == 1) - await depository.connect(alice).deposit(productId, amounts[1]); + await depository.connect(alice).deposit(productId, amounts[1], vesting); // Deposit to another bond for bob (bondId == 2) - await depository.connect(bob).deposit(productId, amounts[2]); + await depository.connect(bob).deposit(productId, amounts[2], vesting); // Get bond statuses let bondStatus; diff --git a/test/Depository2.js b/test/Depository2.js index 1fc69b1b..8518d07c 100644 --- a/test/Depository2.js +++ b/test/Depository2.js @@ -11,6 +11,7 @@ describe("Depository LP 2", async () => { const initialMint = "4" + "0".repeat(4) + decimals; const AddressZero = "0x" + "0".repeat(40); const oneWeek = 86400 * 7; + const baseURI = "https://localhost/depository/"; let deployer, alice, bob; let erc20Token; @@ -77,8 +78,8 @@ describe("Depository LP 2", async () => { genericBondCalculator = await GenericBondCalculator.deploy(olas.address, tokenomics.address); await genericBondCalculator.deployed(); // Deploy depository contract - depository = await depositoryFactory.deploy(olas.address, tokenomics.address, treasury.address, - genericBondCalculator.address); + depository = await depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, + tokenomics.address, treasury.address, genericBondCalculator.address); // Deploy Attack example attackDeposit = await attackDepositFactory.deploy(); @@ -428,10 +429,10 @@ describe("Depository LP 2", async () => { await dai.approve(router.address, LARGE_APPROVAL); const bamount = (await pairODAI.balanceOf(bob.address)); - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); expect(Array(await depository.callStatic.getBonds(bob.address, false)).length).to.equal(1); const res = await depository.getBondStatus(0); - // The default IDF without any incentivized coefficient or epsilon rate is 1 + // The default IDF now is 1 // 1250 * 1.0 = 1250 * e18 = 1.25 * e21 expect(Number(res.payout)).to.equal(1.25e+21); }); @@ -448,17 +449,17 @@ describe("Depository LP 2", async () => { const product = await depository.mapBondProducts(0); const e18 = ethers.BigNumber.from("1" + decimals); const numLP = (ethers.BigNumber.from(product.supply).mul(e18)).div(priceLP); - await depository.connect(bob).deposit(productId, numLP); + await depository.connect(bob).deposit(productId, numLP, vesting); await expect( - depository.connect(bob).deposit(productId, 1) + depository.connect(bob).deposit(productId, 1, vesting) ).to.be.revertedWithCustomError(depository, "ProductClosed"); }); it("Should not allow a deposit with insufficient allowance", async () => { let amount = (await pairODAI.balanceOf(bob.address)); await expect( - depository.connect(deployer).deposit(productId, amount) + depository.connect(deployer).deposit(productId, amount, vesting) ).to.be.revertedWithCustomError(treasury, "InsufficientAllowance"); }); @@ -469,7 +470,7 @@ describe("Depository LP 2", async () => { await pairODAI.connect(deployer).approve(treasury.address, LARGE_APPROVAL); await expect( - depository.connect(deployer).deposit(productId, amount) + depository.connect(deployer).deposit(productId, amount, vesting) ).to.be.revertedWithCustomError(depository, "ProductSupplyLow"); }); }); @@ -479,7 +480,7 @@ describe("Depository LP 2", async () => { let balance = await olas.balanceOf(bob.address); let bamount = (await pairODAI.balanceOf(bob.address)); // console.log("bob LP:%s depoist:%s",bamount,amount); - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); await expect( depository.connect(bob).redeem([0]) ).to.be.revertedWithCustomError(depository, "BondNotRedeemable"); @@ -490,9 +491,9 @@ describe("Depository LP 2", async () => { it("Redeem OLAS after the product is vested", async () => { let amount = (await pairODAI.balanceOf(bob.address)); - let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, amount); + let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, amount, vesting); // console.log("[expectedPayout, expiry, index]:",[expectedPayout, expiry, index]); - await depository.connect(bob).deposit(productId, amount); + await depository.connect(bob).deposit(productId, amount, vesting); // Increase the time to a half vesting await helpers.time.increase(vesting / 2); @@ -538,7 +539,7 @@ describe("Depository LP 2", async () => { await pairODAI.connect(deployer).transfer(bob.address, amountTo); // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); await depository.close([productId]); }); @@ -549,7 +550,7 @@ describe("Depository LP 2", async () => { // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - await depository.connect(bob).deposit(productId, bamount); + await depository.connect(bob).deposit(productId, bamount, vesting); // The product is now closed as its supply has been depleted expect(await depository.isActiveProduct(productId)).to.equal(false); @@ -572,8 +573,8 @@ describe("Depository LP 2", async () => { // Deposit for the full amount of OLAS const bamount = "2" + "0".repeat(3) + decimals; - let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount); - await depository.connect(bob).deposit(productId, bamount); + let [expectedPayout,,] = await depository.connect(bob).callStatic.deposit(productId, bamount, vesting); + await depository.connect(bob).deposit(productId, bamount, vesting); // Close the product right away await depository.close([productId]); @@ -594,13 +595,13 @@ describe("Depository LP 2", async () => { const amounts = [amount.add(deviation), amount, amount.add(deviation).add(deviation)]; // Deposit a bond for bob (bondId == 0) - await depository.connect(bob).deposit(productId, amounts[0]); + await depository.connect(bob).deposit(productId, amounts[0], vesting); // Transfer LP tokens from bob to alice await pairODAI.connect(bob).transfer(alice.address, amount); // Deposit from alice to the same product (bondId == 1) - await depository.connect(alice).deposit(productId, amounts[1]); + await depository.connect(alice).deposit(productId, amounts[1], vesting); // Deposit to another bond for bob (bondId == 2) - await depository.connect(bob).deposit(productId, amounts[2]); + await depository.connect(bob).deposit(productId, amounts[2], vesting); // Get bond statuses let bondStatus; @@ -699,12 +700,12 @@ describe("Depository LP 2", async () => { await pairODAI.connect(bob).transfer(attackDeposit.address, amountTo); // Trying to deposit the amount that would result in an overflow payout for the LP supply - const payout = await attackDeposit.callStatic.flashAttackDepositImmuneClone(depository.address, treasury.address, - pairODAI.address, olas.address, productId, amountTo, router.address); + const payout = await attackDeposit.callStatic.flashAttackDepositImmuneClone(depository.address, + treasury.address, pairODAI.address, olas.address, productId, amountTo, router.address); // Try to attack via flash loan - await attackDeposit.flashAttackDepositImmuneClone(depository.address, treasury.address, pairODAI.address, olas.address, - productId, amountTo, router.address); + await attackDeposit.flashAttackDepositImmuneClone(depository.address, treasury.address, pairODAI.address, + olas.address, productId, amountTo, router.address); // Check that the flash attack did not do anything but obtained the same bond as everybody const res = await depository.getBondStatus(0); From 002218c54dabdcdeb26761725c0a40b551d3bea5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Sun, 14 Jul 2024 14:13:21 +0100 Subject: [PATCH 08/17] chore: adding a comment --- contracts/Depository.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 459acf73..5b17a2a7 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -109,7 +109,7 @@ contract Depository is ERC721, IErrorsTokenomics { // Bond product counter // We assume that the number of products will not be bigger than the number of seconds uint256 public productCounter; - // Minimum amount of supply that is given to the + // Minimum amount of supply such that any value below is given to the bonding account in order to close the product uint256 public minOLASLeftoverAmount; // OLAS token address From 721703f18a0134e42aaae553f48294df6a24d4af Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 17:00:24 +0100 Subject: [PATCH 09/17] test: adding bond calculator tests --- contracts/BondCalculator.sol | 67 ++-- contracts/GenericBondCalculator.sol | 29 +- test/Depository2BondCalculator.js | 365 ++++++++++++++++++ ...js => Depository2GenericBondCalculator.js} | 2 +- ....js => DepositoryGenericBondCalculator.js} | 2 +- 5 files changed, 413 insertions(+), 52 deletions(-) create mode 100644 test/Depository2BondCalculator.js rename test/{Depository2.js => Depository2GenericBondCalculator.js} (99%) rename test/{Depository.js => DepositoryGenericBondCalculator.js} (99%) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index f478cb9c..e75ca05d 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; - +import "hardhat/console.sol"; import {mulDiv} from "@prb/math/src/Common.sol"; import {GenericBondCalculator} from "./GenericBondCalculator.sol"; import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; @@ -153,38 +153,6 @@ contract BondCalculator is GenericBondCalculator { emit DiscountParamsUpdated(newDiscountParams); } - /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. - /// @param tokenAmount LP token amount. - /// @param priceLP LP token price. - /// @param data Custom data that is used to calculate the IDF. - /// @return amountOLAS Resulting amount of OLAS tokens. - function calculatePayoutOLAS( - uint256 tokenAmount, - uint256 priceLP, - bytes memory data - ) external view override returns (uint256 amountOLAS) { - // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation - // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; - // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) - // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced); - // tokenAmount is of the order of sqrt(r0*r1) ~ 104 bits (if balanced) or sqrt(96) ~ 10 bits (if max unbalanced); - // overall: 64 + 53 + 104 = 221 < 256 - regular case if LP is balanced, and 64 + 147 + 10 = 221 < 256 if unbalanced - // mulDiv will correctly fit the total amount up to the value of max uint256, i.e., max of priceLP and max of tokenAmount, - // however their multiplication can not be bigger than the max of uint192 - uint256 totalTokenValue = mulDiv(priceLP, tokenAmount, 1); - // Check for the cumulative LP tokens value limit - if (totalTokenValue > type(uint192).max) { - revert Overflow(totalTokenValue, type(uint192).max); - } - - // Calculate the dynamic inverse discount factor - uint256 idf = calculateIDF(data); - - // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 - // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 - amountOLAS = (idf * totalTokenValue) / 1e36; - } - /// @dev Calculated inverse discount factor based on bonding and account parameters. /// @param data Custom data that is used to calculate the IDF: /// - account Account address. @@ -193,7 +161,7 @@ contract BondCalculator is GenericBondCalculator { /// - productSupply Current product supply. /// - productPayout Current product payout. /// @return idf Inverse discount factor in 18 decimals format. - function calculateIDF(bytes memory data) public view virtual returns (uint256 idf) { + function calculateIDF(bytes memory data) public view override returns (uint256 idf) { // Decode the required data (address account, uint256 bondVestingTime, uint256 productMaxVestingTime, uint256 productSupply, uint256 productPayout) = abi.decode(data, (address, uint256, uint256, uint256, uint256)); @@ -209,22 +177,33 @@ contract BondCalculator is GenericBondCalculator { // If the number of new units exceeds the target, bound by the target number if (numNewUnits >= localParams.targetNewUnits) { - numNewUnits = localParams.targetNewUnits; + discountBooster = uint256(localParams.weightFactors[0]) * 1e18; + } else { + discountBooster = (uint256(localParams.weightFactors[0]) * numNewUnits * 1e18) / + uint256(localParams.targetNewUnits); } - discountBooster = (localParams.weightFactors[0] * numNewUnits * 1e18) / localParams.targetNewUnits; } // Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime // Add vesting time discount booster if (localParams.weightFactors[1] > 0) { - discountBooster += (localParams.weightFactors[1] * bondVestingTime * 1e18) / productMaxVestingTime; + if (bondVestingTime == productMaxVestingTime) { + discountBooster += uint256(localParams.weightFactors[1]) * 1e18; + } else { + discountBooster += (uint256(localParams.weightFactors[1]) * bondVestingTime * 1e18) / productMaxVestingTime; + } } // Third discount booster: booster += k3 * (1 - productPayout(at bonding time) / productSupply) // Add product supply discount booster if (localParams.weightFactors[2] > 0) { - productSupply = productSupply + productPayout; - discountBooster += localParams.weightFactors[2] * (1e18 - ((productPayout * 1e18) / productSupply)); + if (productPayout == 0) { + discountBooster += uint256(localParams.weightFactors[2]) * 1e18; + } else { + // Get the total product supply + productSupply = productSupply + productPayout; + discountBooster += uint256(localParams.weightFactors[2]) * (1e18 - ((productPayout * 1e18) / productSupply)); + } } // Fourth discount booster: booster += k4 * getVotes(bonding account) / targetVotingPower @@ -234,9 +213,11 @@ contract BondCalculator is GenericBondCalculator { // If the number of new units exceeds the target, bound by the target number if (vPower >= localParams.targetVotingPower) { - vPower = localParams.targetVotingPower; + discountBooster += uint256(localParams.weightFactors[3]) * 1e18; + } else { + discountBooster += (uint256(localParams.weightFactors[3]) * vPower * 1e18) / + uint256(localParams.targetVotingPower); } - discountBooster += (localParams.weightFactors[3] * vPower * 1e18) / localParams.targetVotingPower; } // Normalize discount booster by the max sum of weights @@ -245,4 +226,8 @@ contract BondCalculator is GenericBondCalculator { // IDF = 1 + normalized booster idf = 1e18 + discountBooster; } + + function getDiscountParams() external view returns (DiscountParams memory) { + return discountParams; + } } diff --git a/contracts/GenericBondCalculator.sol b/contracts/GenericBondCalculator.sol index a51c23cc..5d2f3a0d 100644 --- a/contracts/GenericBondCalculator.sol +++ b/contracts/GenericBondCalculator.sol @@ -35,16 +35,23 @@ contract GenericBondCalculator { tokenomics = _tokenomics; } - /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism. - /// @notice Currently there is only one implementation of a bond calculation mechanism based on the UniswapV2 LP. - /// @notice IDF has a 10^18 multiplier and priceLP has the same as well, so the result must be divided by 10^36. + /// @dev Calculated inverse discount factor. + /// @return idf Inverse discount factor in 18 decimals format. + function calculateIDF(bytes memory) public view virtual returns (uint256 idf) { + // Note: IDF is deprecated in Tokenomics, and can be assumed as equal to 1e18 by default + idf = 1e18; + } + + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. /// @param tokenAmount LP token amount. /// @param priceLP LP token price. + /// @param data Custom data to calculate the IDF. /// @return amountOLAS Resulting amount of OLAS tokens. - /// #if_succeeds {:msg "LP price limit"} priceLP * tokenAmount <= type(uint192).max; - function calculatePayoutOLAS(uint256 tokenAmount, uint256 priceLP, bytes memory) external view virtual - returns (uint256 amountOLAS) - { + function calculatePayoutOLAS( + uint256 tokenAmount, + uint256 priceLP, + bytes memory data + ) external view virtual returns (uint256 amountOLAS) { // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) @@ -58,9 +65,13 @@ contract GenericBondCalculator { if (totalTokenValue > type(uint192).max) { revert Overflow(totalTokenValue, type(uint192).max); } + + // Calculate the dynamic inverse discount factor + uint256 idf = calculateIDF(data); + // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 - // Note IDF in Tokenomics is deprecated, and can be assumed as equal to 1e18 by default - amountOLAS = totalTokenValue / 1e18; + // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 + amountOLAS = (idf * totalTokenValue) / 1e36; } /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. diff --git a/test/Depository2BondCalculator.js b/test/Depository2BondCalculator.js new file mode 100644 index 00000000..e70b1beb --- /dev/null +++ b/test/Depository2BondCalculator.js @@ -0,0 +1,365 @@ +/*global describe, beforeEach, it, context*/ +const { ethers } = require("hardhat"); +const { expect } = require("chai"); +const helpers = require("@nomicfoundation/hardhat-network-helpers"); + +describe("Depository LP 2 Bond Calculator", async () => { + // 1 million token + const LARGE_APPROVAL = ethers.utils.parseEther("1000000"); + // Initial mint for OLAS and DAI (40,000) + const initialMint = ethers.utils.parseEther("40000"); + const AddressZero = ethers.constants.AddressZero; + const oneWeek = 86400 * 7; + const baseURI = "https://localhost/depository/"; + + let deployer, alice, bob; + let erc20Token; + let olasFactory; + let depositoryFactory; + let tokenomicsFactory; + let bondCalculator; + let router; + let factory; + + let dai; + let olas; + let pairODAI; + let depository; + let treasury; + let treasuryFactory; + let tokenomics; + let ve; + let epochLen = 86400 * 10; + let defaultPriceLP = ethers.utils.parseEther("2"); + + // 2,000 + let supplyProductOLAS = ethers.utils.parseEther("2000"); + const maxUint96 = "79228162514264337593543950335"; + const maxUint32 = "4294967295"; + + let vesting = 2 * oneWeek; + + let productId = 0; + let first; + let id; + + const discountParams = { + targetVotingPower: ethers.utils.parseEther("10"), + targetNewUnits: 10, + weightFactors: new Array(4).fill(100) + } + + /** + * Everything in this block is only run once before all tests. + * This is the home for setup methods + */ + + beforeEach(async () => { + [deployer, alice, bob] = await ethers.getSigners(); + // Note: this is not a real OLAS token, just an ERC20 mock-up + olasFactory = await ethers.getContractFactory("ERC20Token"); + erc20Token = await ethers.getContractFactory("ERC20Token"); + depositoryFactory = await ethers.getContractFactory("Depository"); + treasuryFactory = await ethers.getContractFactory("Treasury"); + tokenomicsFactory = await ethers.getContractFactory("Tokenomics"); + + dai = await erc20Token.deploy(); + olas = await olasFactory.deploy(); + + // Voting Escrow mock + const VE = await ethers.getContractFactory("MockVE"); + ve = await VE.deploy(); + await ve.deployed(); + + // Correct treasury address is missing here, it will be defined just one line below + tokenomics = await tokenomicsFactory.deploy(); + await tokenomics.initializeTokenomics(olas.address, deployer.address, deployer.address, deployer.address, + ve.address, epochLen, deployer.address, deployer.address, deployer.address, AddressZero); + // Correct depository address is missing here, it will be defined just one line below + treasury = await treasuryFactory.deploy(olas.address, tokenomics.address, deployer.address, deployer.address); + // Change bond fraction to 100% in these tests + await tokenomics.changeIncentiveFractions(66, 34, 100, 0, 0, 0); + + // Deploy bond calculator contract + const BondCalculator = await ethers.getContractFactory("BondCalculator"); + bondCalculator = await BondCalculator.deploy(olas.address, tokenomics.address, ve.address, discountParams); + await bondCalculator.deployed(); + // Deploy depository contract + depository = await depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, + tokenomics.address, treasury.address, bondCalculator.address); + + // Change to the correct addresses + await treasury.changeManagers(AddressZero, depository.address, AddressZero); + await tokenomics.changeManagers(treasury.address, depository.address, AddressZero); + + // Airdrop from the deployer :) + await dai.mint(deployer.address, initialMint); + await olas.mint(deployer.address, initialMint); + await olas.mint(alice.address, initialMint); + + // Change the minter to treasury + await olas.changeMinter(treasury.address); + + // Deploy Uniswap factory + const Factory = await ethers.getContractFactory("ZuniswapV2Factory"); + factory = await Factory.deploy(); + await factory.deployed(); + // console.log("Uniswap factory deployed to:", factory.address); + + // Deploy Uniswap V2 library + const ZuniswapV2Library = await ethers.getContractFactory("ZuniswapV2Library"); + const zuniswapV2Library = await ZuniswapV2Library.deploy(); + await zuniswapV2Library.deployed(); + + // Deploy Router02 + const Router = await ethers.getContractFactory("ZuniswapV2Router", { + libraries: { + ZuniswapV2Library: zuniswapV2Library.address, + }, + }); + + router = await Router.deploy(factory.address); + await router.deployed(); + // console.log("Uniswap router02 deployed to:", router.address); + + //var json = require("../../../artifacts/@uniswap/v2-core/contracts/UniswapV2Pair.sol/UniswapV2Pair.json"); + //const actual_bytecode1 = json["bytecode"]; + //const COMPUTED_INIT_CODE_HASH1 = ethers.utils.keccak256(actual_bytecode1); + //console.log("init hash:", COMPUTED_INIT_CODE_HASH1, "in UniswapV2Library :: hash:0xe9d807835bf1c75fb519759197ec594400ca78aa1d4b77743b1de676f24f8103"); + + //const pairODAItxReceipt = await factory.createPair(olas.address, dai.address); + await factory.createPair(olas.address, dai.address); + // const pairODAIdata = factory.interface.decodeFunctionData("createPair", pairODAItxReceipt.data); + // console.log("olas[%s]:DAI[%s] pool", pairODAIdata[0], pairODAIdata[1]); + let pairAddress = await factory.allPairs(0); + // console.log("olas - DAI address:", pairAddress); + pairODAI = await ethers.getContractAt("ZuniswapV2Pair", pairAddress); + // let reserves = await pairODAI.getReserves(); + // console.log("olas - DAI reserves:", reserves.toString()); + // console.log("balance dai for deployer:",(await dai.balanceOf(deployer.address))); + + // Add liquidity + //const amountOLAS = await olas.balanceOf(deployer.address); + const amountOLAS = ethers.utils.parseEther("5000"); + const amountDAI = ethers.utils.parseEther("5000"); + const minAmountOLA = ethers.utils.parseEther("500"); + const minAmountDAI = ethers.utils.parseEther("1000"); + const toAddress = deployer.address; + await olas.approve(router.address, LARGE_APPROVAL); + await dai.approve(router.address, LARGE_APPROVAL); + + await router.connect(deployer).addLiquidity( + dai.address, + olas.address, + amountDAI, + amountOLAS, + minAmountDAI, + minAmountOLA, + toAddress + ); + + //console.log("deployer LP balance:", await pairODAI.balanceOf(deployer.address)); + //console.log("LP total supplyProductOLAS:", await pairODAI.totalSupply()); + // send half of the balance from deployer + const amountTo = new ethers.BigNumber.from(await pairODAI.balanceOf(deployer.address)).div(4); + await pairODAI.connect(deployer).transfer(bob.address, amountTo); + //console.log("balance LP for bob:", (await pairODAI.balanceOf(bob.address))); + //console.log("deployer LP new balance:", await pairODAI.balanceOf(deployer.address)); + + await pairODAI.connect(bob).approve(treasury.address, LARGE_APPROVAL); + await pairODAI.connect(alice).approve(treasury.address, LARGE_APPROVAL); + + await treasury.enableToken(pairODAI.address); + const priceLP = await depository.getCurrentPriceLP(pairODAI.address); + await depository.create(pairODAI.address, priceLP, supplyProductOLAS, vesting); + }); + + context("Initialization", async function () { + it("Changing Bond Calculator owner", async function () { + const account = alice; + + // Trying to change owner from a non-owner account address + await expect( + bondCalculator.connect(alice).changeOwner(alice.address) + ).to.be.revertedWithCustomError(bondCalculator, "OwnerOnly"); + + // Trying to change the owner to the zero address + await expect( + bondCalculator.connect(deployer).changeOwner(AddressZero) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroAddress"); + + // Changing the owner + await bondCalculator.connect(deployer).changeOwner(alice.address); + + // Trying to change owner from the previous owner address + await expect( + bondCalculator.connect(deployer).changeOwner(alice.address) + ).to.be.revertedWithCustomError(bondCalculator, "OwnerOnly"); + }); + + it("Should fail when initializing with incorrect values", async function () { + const defaultDiscountParams = { + targetVotingPower: 0, + targetNewUnits: 0, + weightFactors: new Array(4).fill(2550) + } + + // Trying to deploy with the zero veOLAS address + const BondCalculator = await ethers.getContractFactory("BondCalculator"); + await expect( + BondCalculator.deploy(olas.address, tokenomics.address, AddressZero, defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroAddress"); + + // Trying to deploy with the zero targetNewUnits + await expect( + BondCalculator.deploy(olas.address, tokenomics.address, ve.address, defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroValue"); + + defaultDiscountParams.targetNewUnits = 10; + + // Trying to deploy with the zero targetVotingPower + await expect( + BondCalculator.deploy(olas.address, tokenomics.address, ve.address, defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroValue"); + + defaultDiscountParams.targetVotingPower = 10; + + // Trying to deploy with the overflow weights + await expect( + BondCalculator.deploy(olas.address, tokenomics.address, ve.address, defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "Overflow"); + }); + + it("Should fail when changing discount parameters for incorrect values", async function () { + const defaultDiscountParams = { + targetVotingPower: 0, + targetNewUnits: 0, + weightFactors: new Array(4).fill(2550) + } + + // Trying to change discount params not by the owner + await expect( + bondCalculator.connect(alice).changeDiscountParams(defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "OwnerOnly"); + + // Trying to change discount params with the zero targetNewUnits + await expect( + bondCalculator.changeDiscountParams(defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroValue"); + + defaultDiscountParams.targetNewUnits = 10; + + // Trying to change discount params with the zero targetVotingPower + await expect( + bondCalculator.changeDiscountParams(defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "ZeroValue"); + + defaultDiscountParams.targetVotingPower = 10; + + // Trying to change discount params with the overflow weights + await expect( + bondCalculator.changeDiscountParams(defaultDiscountParams) + ).to.be.revertedWithCustomError(bondCalculator, "Overflow"); + + defaultDiscountParams.weightFactors[3] = 1000; + // Now able to change discount params + await bondCalculator.changeDiscountParams(defaultDiscountParams); + }); + }); + + context("Bond deposits", async function () { + it("Should not allow a deposit with incorrect vesting time", async () => { + const amount = (await pairODAI.balanceOf(bob.address)); + + await expect( + depository.connect(deployer).deposit(productId, amount, 0) + ).to.be.revertedWithCustomError(treasury, "LowerThan"); + + await expect( + depository.connect(deployer).deposit(productId, amount, vesting + 1) + ).to.be.revertedWithCustomError(treasury, "Overflow"); + }); + + it("Should not allow a deposit greater than max payout", async () => { + const amount = (await pairODAI.balanceOf(deployer.address)); + + // Trying to deposit the amount that would result in an overflow payout for the LP supply + await pairODAI.connect(deployer).approve(treasury.address, LARGE_APPROVAL); + + await expect( + depository.connect(deployer).deposit(productId, amount, vesting) + ).to.be.revertedWithCustomError(depository, "ProductSupplyLow"); + }); + + it("Deposit to a bonding product for the OLAS payout with a full vesting time", async () => { + await olas.approve(router.address, LARGE_APPROVAL); + await dai.approve(router.address, LARGE_APPROVAL); + + // Get the full amount of LP tokens and deposit them + const bamount = (await pairODAI.balanceOf(bob.address)); + await depository.connect(bob).deposit(productId, bamount, vesting); + + const res = await depository.getBondStatus(0); + // The default IDF without any incentivized coefficient or epsilon rate is 1 + // 1250 * 1.0 = 1250 * e18 = 1.25 * e21 + // The calculated IDF must be bigger + expect(Number(res.payout)).to.gt(1.25e+21); + }); + + it("Deposit to a bonding product for several amounts", async () => { + await olas.approve(router.address, LARGE_APPROVAL); + await dai.approve(router.address, LARGE_APPROVAL); + + // Get the full amount of LP tokens and deposit them + const bamount = (await pairODAI.balanceOf(bob.address)); + await depository.connect(bob).deposit(productId, bamount.div(2), vesting); + await depository.connect(bob).deposit(productId, bamount.div(2), vesting); + + const res = await depository.getBondStatus(0); + // The default IDF without any incentivized coefficient or epsilon rate is 1 + // 1250 * 1.0 / 2 = 1250 * e18 / 2 = 6.25 * e20 + // The calculated IDF must be bigger + expect(Number(res.payout)).to.gt(6.25e+20); + + const res2 = await depository.getBondStatus(1); + expect(Number(res2.payout)).to.gt(6.25e+20); + + // The second deposit amount must be smaller as the first one gets a bigger discount factor + expect(res.payout).to.gt(res2.payout); + }); + + it("Deposit to a bonding product for the OLAS payout with a half vesting time", async () => { + await olas.approve(router.address, LARGE_APPROVAL); + await dai.approve(router.address, LARGE_APPROVAL); + + // Get the full amount of LP tokens and deposit them + const bamount = (await pairODAI.balanceOf(bob.address)); + await depository.connect(bob).deposit(productId, bamount, oneWeek); + + const res = await depository.getBondStatus(0); + // The default IDF without any incentivized coefficient or epsilon rate is 1 + // 1250 * 1.0 = 1250 * e18 = 1.25 * e21 + // The calculated IDF must be bigger + expect(Number(res.payout)).to.gt(1.25e+21); + }); + + it("Deposit to a bonding product for the OLAS payout with partial veOLAS limit", async () => { + await olas.approve(router.address, LARGE_APPROVAL); + await dai.approve(router.address, LARGE_APPROVAL); + + // Lock OLAS balances with Voting Escrow + await ve.setWeightedBalance(ethers.utils.parseEther("50")); + await ve.createLock(bob.address); + + // Get the full amount of LP tokens and deposit them + const bamount = (await pairODAI.balanceOf(bob.address)); + await depository.connect(bob).deposit(productId, bamount, oneWeek); + + const res = await depository.getBondStatus(0); + // The default IDF without any incentivized coefficient or epsilon rate is 1 + // 1250 * 1.0 = 1250 * e18 = 1.25 * e21 + // The calculated IDF must be bigger + expect(Number(res.payout)).to.gt(1.25e+21); + }); + }); +}); diff --git a/test/Depository2.js b/test/Depository2GenericBondCalculator.js similarity index 99% rename from test/Depository2.js rename to test/Depository2GenericBondCalculator.js index 8518d07c..1f0ae270 100644 --- a/test/Depository2.js +++ b/test/Depository2GenericBondCalculator.js @@ -3,7 +3,7 @@ const { ethers } = require("hardhat"); const { expect } = require("chai"); const helpers = require("@nomicfoundation/hardhat-network-helpers"); -describe("Depository LP 2", async () => { +describe("Depository LP 2 Generic Bond Calculator", async () => { const decimals = "0".repeat(18); // 1 million token const LARGE_APPROVAL = "1" + "0".repeat(6) + decimals; diff --git a/test/Depository.js b/test/DepositoryGenericBondCalculator.js similarity index 99% rename from test/Depository.js rename to test/DepositoryGenericBondCalculator.js index c575ffd9..036b9f10 100644 --- a/test/Depository.js +++ b/test/DepositoryGenericBondCalculator.js @@ -3,7 +3,7 @@ const { ethers } = require("hardhat"); const { expect } = require("chai"); const helpers = require("@nomicfoundation/hardhat-network-helpers"); -describe("Depository LP", async () => { +describe("Depository LP Generic Bond Calculator", async () => { const decimals = "0".repeat(18); // 1 million token const LARGE_APPROVAL = "1" + "0".repeat(10) + decimals; From 1493900321a64de95f606f78ed6b00d2d51c12e7 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 17:25:31 +0100 Subject: [PATCH 10/17] chore: linter --- test/Depository2BondCalculator.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Depository2BondCalculator.js b/test/Depository2BondCalculator.js index e70b1beb..37004f6c 100644 --- a/test/Depository2BondCalculator.js +++ b/test/Depository2BondCalculator.js @@ -47,7 +47,7 @@ describe("Depository LP 2 Bond Calculator", async () => { targetVotingPower: ethers.utils.parseEther("10"), targetNewUnits: 10, weightFactors: new Array(4).fill(100) - } + }; /** * Everything in this block is only run once before all tests. @@ -202,7 +202,7 @@ describe("Depository LP 2 Bond Calculator", async () => { targetVotingPower: 0, targetNewUnits: 0, weightFactors: new Array(4).fill(2550) - } + }; // Trying to deploy with the zero veOLAS address const BondCalculator = await ethers.getContractFactory("BondCalculator"); @@ -235,7 +235,7 @@ describe("Depository LP 2 Bond Calculator", async () => { targetVotingPower: 0, targetNewUnits: 0, weightFactors: new Array(4).fill(2550) - } + }; // Trying to change discount params not by the owner await expect( From f921c5c31d33703960f232730fa72d0d838332c7 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 17:28:47 +0100 Subject: [PATCH 11/17] chore: linter --- contracts/BondCalculator.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index e75ca05d..98974766 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import "hardhat/console.sol"; + import {mulDiv} from "@prb/math/src/Common.sol"; import {GenericBondCalculator} from "./GenericBondCalculator.sol"; import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; From 543d825e595db909184e006efcbddee7d8bc5c80 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 18:41:54 +0100 Subject: [PATCH 12/17] test: fixing tests --- test/DepositoryGenericBondCalculator.js | 27 ++++++++----- test/Tokenomics.js | 53 ++++++++----------------- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/test/DepositoryGenericBondCalculator.js b/test/DepositoryGenericBondCalculator.js index 036b9f10..22082e6a 100644 --- a/test/DepositoryGenericBondCalculator.js +++ b/test/DepositoryGenericBondCalculator.js @@ -207,20 +207,29 @@ describe("Depository LP Generic Bond Calculator", async () => { it("Should fail when deploying with zero addresses", async function () { await expect( - depositoryFactory.deploy(AddressZero, AddressZero, AddressZero, AddressZero) + depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, AddressZero, AddressZero, AddressZero, + AddressZero) ).to.be.revertedWithCustomError(depository, "ZeroAddress"); await expect( - depositoryFactory.deploy(olas.address, AddressZero, AddressZero, AddressZero) + depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, AddressZero, AddressZero, + AddressZero) ).to.be.revertedWithCustomError(depository, "ZeroAddress"); await expect( - depositoryFactory.deploy(olas.address, deployer.address, AddressZero, AddressZero) + depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, deployer.address, AddressZero, + AddressZero) ).to.be.revertedWithCustomError(depository, "ZeroAddress"); await expect( - depositoryFactory.deploy(olas.address, deployer.address, deployer.address, AddressZero) + depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, deployer.address, + deployer.address, AddressZero) ).to.be.revertedWithCustomError(depository, "ZeroAddress"); + + await expect( + depositoryFactory.deploy("Depository", "OLAS_BOND", "", olas.address, deployer.address, + deployer.address, deployer.address) + ).to.be.revertedWithCustomError(depository, "ZeroValue"); }); it("Changing Bond Calculator contract", async function () { @@ -910,8 +919,8 @@ describe("Depository LP Generic Bond Calculator", async () => { expect(await depository.isActiveProduct(productId)).to.equal(false); // Now change the depository contract address - const newDepository = await depositoryFactory.deploy(olas.address, tokenomics.address, treasury.address, - genericBondCalculator.address); + const newDepository = await depositoryFactory.deploy("Depository", "OLAS_BOND", baseURI, olas.address, + tokenomics.address, treasury.address, genericBondCalculator.address); // Change to a new depository address await treasury.changeManagers(AddressZero, newDepository.address, AddressZero); @@ -963,8 +972,8 @@ describe("Depository LP Generic Bond Calculator", async () => { // Check product and bond counters at this point of time const productCounter = await depository.productCounter(); expect(productCounter).to.equal(3); - const bondCounter = await depository.bondCounter(); - expect(bondCounter).to.equal(0); + const totalSupply = await depository.totalSupply(); + expect(totalSupply).to.equal(0); // Close tree products await depository.connect(deployer).close([0, 1]); @@ -1012,7 +1021,7 @@ describe("Depository LP Generic Bond Calculator", async () => { // Deposit a bond for bob (bondId == 0) await depository.connect(bob).deposit(productId, amounts[0], vesting); // Transfer LP tokens from bob to alice - await pairODAI.connect(bob).transfer(alice.address, amount, vesting); + await pairODAI.connect(bob).transfer(alice.address, amount); // Deposit from alice to the same product (bondId == 1) await depository.connect(alice).deposit(productId, amounts[1], vesting); // Deposit to another bond for bob (bondId == 2) diff --git a/test/Tokenomics.js b/test/Tokenomics.js index 669e1b80..c95b6134 100644 --- a/test/Tokenomics.js +++ b/test/Tokenomics.js @@ -271,13 +271,13 @@ describe("Tokenomics", async () => { 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, 10, epochLen * 2, 10) + tokenomics.connect(signers[1]).changeTokenomicsParameters(10, 10, epochLen * 2, 10) ).to.be.revertedWithCustomError(tokenomics, "OwnerOnly"); // Trying to set epoch length smaller than the minimum allowed value - await tokenomics.changeTokenomicsParameters(10, 10, 10, lessThanMinEpochLen, 10); + await tokenomics.changeTokenomicsParameters(10, 10, lessThanMinEpochLen, 10); // Trying to set epoch length bigger than one year - await tokenomics.changeTokenomicsParameters(10, 10, 10, oneYear + 1, 10); + await tokenomics.changeTokenomicsParameters(10, 10, oneYear + 1, 10); // Move one epoch in time and finish the epoch await helpers.time.increase(epochLen + 100); await tokenomics.checkpoint(); @@ -285,7 +285,7 @@ describe("Tokenomics", async () => { expect(await tokenomics.epochLen()).to.equal(epochLen); // Change epoch length to a bigger number - await tokenomics.changeTokenomicsParameters(10, 10, 10, epochLen * 2, 10); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen * 2, 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 @@ -294,7 +294,7 @@ describe("Tokenomics", async () => { expect(await tokenomics.epochLen()).to.equal(epochLen * 2); // Change epoch len to a smaller value - await tokenomics.changeTokenomicsParameters(10, 10, 10, epochLen, 10); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 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 @@ -303,10 +303,10 @@ describe("Tokenomics", async () => { expect(await tokenomics.epochLen()).to.equal(epochLen); // Leave the epoch length untouched - await tokenomics.changeTokenomicsParameters(10, 10, 10, epochLen, 10); + await tokenomics.changeTokenomicsParameters(10, 10, epochLen, 10); // And then change back to the bigger one and change other parameters const genericParam = "1" + "0".repeat(17); - await tokenomics.changeTokenomicsParameters(genericParam, genericParam, 10, epochLen + 100, 10); + await tokenomics.changeTokenomicsParameters(genericParam, genericParam, epochLen + 100, 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 @@ -314,14 +314,9 @@ describe("Tokenomics", async () => { await tokenomics.checkpoint(); expect(await tokenomics.epochLen()).to.equal(epochLen + 100); - // Trying to set epsilonRate bigger than 17e18 - await tokenomics.changeTokenomicsParameters(0, 0, "171"+"0".repeat(17), 0, 0); - expect(await tokenomics.epsilonRate()).to.equal(10); - // Trying to set all zeros - await tokenomics.changeTokenomicsParameters(0, 0, 0, 0, 0); + await tokenomics.changeTokenomicsParameters(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); expect(await tokenomics.veOLASThreshold()).to.equal(10); @@ -514,10 +509,6 @@ describe("Tokenomics", async () => { }); it("Checkpoint with revenues", async () => { - // Get IDF of the first epoch - let lastIDF = Number(await tokenomics.getLastIDF()) / E18; - expect(lastIDF).to.equal(1); - // Send the revenues to services await treasury.connect(deployer).depositServiceDonationsETH([1, 2], [regDepositFromServices, regDepositFromServices], {value: twoRegDepositFromServices}); @@ -525,14 +516,6 @@ describe("Tokenomics", async () => { await helpers.time.increase(epochLen + 10); // Start new epoch and calculate tokenomics parameters and rewards await tokenomics.connect(deployer).checkpoint(); - - // Get IDF of the last epoch - const idf = Number(await tokenomics.getLastIDF()) / E18; - expect(idf).to.greaterThan(0); - - // Get last IDF that must match the idf of the last epoch - lastIDF = Number(await tokenomics.getLastIDF()) / E18; - expect(idf).to.equal(lastIDF); }); it("Checkpoint with inability to re-balance treasury rewards", async () => { @@ -568,10 +551,6 @@ describe("Tokenomics", async () => { // Start new epoch and calculate tokenomics parameters and rewards await helpers.time.increase(epochLen + 10); await tokenomics.connect(deployer).checkpoint(); - - // Get IDF - const idf = Number(await tokenomics.getLastIDF()) / E18; - expect(idf).to.greaterThan(Number(await tokenomics.epsilonRate()) / E18); }); }); @@ -729,8 +708,8 @@ describe("Tokenomics", async () => { ]; const accountTopUps = topUps[1].add(topUps[2]); - expect(result.events[1].args.accountRewards).to.equal(accountRewards); - expect(result.events[1].args.accountTopUps).to.equal(accountTopUps); + expect(result.events[0].args.accountRewards).to.equal(accountRewards); + expect(result.events[0].args.accountTopUps).to.equal(accountTopUps); // Restore the state of the blockchain back to the very beginning of this test snapshot.restore(); @@ -766,7 +745,7 @@ describe("Tokenomics", async () => { // Change the epoch length let newEpochLen = 2 * epochLen; - await tokenomics.changeTokenomicsParameters(0, 0, 0, newEpochLen, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0); await helpers.time.increase(epochLen); await tokenomics.checkpoint(); @@ -788,7 +767,7 @@ describe("Tokenomics", async () => { // Change now maxBondFraction and epoch length at the same time await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0, 0); - await tokenomics.changeTokenomicsParameters(0, 0, 0, newEpochLen, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0); await helpers.time.increase(epochLen); await tokenomics.checkpoint(); @@ -903,7 +882,7 @@ describe("Tokenomics", async () => { await tokenomics.checkpoint(); // Change the epoch length - await tokenomics.changeTokenomicsParameters(0, 0, 0, newEpochLen, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0); // Calculate the maxBond manually and compare with the tokenomics one let inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); // Get the part of a max bond before the year change @@ -944,7 +923,7 @@ describe("Tokenomics", async () => { // Change now maxBondFraction and epoch length at the same time await tokenomics.connect(deployer).changeIncentiveFractions(0, 0, 100, 0, 0, 0); - await tokenomics.changeTokenomicsParameters(0, 0, 0, newEpochLen, 0); + await tokenomics.changeTokenomicsParameters(0, 0, newEpochLen, 0); // Calculate the maxBond manually and compare with the tokenomics one let inflationPerSecond = ethers.BigNumber.from(await tokenomics.inflationPerSecond()); // Get the part of a max bond before the year change @@ -1057,7 +1036,7 @@ describe("Tokenomics", async () => { 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, 0, 2 * epochLen, 0); + await tokenomics.changeTokenomicsParameters(0, 0, 2 * epochLen, 0); // Move to the end of epoch and check the updated epoch length await helpers.time.increase(epochLen); await tokenomics.checkpoint(); @@ -1072,7 +1051,7 @@ describe("Tokenomics", async () => { await tokenomics.checkpoint(); // The maxBond lock flag must be set to true, now try to change the epochLen - await tokenomics.changeTokenomicsParameters(0, 0, 0, epochLen + 100, 0); + await tokenomics.changeTokenomicsParameters(0, 0, epochLen + 100, 0); // Try to change the maxBondFraction as well await tokenomics.changeIncentiveFractions(30, 40, 60, 40, 0, 0); From 0868302d607c9876123ff344c980e92af3b74a29 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 18:49:57 +0100 Subject: [PATCH 13/17] test: forge --- test/Depository.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Depository.t.sol b/test/Depository.t.sol index 82a811bb..2dcd997e 100644 --- a/test/Depository.t.sol +++ b/test/Depository.t.sol @@ -59,7 +59,8 @@ contract BaseSetup is Test { // Deploy generic bond calculator contract genericBondCalculator = new GenericBondCalculator(address(olas), address(tokenomics)); // Deploy depository contract - depository = new Depository(address(olas), address(tokenomics), address(treasury), address(genericBondCalculator)); + depository = new Depository("Depository", "OLAS_BOND", "baseURI", address(olas), address(tokenomics), + address(treasury), address(genericBondCalculator)); // Change depository contract addresses to the correct ones treasury.changeManagers(address(0), address(depository), address(0)); From 83116248685394cc55b3d1babb993b8617c7f927 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 18 Jul 2024 18:56:05 +0100 Subject: [PATCH 14/17] test: forge --- test/Depository.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Depository.t.sol b/test/Depository.t.sol index 2dcd997e..0036156a 100644 --- a/test/Depository.t.sol +++ b/test/Depository.t.sol @@ -132,7 +132,7 @@ contract DepositoryTest is BaseSetup { uint256 bamount = ZuniswapV2Pair(pair).balanceOf(deployer); // Deposit to the product Id 0 vm.prank(deployer); - depository.deposit(0, bamount); + depository.deposit(0, bamount, vesting); // Check the size of pending bond array (uint256[] memory bondIds, ) = depository.getBonds(deployer, false); assertEq(bondIds.length, 1); @@ -149,7 +149,7 @@ contract DepositoryTest is BaseSetup { // Make a bond deposit for the product Id 0 vm.prank(deployer); - depository.deposit(0, bamount); + depository.deposit(0, bamount, vesting); // Increase time such that the vesting is complete vm.warp(block.timestamp + vesting + 60); From dc18a3dc7a57933d4c3a9387d8b066333c20e9ce Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Mon, 22 Jul 2024 12:49:04 +0100 Subject: [PATCH 15/17] chore: remove unused import --- contracts/BondCalculator.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index 98974766..fb6bfce1 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import {mulDiv} from "@prb/math/src/Common.sol"; import {GenericBondCalculator} from "./GenericBondCalculator.sol"; import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; From bb35f9a184701361f1a6dbf945560225748538d4 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 24 Jul 2024 11:31:13 +0100 Subject: [PATCH 16/17] refactor: remove numNewOwners tracking --- contracts/Tokenomics.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/Tokenomics.sol b/contracts/Tokenomics.sol index 76eb6f51..161ce007 100644 --- a/contracts/Tokenomics.sol +++ b/contracts/Tokenomics.sol @@ -950,13 +950,6 @@ contract Tokenomics is TokenomicsConstants { if (!mapNewUnits[unitType][serviceUnitIds[j]]) { mapNewUnits[unitType][serviceUnitIds[j]] = true; mapEpochTokenomics[curEpoch].unitPoints[unitType].numNewUnits++; - // Check if the owner has introduced component / agent for the first time - // This is done together with the new unit check, otherwise it could be just a new unit owner - address unitOwner = IToken(registries[unitType]).ownerOf(serviceUnitIds[j]); - if (!mapNewOwners[unitOwner]) { - mapNewOwners[unitOwner] = true; - mapEpochTokenomics[curEpoch].epochPoint.numNewOwners++; - } } } } @@ -974,7 +967,6 @@ contract Tokenomics is TokenomicsConstants { /// ==> mapEpochTokenomics[epochCounter].epochPoint.totalDonationsETH == old(mapEpochTokenomics[epochCounter].epochPoint.totalDonationsETH) + donationETH; /// #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); function trackServiceDonations( address donator, uint256[] memory serviceIds, From 7035d0417ce8f122777c4f604239ca2c482466ea Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 24 Jul 2024 14:41:25 +0100 Subject: [PATCH 17/17] refactor: accounting for PR comments --- contracts/BondCalculator.sol | 103 +++++++++++++++--- contracts/Depository.sol | 19 +++- contracts/GenericBondCalculator.sol | 103 ------------------ ...TestTokenomics.sol => TestTokenomics._sol} | 0 4 files changed, 99 insertions(+), 126 deletions(-) delete mode 100644 contracts/GenericBondCalculator.sol rename contracts/test/{TestTokenomics.sol => TestTokenomics._sol} (100%) diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol index fb6bfce1..abb35d5a 100644 --- a/contracts/BondCalculator.sol +++ b/contracts/BondCalculator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import {GenericBondCalculator} from "./GenericBondCalculator.sol"; +import {mulDiv} from "@prb/math/src/Common.sol"; import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol"; +import "./interfaces/IUniswapV2Pair.sol"; interface ITokenomics { /// @dev Gets number of new units that were donated in the last epoch. @@ -63,12 +64,16 @@ struct Product { /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - -contract BondCalculator is GenericBondCalculator { +contract BondCalculator { event OwnerUpdated(address indexed owner); event DiscountParamsUpdated(DiscountParams newDiscountParams); // Maximum sum of discount factor weights uint256 public constant MAX_SUM_WEIGHTS = 10_000; + // OLAS contract address + address public immutable olas; + // Tokenomics contract address + address public immutable tokenomics; // veOLAS contract address address public immutable ve; @@ -83,14 +88,14 @@ contract BondCalculator is GenericBondCalculator { /// @param _tokenomics Tokenomics contract address. /// @param _ve veOLAS contract address. /// @param _discountParams Discount factor parameters. - constructor(address _olas, address _tokenomics, address _ve, DiscountParams memory _discountParams) - GenericBondCalculator(_olas, _tokenomics) - { - // Check for zero address - if (_ve == address(0)) { + constructor(address _olas, address _tokenomics, address _ve, DiscountParams memory _discountParams) { + // Check for at least one zero contract address + if (_olas == address(0) || _tokenomics == address(0) || _ve == address(0)) { revert ZeroAddress(); } + olas = _olas; + tokenomics = _tokenomics; ve = _ve; owner = msg.sender; @@ -153,17 +158,14 @@ contract BondCalculator is GenericBondCalculator { } /// @dev Calculated inverse discount factor based on bonding and account parameters. - /// @param data Custom data that is used to calculate the IDF: - /// - account Account address. - /// - bondVestingTime Bond vesting time. - /// - productMaxVestingTime Product max vesting time. - /// - productSupply Current product supply. - /// - productPayout Current product payout. + /// @param account Account address. + /// @param bondVestingTime Bond vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. /// @return idf Inverse discount factor in 18 decimals format. - function calculateIDF(bytes memory data) public view override returns (uint256 idf) { - // Decode the required data - (address account, uint256 bondVestingTime, uint256 productMaxVestingTime, uint256 productSupply, - uint256 productPayout) = abi.decode(data, (address, uint256, uint256, uint256, uint256)); + function calculateIDF(address account, uint256 bondVestingTime, uint256 productMaxVestingTime, uint256 productSupply, + uint256 productPayout) public view returns (uint256 idf) { // Get the copy of the discount params DiscountParams memory localParams = discountParams; @@ -226,6 +228,73 @@ contract BondCalculator is GenericBondCalculator { idf = 1e18 + discountBooster; } + /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. + /// @param account Account address. + /// @param tokenAmount LP token amount. + /// @param priceLP LP token price. + /// @param bondVestingTime Bond vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. + /// @return amountOLAS Resulting amount of OLAS tokens. + function calculatePayoutOLAS( + address account, + uint256 tokenAmount, + uint256 priceLP, + uint256 bondVestingTime, + uint256 productMaxVestingTime, + uint256 productSupply, + uint256 productPayout + ) external view returns (uint256 amountOLAS) { + // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation + // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; + // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) + // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced); + // tokenAmount is of the order of sqrt(r0*r1) ~ 104 bits (if balanced) or sqrt(96) ~ 10 bits (if max unbalanced); + // overall: 64 + 53 + 104 = 221 < 256 - regular case if LP is balanced, and 64 + 147 + 10 = 221 < 256 if unbalanced + // mulDiv will correctly fit the total amount up to the value of max uint256, i.e., max of priceLP and max of tokenAmount, + // however their multiplication can not be bigger than the max of uint192 + uint256 totalTokenValue = mulDiv(priceLP, tokenAmount, 1); + // Check for the cumulative LP tokens value limit + if (totalTokenValue > type(uint192).max) { + revert Overflow(totalTokenValue, type(uint192).max); + } + + // Calculate the dynamic inverse discount factor + uint256 idf = calculateIDF(account, bondVestingTime, productMaxVestingTime, productSupply, productPayout); + + // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 + // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 + amountOLAS = (idf * totalTokenValue) / 1e36; + } + + /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. + /// @notice The price LP calculation is based on the UniswapV2Pair contract. + /// @param token Token address. + /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. + function getCurrentPriceLP(address token) external view returns (uint256 priceLP) { + IUniswapV2Pair pair = IUniswapV2Pair(token); + uint256 totalSupply = pair.totalSupply(); + if (totalSupply > 0) { + address token0 = pair.token0(); + address token1 = pair.token1(); + uint256 reserve0; + uint256 reserve1; + // requires low gas + (reserve0, reserve1, ) = pair.getReserves(); + // token0 != olas && token1 != olas, this should never happen + if (token0 == olas || token1 == olas) { + // If OLAS is in token0, assign its reserve to reserve1, otherwise the reserve1 is already correct + if (token0 == olas) { + reserve1 = reserve0; + } + // Calculate the LP price based on reserves and totalSupply ratio multiplied by 1e18 + // Inspired by: https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy#L262 + priceLP = (reserve1 * 1e18) / totalSupply; + } + } + } + function getDiscountParams() external view returns (DiscountParams memory) { return discountParams; } diff --git a/contracts/Depository.sol b/contracts/Depository.sol index 5b17a2a7..f18559fc 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -9,14 +9,22 @@ import {ITreasury} from "./interfaces/ITreasury.sol"; interface IBondCalculator { /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. + /// @param account Account address. /// @param tokenAmount LP token amount. /// @param priceLP LP token price. - /// @param data Custom data that is used to calculate the IDF. + /// @param bondVestingTime Bond vesting time. + /// @param productMaxVestingTime Product max vesting time. + /// @param productSupply Current product supply. + /// @param productPayout Current product payout. /// @return amountOLAS Resulting amount of OLAS tokens. function calculatePayoutOLAS( + address account, uint256 tokenAmount, uint256 priceLP, - bytes memory data + uint256 bondVestingTime, + uint256 productMaxVestingTime, + uint256 productSupply, + uint256 productPayout ) external view returns (uint256 amountOLAS); /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. @@ -114,7 +122,7 @@ contract Depository is ERC721, IErrorsTokenomics { // OLAS token address address public immutable olas; - // Tkenomics contract address + // Tokenomics contract address address public tokenomics; // Treasury contract address address public treasury; @@ -370,9 +378,8 @@ contract Depository is ERC721, IErrorsTokenomics { // Calculate the payout in OLAS tokens based on the LP pair with the inverse discount factor (IDF) calculation // Note that payout cannot be zero since the price LP is non-zero, otherwise the product would not be created - payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP, - // Encode parameters required for the IDF calculation - abi.encode(msg.sender, bondVestingTime, productMaxVestingTime, supply, product.payout)); + payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, + bondVestingTime, productMaxVestingTime, supply, product.payout); // Check for the sufficient supply if (payout > supply) { diff --git a/contracts/GenericBondCalculator.sol b/contracts/GenericBondCalculator.sol deleted file mode 100644 index 5d2f3a0d..00000000 --- a/contracts/GenericBondCalculator.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.25; - -import {mulDiv} from "@prb/math/src/Common.sol"; -import "./interfaces/IUniswapV2Pair.sol"; - -/// @dev Value overflow. -/// @param provided Overflow value. -/// @param max Maximum possible value. -error Overflow(uint256 provided, uint256 max); - -/// @dev Provided zero address. -error ZeroAddress(); - -/// @title GenericBondCalculator - Smart contract for generic bond calculation mechanisms in exchange for OLAS tokens. -/// @author Aleksandr Kuperman - -/// @author Andrey Lebedev - -/// @author Mariapia Moscatiello - -contract GenericBondCalculator { - // OLAS contract address - address public immutable olas; - // Tokenomics contract address - address public immutable tokenomics; - - /// @dev Generic Bond Calculator constructor - /// @param _olas OLAS contract address. - /// @param _tokenomics Tokenomics contract address. - constructor(address _olas, address _tokenomics) { - // Check for at least one zero contract address - if (_olas == address(0) || _tokenomics == address(0)) { - revert ZeroAddress(); - } - - olas = _olas; - tokenomics = _tokenomics; - } - - /// @dev Calculated inverse discount factor. - /// @return idf Inverse discount factor in 18 decimals format. - function calculateIDF(bytes memory) public view virtual returns (uint256 idf) { - // Note: IDF is deprecated in Tokenomics, and can be assumed as equal to 1e18 by default - idf = 1e18; - } - - /// @dev Calculates the amount of OLAS tokens based on the bonding calculator mechanism accounting for dynamic IDF. - /// @param tokenAmount LP token amount. - /// @param priceLP LP token price. - /// @param data Custom data to calculate the IDF. - /// @return amountOLAS Resulting amount of OLAS tokens. - function calculatePayoutOLAS( - uint256 tokenAmount, - uint256 priceLP, - bytes memory data - ) external view virtual returns (uint256 amountOLAS) { - // The result is divided by additional 1e18, since it was multiplied by in the current LP price calculation - // The resulting amountDF can not overflow by the following calculations: idf = 64 bits; - // priceLP = 2 * r0/L * 10^18 = 2*r0*10^18/sqrt(r0*r1) ~= 61 + 96 - sqrt(96 * 112) ~= 53 bits (if LP is balanced) - // or 2* r0/sqrt(r0) * 10^18 => 87 bits + 60 bits = 147 bits (if LP is unbalanced); - // tokenAmount is of the order of sqrt(r0*r1) ~ 104 bits (if balanced) or sqrt(96) ~ 10 bits (if max unbalanced); - // overall: 64 + 53 + 104 = 221 < 256 - regular case if LP is balanced, and 64 + 147 + 10 = 221 < 256 if unbalanced - // mulDiv will correctly fit the total amount up to the value of max uint256, i.e., max of priceLP and max of tokenAmount, - // however their multiplication can not be bigger than the max of uint192 - uint256 totalTokenValue = mulDiv(priceLP, tokenAmount, 1); - // Check for the cumulative LP tokens value limit - if (totalTokenValue > type(uint192).max) { - revert Overflow(totalTokenValue, type(uint192).max); - } - - // Calculate the dynamic inverse discount factor - uint256 idf = calculateIDF(data); - - // Amount with the discount factor is IDF * priceLP * tokenAmount / 1e36 - // At this point of time IDF is bound by the max of uint64, and totalTokenValue is no bigger than the max of uint192 - amountOLAS = (idf * totalTokenValue) / 1e36; - } - - /// @dev Gets current reserves of OLAS / totalSupply of Uniswap V2-like LP tokens. - /// @notice The price LP calculation is based on the UniswapV2Pair contract. - /// @param token Token address. - /// @return priceLP Resulting reserveX / totalSupply ratio with 18 decimals. - function getCurrentPriceLP(address token) external view virtual returns (uint256 priceLP) { - IUniswapV2Pair pair = IUniswapV2Pair(token); - uint256 totalSupply = pair.totalSupply(); - if (totalSupply > 0) { - address token0 = pair.token0(); - address token1 = pair.token1(); - uint256 reserve0; - uint256 reserve1; - // requires low gas - (reserve0, reserve1, ) = pair.getReserves(); - // token0 != olas && token1 != olas, this should never happen - if (token0 == olas || token1 == olas) { - // If OLAS is in token0, assign its reserve to reserve1, otherwise the reserve1 is already correct - if (token0 == olas) { - reserve1 = reserve0; - } - // Calculate the LP price based on reserves and totalSupply ratio multiplied by 1e18 - // Inspired by: https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy#L262 - priceLP = (reserve1 * 1e18) / totalSupply; - } - } - } -} diff --git a/contracts/test/TestTokenomics.sol b/contracts/test/TestTokenomics._sol similarity index 100% rename from contracts/test/TestTokenomics.sol rename to contracts/test/TestTokenomics._sol