diff --git a/contracts/BondCalculator.sol b/contracts/BondCalculator.sol new file mode 100644 index 00000000..abb35d5a --- /dev/null +++ b/contracts/BondCalculator.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +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. + /// @return Number of new units. + 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. +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 { + // DAO set voting power limit for the bonding account + // This value is bound by the veOLAS total voting power + uint96 targetVotingPower; + // 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; +} + +// 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; + // 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 BondCalculator - Smart contract for bond calculation payout in exchange for OLAS tokens based on dynamic IDF. +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - +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; + + // Contract owner + address public owner; + // Discount params + DiscountParams public discountParams; + + + /// @dev Bond Calculator constructor. + /// @param _olas OLAS contract address. + /// @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) { + // 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; + + // 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) { + sumWeights += _discountParams.weightFactors[i]; + } + if (sumWeights > MAX_SUM_WEIGHTS) { + revert Overflow(sumWeights, MAX_SUM_WEIGHTS); + } + discountParams = _discountParams; + } + + /// @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 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 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) { + sumWeights += newDiscountParams.weightFactors[i]; + } + if (sumWeights > MAX_SUM_WEIGHTS) { + revert Overflow(sumWeights, MAX_SUM_WEIGHTS); + } + + discountParams = newDiscountParams; + + emit DiscountParamsUpdated(newDiscountParams); + } + + /// @dev Calculated inverse discount factor based on bonding and account parameters. + /// @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(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; + 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.weightFactors[0] > 0) { + uint256 numNewUnits = ITokenomics(tokenomics).getLastEpochNumNewUnits(); + + // If the number of new units exceeds the target, bound by the target number + if (numNewUnits >= localParams.targetNewUnits) { + discountBooster = uint256(localParams.weightFactors[0]) * 1e18; + } else { + discountBooster = (uint256(localParams.weightFactors[0]) * numNewUnits * 1e18) / + uint256(localParams.targetNewUnits); + } + } + + // Second discount booster: booster += k2 * bondVestingTime / productMaxVestingTime + // Add vesting time discount booster + if (localParams.weightFactors[1] > 0) { + 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) { + 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 + // Check the veOLAS balance of a bonding account + 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 + if (vPower >= localParams.targetVotingPower) { + discountBooster += uint256(localParams.weightFactors[3]) * 1e18; + } else { + discountBooster += (uint256(localParams.weightFactors[3]) * vPower * 1e18) / + uint256(localParams.targetVotingPower); + } + } + + // Normalize discount booster by the max sum of weights + discountBooster /= MAX_SUM_WEIGHTS; + + // IDF = 1 + normalized booster + 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 f9c7d64c..f18559fc 100644 --- a/contracts/Depository.sol +++ b/contracts/Depository.sol @@ -1,12 +1,43 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.25; +import {ERC721} from "../lib/solmate/src/tokens/ERC721.sol"; 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 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); + + /// @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. +/// @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; @@ -25,10 +56,8 @@ import {ITreasury} from "./interfaces/ITreasury.sol"; * 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; @@ -40,33 +69,36 @@ 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 /// @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); 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,20 +106,23 @@ 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"; + // 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 - uint32 public bondCounter; + uint256 public totalSupply; // 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 such that any value below is given to the bonding account in order to close the product + uint256 public minOLASLeftoverAmount; // OLAS token address address public immutable olas; - // Tkenomics contract address + // Tokenomics contract address address public tokenomics; // Treasury contract address address public treasury; @@ -100,21 +135,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. @@ -229,9 +282,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)); + 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); } @@ -285,10 +338,10 @@ 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) external + function deposit(uint256 productId, uint256 tokenAmount, uint256 bondVestingTime) external returns (uint256 payout, uint256 maturity, uint256 bondId) { // Check the token amount @@ -305,8 +358,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 (bondVestingTime > productMaxVestingTime) { + revert Overflow(bondVestingTime, productMaxVestingTime); + } // Calculate the bond maturity based on its vesting time - maturity = block.timestamp + product.vesting; + maturity = block.timestamp + bondVestingTime; // Check for the time limits if (maturity > type(uint32).max) { revert Overflow(maturity, type(uint32).max); @@ -315,9 +376,10 @@ contract Depository is 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 = IGenericBondCalculator(bondCalculator).calculatePayoutOLAS(tokenAmount, product.priceLP); + payout = IBondCalculator(bondCalculator).calculatePayoutOLAS(msg.sender, tokenAmount, product.priceLP, + bondVestingTime, productMaxVestingTime, supply, product.payout); // Check for the sufficient supply if (payout > supply) { @@ -326,16 +388,34 @@ 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); + product.payout += uint96(payout); + + // 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)); - // 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); + // Increase bond total supply + totalSupply = bondId + 1; + 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]; @@ -349,8 +429,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) { @@ -365,8 +445,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 @@ -375,9 +456,12 @@ 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]); + emit RedeemBond(productId, msg.sender, bondIds[i], pay); } // Check for the non-zero payout @@ -442,13 +526,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 @@ -485,10 +569,28 @@ contract Depository is IErrorsTokenomics { } } - /// @dev Gets current reserves of OLAS / totalSupply of LP tokens. + /// @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 IGenericBondCalculator(bondCalculator).getCurrentPriceLP(token); + return IBondCalculator(bondCalculator).getCurrentPriceLP(token); + } + + /// @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)); } } diff --git a/contracts/GenericBondCalculator.sol b/contracts/GenericBondCalculator.sol deleted file mode 100644 index 4a0286df..00000000 --- a/contracts/GenericBondCalculator.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import {mulDiv} from "@prb/math/src/Common.sol"; -import "./interfaces/ITokenomics.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 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 { - // OLAS contract address - address public immutable olas; - // Tokenomics contract address - address public immutable tokenomics; - - /// @dev Generic Bond Calcolator 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 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(uint256 tokenAmount, uint256 priceLP) 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); - } - // 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; - } - - /// @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) - { - 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/Tokenomics.sol b/contracts/Tokenomics.sol index 6416cdd5..161ce007 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); @@ -689,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); } @@ -962,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++; - } } } } @@ -986,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, @@ -1026,41 +1006,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 +1017,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 +1214,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 +1400,12 @@ 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].numNewUnits + + mapEpochTokenomics[eCounter].unitPoints[1].numNewUnits; } /// @dev Gets epoch end time. 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/contracts/test/TestTokenomics.sol b/contracts/test/TestTokenomics._sol similarity index 96% rename from contracts/test/TestTokenomics.sol rename to 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. diff --git a/test/Depository.t.sol b/test/Depository.t.sol index 82a811bb..0036156a 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)); @@ -131,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); @@ -148,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); diff --git a/test/Depository2BondCalculator.js b/test/Depository2BondCalculator.js new file mode 100644 index 00000000..37004f6c --- /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 97% rename from test/Depository2.js rename to test/Depository2GenericBondCalculator.js index 1fc69b1b..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; @@ -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); diff --git a/test/Depository.js b/test/DepositoryGenericBondCalculator.js similarity index 96% rename from test/Depository.js rename to test/DepositoryGenericBondCalculator.js index 1d848aa4..22082e6a 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; @@ -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(); @@ -206,20 +207,29 @@ describe("Depository LP", 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 () { @@ -345,7 +355,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 +744,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 +769,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 +791,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 +808,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 +830,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 +878,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 +889,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 +910,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); @@ -909,8 +919,8 @@ describe("Depository LP", 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); @@ -937,8 +947,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]); @@ -962,8 +972,8 @@ describe("Depository LP", 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]); @@ -982,13 +992,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 +1019,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); // 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/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);