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