diff --git a/packages/contracts/scripts/deploy/dollar/solidityScripting/01_Diamond.s.sol b/packages/contracts/scripts/deploy/dollar/solidityScripting/01_Diamond.s.sol index 275def2ee..1baec77fc 100644 --- a/packages/contracts/scripts/deploy/dollar/solidityScripting/01_Diamond.s.sol +++ b/packages/contracts/scripts/deploy/dollar/solidityScripting/01_Diamond.s.sol @@ -21,6 +21,7 @@ import {CreditNftRedemptionCalculatorFacet} from "../../../../src/dollar/facets/ import {CreditRedemptionCalculatorFacet} from "../../../../src/dollar/facets/CreditRedemptionCalculatorFacet.sol"; import {DollarMintCalculatorFacet} from "../../../../src/dollar/facets/DollarMintCalculatorFacet.sol"; import {DollarMintExcessFacet} from "../../../../src/dollar/facets/DollarMintExcessFacet.sol"; +import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; import {Diamond, DiamondArgs} from "../../../../src/dollar/Diamond.sol"; import {DiamondInit} from "../../../../src/dollar/upgradeInitializers/DiamondInit.sol"; import {UbiquityDollarToken} from "../../../../src/dollar/core/UbiquityDollarToken.sol"; @@ -48,6 +49,7 @@ contract DiamondScript is Constants { bytes4[] selectorsOfStakingFacet; bytes4[] selectorsOfStakingFormulasFacet; bytes4[] selectorsOfTWAPOracleDollar3poolFacet; + bytes4[] selectorsOfUbiquityPoolFacet; // contract types of facets to be deployed Diamond diamond; @@ -66,6 +68,7 @@ contract DiamondScript is Constants { ChefFacet chefFacet; StakingFacet stakingFacet; StakingFormulasFacet stakingFormulasFacet; + UbiquityPoolFacet ubiquityPoolFacet; CreditNftManagerFacet creditNftManagerFacet; CreditNftRedemptionCalculatorFacet creditNftRedemptionCalculatorFacet; @@ -91,6 +94,7 @@ contract DiamondScript is Constants { chefFacet = new ChefFacet(); stakingFacet = new StakingFacet(); stakingFormulasFacet = new StakingFormulasFacet(); + ubiquityPoolFacet = new UbiquityPoolFacet(); creditNftManagerFacet = new CreditNftManagerFacet(); creditNftRedemptionCalculatorFacet = new CreditNftRedemptionCalculatorFacet(); @@ -117,7 +121,8 @@ contract DiamondScript is Constants { "CreditRedemptionCalculatorFacet", "DollarMintCalculatorFacet", "DollarMintExcessFacet", - "CreditClockFacet" + "CreditClockFacet", + "UbiquityPoolFacet" ]; DiamondInit.Args memory initArgs = DiamondInit.Args({ @@ -137,7 +142,7 @@ contract DiamondScript is Constants { initArgs ) }); - IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](16); + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](17); setFacet(cuts); // deploy diamond @@ -146,6 +151,28 @@ contract DiamondScript is Constants { IAccessControl = AccessControlFacet(address(diamond)); StakingFacet IStakingFacet = StakingFacet(address(diamond)); IStakingFacet.setBlockCountInAWeek(420); + + // init UbiquityPool + // add collateral LUSD token + uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed + address lusdAddress = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; + UbiquityPoolFacet(address(diamond)).addCollateralToken( + lusdAddress, + poolCeiling + ); + // enable collateral at index 0 + UbiquityPoolFacet(address(diamond)).toggleCollateral(0); + // set mint and redeem fees + UbiquityPoolFacet(address(diamond)).setFees( + 0, // collateral index + 0, // 0% mint fee + 0 // 0% redeem fee + ); + // set redemption delay to 2 blocks + UbiquityPoolFacet(address(diamond)).setRedemptionDelay(2); + // set mint price threshold to $1.01 and redeem price to $0.99 + UbiquityPoolFacet(address(diamond)).setPriceThresholds(1010000, 990000); + vm.stopBroadcast(); } @@ -267,6 +294,13 @@ contract DiamondScript is Constants { functionSelectors: selectorsOfCreditClockFacet }) ); + cuts[16] = ( + IDiamondCut.FacetCut({ + facetAddress: address(ubiquityPoolFacet), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectorsOfUbiquityPoolFacet + }) + ); } function getSelectors() internal { @@ -319,5 +353,8 @@ contract DiamondScript is Constants { selectorsOfTWAPOracleDollar3poolFacet = getSelectorsFromAbi( "/out/TWAPOracleDollar3poolFacet.sol/TWAPOracleDollar3poolFacet.json" ); + selectorsOfUbiquityPoolFacet = getSelectorsFromAbi( + "/out/UbiquityPoolFacet.sol/UbiquityPoolFacet.json" + ); } } diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index c975647ad..da1cc4099 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -1,13 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; -// Modified from FraxPool.sol by Frax Finance -// https://github.com/FraxFinance/frax-solidity/blob/master/src/hardhat/contracts/Frax/Pools/FraxPool.sol - -import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; +import {IUbiquityPool} from "../interfaces/IUbiquityPool.sol"; import {Modifiers} from "../libraries/LibAppStorage.sol"; -import {IMetaPool} from "../interfaces/IMetaPool.sol"; -import "../interfaces/IUbiquityPool.sol"; +import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; /** * @notice Ubiquity pool facet @@ -15,85 +11,187 @@ import "../interfaces/IUbiquityPool.sol"; * - deposit collateral in exchange for Ubiquity Dollars * - redeem Ubiquity Dollars in exchange for the earlier provided collateral */ -contract UbiquityPoolFacet is Modifiers, IUbiquityPool { +contract UbiquityPoolFacet is IUbiquityPool, Modifiers { + //===================== + // Views + //===================== + + /// @inheritdoc IUbiquityPool + function allCollaterals() external view returns (address[] memory) { + return LibUbiquityPool.allCollaterals(); + } + + /// @inheritdoc IUbiquityPool + function collateralInformation( + address collateralAddress + ) + external + view + returns (LibUbiquityPool.CollateralInformation memory returnData) + { + return LibUbiquityPool.collateralInformation(collateralAddress); + } + + /// @inheritdoc IUbiquityPool + function collateralUsdBalance() + external + view + returns (uint256 balanceTally) + { + return LibUbiquityPool.collateralUsdBalance(); + } + + /// @inheritdoc IUbiquityPool + function freeCollateralBalance( + uint256 collateralIndex + ) external view returns (uint256) { + return LibUbiquityPool.freeCollateralBalance(collateralIndex); + } + + /// @inheritdoc IUbiquityPool + function getDollarInCollateral( + uint256 collateralIndex, + uint256 dollarAmount + ) external view returns (uint256) { + return + LibUbiquityPool.getDollarInCollateral( + collateralIndex, + dollarAmount + ); + } + + /// @inheritdoc IUbiquityPool + function getDollarPriceUsd() + external + view + returns (uint256 dollarPriceUsd) + { + return LibUbiquityPool.getDollarPriceUsd(); + } + + //==================== + // Public functions + //==================== + /// @inheritdoc IUbiquityPool function mintDollar( - address collateralAddress, - uint256 collateralAmount, - uint256 dollarOutMin - ) external { - LibUbiquityPool.mintDollar( - collateralAddress, - collateralAmount, - dollarOutMin - ); + uint256 collateralIndex, + uint256 dollarAmount, + uint256 dollarOutMin, + uint256 maxCollateralIn + ) external returns (uint256 totalDollarMint, uint256 collateralNeeded) { + return + LibUbiquityPool.mintDollar( + collateralIndex, + dollarAmount, + dollarOutMin, + maxCollateralIn + ); } /// @inheritdoc IUbiquityPool function redeemDollar( - address collateralAddress, + uint256 collateralIndex, uint256 dollarAmount, uint256 collateralOutMin - ) external { - LibUbiquityPool.redeemDollar( - collateralAddress, - dollarAmount, - collateralOutMin - ); + ) external returns (uint256 collateralOut) { + return + LibUbiquityPool.redeemDollar( + collateralIndex, + dollarAmount, + collateralOutMin + ); + } + + /// @inheritdoc IUbiquityPool + function collectRedemption( + uint256 collateralIndex + ) external returns (uint256 collateralAmount) { + return LibUbiquityPool.collectRedemption(collateralIndex); } + //========================= + // AMO minters functions + //========================= + + /// @inheritdoc IUbiquityPool + function amoMinterBorrow(uint256 collateralAmount) external { + LibUbiquityPool.amoMinterBorrow(collateralAmount); + } + + //======================== + // Restricted functions + //======================== + /// @inheritdoc IUbiquityPool - function collectRedemption(address collateralAddress) external { - LibUbiquityPool.collectRedemption(collateralAddress); + function addAmoMinter(address amoMinterAddress) external onlyAdmin { + LibUbiquityPool.addAmoMinter(amoMinterAddress); } /// @inheritdoc IUbiquityPool - function addToken( + function addCollateralToken( address collateralAddress, - IMetaPool collateralMetaPool + uint256 poolCeiling ) external onlyAdmin { - LibUbiquityPool.addToken(collateralAddress, collateralMetaPool); + LibUbiquityPool.addCollateralToken(collateralAddress, poolCeiling); } /// @inheritdoc IUbiquityPool - function setRedeemActive( - address collateralAddress, - bool notRedeemPaused + function removeAmoMinter(address amoMinterAddress) external onlyAdmin { + LibUbiquityPool.removeAmoMinter(amoMinterAddress); + } + + /// @inheritdoc IUbiquityPool + function setCollateralPrice( + uint256 collateralIndex, + uint256 newPrice ) external onlyAdmin { - LibUbiquityPool.setRedeemActive(collateralAddress, notRedeemPaused); + LibUbiquityPool.setCollateralPrice(collateralIndex, newPrice); } /// @inheritdoc IUbiquityPool - function getRedeemActive( - address _collateralAddress - ) external view returns (bool) { - return LibUbiquityPool.getRedeemActive(_collateralAddress); + function setFees( + uint256 collateralIndex, + uint256 newMintFee, + uint256 newRedeemFee + ) external onlyAdmin { + LibUbiquityPool.setFees(collateralIndex, newMintFee, newRedeemFee); } /// @inheritdoc IUbiquityPool - function setMintActive( - address collateralAddress, - bool notMintPaused + function setPoolCeiling( + uint256 collateralIndex, + uint256 newCeiling ) external onlyAdmin { - LibUbiquityPool.setMintActive(collateralAddress, notMintPaused); + LibUbiquityPool.setPoolCeiling(collateralIndex, newCeiling); } /// @inheritdoc IUbiquityPool - function getMintActive( - address _collateralAddress - ) external view returns (bool) { - return LibUbiquityPool.getMintActive(_collateralAddress); + function setPriceThresholds( + uint256 newMintPriceThreshold, + uint256 newRedeemPriceThreshold + ) external onlyAdmin { + LibUbiquityPool.setPriceThresholds( + newMintPriceThreshold, + newRedeemPriceThreshold + ); } /// @inheritdoc IUbiquityPool - function getRedeemCollateralBalances( - address account, - address collateralAddress - ) external view returns (uint256) { - return - LibUbiquityPool.getRedeemCollateralBalances( - account, - collateralAddress - ); + function setRedemptionDelay(uint256 newRedemptionDelay) external onlyAdmin { + LibUbiquityPool.setRedemptionDelay(newRedemptionDelay); + } + + /// @inheritdoc IUbiquityPool + function toggleCollateral(uint256 collateralIndex) external onlyAdmin { + LibUbiquityPool.toggleCollateral(collateralIndex); + } + + /// @inheritdoc IUbiquityPool + function toggleMRB( + uint256 collateralIndex, + uint8 toggleIndex + ) external onlyAdmin { + LibUbiquityPool.toggleMRB(collateralIndex, toggleIndex); } } diff --git a/packages/contracts/src/dollar/interfaces/IDollarAmoMinter.sol b/packages/contracts/src/dollar/interfaces/IDollarAmoMinter.sol new file mode 100644 index 000000000..f1d21eb87 --- /dev/null +++ b/packages/contracts/src/dollar/interfaces/IDollarAmoMinter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/// @notice AMO minter interface +/// @dev AMO minter can borrow collateral from the Ubiquity Pool to make some yield +interface IDollarAmoMinter { + /// @notice Returns collateral Dollar balance + /// @return Collateral Dollar balance + function collateralDollarBalance() external view returns (uint256); + + /// @notice Returns collateral index (from the Ubiquity Pool) for which AMO minter is responsible + /// @return Collateral token index + function collateralIndex() external view returns (uint256); +} diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index 3688b7212..afefa8357 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.8.19; +pragma solidity 0.8.19; -import {IMetaPool} from "./IMetaPool.sol"; +import {LibUbiquityPool} from "../libraries/LibUbiquityPool.sol"; /** * @notice Ubiquity pool interface @@ -10,17 +10,82 @@ import {IMetaPool} from "./IMetaPool.sol"; * - redeem Ubiquity Dollars in exchange for the earlier provided collateral */ interface IUbiquityPool { + //===================== + // Views + //===================== + + /** + * @notice Returns all collateral addresses + * @return All collateral addresses + */ + function allCollaterals() external view returns (address[] memory); + + /** + * @notice Returns collateral information + * @param collateralAddress Address of the collateral token + * @return returnData Collateral info + */ + function collateralInformation( + address collateralAddress + ) + external + view + returns (LibUbiquityPool.CollateralInformation memory returnData); + + /** + * @notice Returns USD value of all collateral tokens held in the pool, in E18 + * @return balanceTally USD value of all collateral tokens + */ + function collateralUsdBalance() + external + view + returns (uint256 balanceTally); + + /** + * @notice Returns free collateral balance (i.e. that can be borrowed by AMO minters) + * @param collateralIndex collateral token index + * @return Amount of free collateral + */ + function freeCollateralBalance( + uint256 collateralIndex + ) external view returns (uint256); + + /** + * @notice Returns Dollar value in collateral tokens + * @param collateralIndex collateral token index + * @param dollarAmount Amount of Dollars + * @return Value in collateral tokens + */ + function getDollarInCollateral( + uint256 collateralIndex, + uint256 dollarAmount + ) external view returns (uint256); + + /** + * @notice Returns Ubiquity Dollar token USD price (1e6 precision) from Curve Metapool (Ubiquity Dollar, Curve Tri-Pool LP) + * @return dollarPriceUsd USD price of Ubiquity Dollar + */ + function getDollarPriceUsd() external view returns (uint256 dollarPriceUsd); + + //==================== + // Public functions + //==================== + /** - * @notice Mints 1 Ubiquity Dollar for every 1 USD of `collateralAddress` token deposited - * @param collateralAddress address of collateral token being deposited - * @param collateralAmount amount of collateral tokens being deposited - * @param dollarOutMin minimum amount of Ubiquity Dollars that'll be minted, used to set acceptable slippage + * @notice Mints Dollars in exchange for collateral tokens + * @param collateralIndex Collateral token index + * @param dollarAmount Amount of dollars to mint + * @param dollarOutMin Min amount of dollars to mint (slippage protection) + * @param maxCollateralIn Max amount of collateral to send (slippage protection) + * @return totalDollarMint Amount of Dollars minted + * @return collateralNeeded Amount of collateral sent to the pool */ function mintDollar( - address collateralAddress, - uint256 collateralAmount, - uint256 dollarOutMin - ) external; + uint256 collateralIndex, + uint256 dollarAmount, + uint256 dollarOutMin, + uint256 maxCollateralIn + ) external returns (uint256 totalDollarMint, uint256 collateralNeeded); /** * @notice Burns redeemable Ubiquity Dollars and sends back 1 USD of collateral token for every 1 Ubiquity Dollar burned @@ -28,15 +93,16 @@ interface IUbiquityPool { * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param collateralAddress address of collateral token being withdrawn - * @param dollarAmount amount of Ubiquity Dollars being burned - * @param collateralOutMin minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage + * @param collateralIndex Collateral token index being withdrawn + * @param dollarAmount Amount of Ubiquity Dollars being burned + * @param collateralOutMin Minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage + * @return collateralOut Amount of collateral tokens ready for redemption */ function redeemDollar( - address collateralAddress, + uint256 collateralIndex, uint256 dollarAmount, uint256 collateralOutMin - ) external; + ) external returns (uint256 collateralOut); /** * @notice Used to collect collateral tokens after redeeming/burning Ubiquity Dollars @@ -44,70 +110,113 @@ interface IUbiquityPool { * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param collateralAddress address of the collateral token being collected + * @param collateralIndex Collateral token index being collected + * @return collateralAmount Amount of collateral tokens redeemed */ - function collectRedemption(address collateralAddress) external; + function collectRedemption( + uint256 collateralIndex + ) external returns (uint256 collateralAmount); + + //========================= + // AMO minters functions + //========================= + + /** + * @notice Allows AMO minters to borrow collateral to make yield in external + * protocols like Compound, Curve, erc... + * @dev Bypasses the gassy mint->redeem cycle for AMOs to borrow collateral + * @param collateralAmount Amount of collateral to borrow + */ + function amoMinterBorrow(uint256 collateralAmount) external; + + //======================== + // Restricted functions + //======================== + + /** + * @notice Adds a new AMO minter + * @param amoMinterAddress AMO minter address + */ + function addAmoMinter(address amoMinterAddress) external; /** - * @notice Admin function for whitelisting a token as collateral - * @param collateralAddress Address of the token being whitelisted - * @param collateralMetaPool 3CRV Metapool for the token being whitelisted + * @notice Adds a new collateral token + * @param collateralAddress Collateral token address + * @param poolCeiling Max amount of available tokens for collateral */ - function addToken( + function addCollateralToken( address collateralAddress, - IMetaPool collateralMetaPool + uint256 poolCeiling ) external; /** - * @notice Admin function to pause and unpause redemption for a specific collateral token - * @param collateralAddress Address of the token being affected - * @param notRedeemPaused True to turn on redemption for token, false to pause redemption of token + * @notice Removes AMO minter + * @param amoMinterAddress AMO minter address to remove */ - function setRedeemActive( - address collateralAddress, - bool notRedeemPaused + function removeAmoMinter(address amoMinterAddress) external; + + /** + * @notice Sets collateral token price in USD + * @param collateralIndex Collateral token index + * @param newPrice New USD price (precision 1e6) + */ + function setCollateralPrice( + uint256 collateralIndex, + uint256 newPrice ) external; /** - * @notice Checks whether redeem is enabled for the `_collateralAddress` token - * @param _collateralAddress Token address to check - * @return Whether redeem is enabled for the `_collateralAddress` token + * @notice Sets mint and redeem fees, 1_000_000 = 100% + * @param collateralIndex Collateral token index + * @param newMintFee New mint fee + * @param newRedeemFee New redeem fee */ - function getRedeemActive( - address _collateralAddress - ) external view returns (bool); + function setFees( + uint256 collateralIndex, + uint256 newMintFee, + uint256 newRedeemFee + ) external; /** - * @notice Admin function to pause and unpause minting for a specific collateral token - * @param collateralAddress Address of the token being affected - * @param notMintPaused True to turn on minting for token, false to pause minting for token + * @notice Sets max amount of collateral for a particular collateral token + * @param collateralIndex Collateral token index + * @param newCeiling Max amount of collateral */ - function setMintActive( - address collateralAddress, - bool notMintPaused + function setPoolCeiling( + uint256 collateralIndex, + uint256 newCeiling ) external; /** - * @notice Checks whether mint is enabled for the `_collateralAddress` token - * @param _collateralAddress Token address to check - * @return Whether mint is enabled for the `_collateralAddress` token + * @notice Sets mint and redeem price thresholds, 1_000_000 = $1.00 + * @param newMintPriceThreshold New mint price threshold + * @param newRedeemPriceThreshold New redeem price threshold */ - function getMintActive( - address _collateralAddress - ) external view returns (bool); + function setPriceThresholds( + uint256 newMintPriceThreshold, + uint256 newRedeemPriceThreshold + ) external; /** - * @notice Returns the amount of collateral ready for collecting after redeeming - * @dev Redeem process is split in two steps: + * @notice Sets a redemption delay in blocks + * @dev Redeeming is split in 2 actions: * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` - * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param account Account address for which to check the balance ready to be collected - * @param collateralAddress Collateral token address - * @return Collateral token balance ready to be collected after redeeming + * @dev `newRedemptionDelay` sets number of blocks that should be mined after which user can call `collectRedemption()` + * @param newRedemptionDelay Redemption delay in blocks */ - function getRedeemCollateralBalances( - address account, - address collateralAddress - ) external view returns (uint256); + function setRedemptionDelay(uint256 newRedemptionDelay) external; + + /** + * @notice Toggles (i.e. enables/disables) a particular collateral token + * @param collateralIndex Collateral token index + */ + function toggleCollateral(uint256 collateralIndex) external; + + /** + * @notice Toggles pause for mint/redeem/borrow methods + * @param collateralIndex Collateral token index + * @param toggleIndex Method index. 0 - toggle mint pause, 1 - toggle redeem pause, 2 - toggle borrow by AMO pause + */ + function toggleMRB(uint256 collateralIndex, uint8 toggleIndex) external; } diff --git a/packages/contracts/src/dollar/libraries/Constants.sol b/packages/contracts/src/dollar/libraries/Constants.sol index a2b323acb..006e1e9c4 100644 --- a/packages/contracts/src/dollar/libraries/Constants.sol +++ b/packages/contracts/src/dollar/libraries/Constants.sol @@ -86,3 +86,6 @@ bytes32 constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae012 uint256 constant _NOT_ENTERED = 1; /// @dev Reentrancy constant uint256 constant _ENTERED = 2; + +/// @dev Ubiquity pool price precision +uint256 constant UBIQUITY_POOL_PRICE_PRECISION = 1e6; diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index 6c3345ba1..6ee5a5461 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -1,18 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; -// Modified from FraxPool.sol by Frax Finance -// https://github.com/FraxFinance/frax-solidity/blob/master/src/hardhat/contracts/Frax/Pools/FraxPool.sol - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {UbiquityDollarToken} from "../core/UbiquityDollarToken.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Ubiquity} from "../interfaces/IERC20Ubiquity.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import {IStableSwap3Pool} from "../interfaces/IStableSwap3Pool.sol"; -import {IMetaPool} from "../interfaces/IMetaPool.sol"; -import {LibAppStorage, AppStorage} from "./LibAppStorage.sol"; +import {IDollarAmoMinter} from "../interfaces/IDollarAmoMinter.sol"; +import {IERC20Ubiquity} from "../interfaces/IERC20Ubiquity.sol"; +import {UBIQUITY_POOL_PRICE_PRECISION} from "./Constants.sol"; +import {LibAppStorage} from "./LibAppStorage.sol"; import {LibTWAPOracle} from "./LibTWAPOracle.sol"; /** @@ -20,11 +16,11 @@ import {LibTWAPOracle} from "./LibTWAPOracle.sol"; * @notice Allows users to: * - deposit collateral in exchange for Ubiquity Dollars * - redeem Ubiquity Dollars in exchange for the earlier provided collateral + * @dev Modified from https://github.com/FraxFinance/frax-solidity/blob/fc9810d72c520d256965b81b6c9cc6aa95d07d9d/src/hardhat/contracts/Frax/Pools/FraxPoolV3.sol */ library LibUbiquityPool { - using SafeMath for uint256; - using SafeMath for uint8; using SafeERC20 for IERC20; + using SafeMath for uint256; /// @notice Storage slot used to store data for this library bytes32 constant UBIQUITY_POOL_STORAGE_POSITION = @@ -32,6 +28,79 @@ library LibUbiquityPool { uint256(keccak256("ubiquity.contracts.ubiquity.pool.storage")) - 1 ); + /// @notice Struct used as a storage for this library + struct UbiquityPoolStorage { + //======== + // Core + //======== + // minter address -> is it enabled + mapping(address => bool) amoMinterAddresses; + //====================== + // Collateral related + //====================== + // available collateral tokens + address[] collateralAddresses; + // collateral address -> collateral index + mapping(address => uint256) collateralAddressToIndex; + // Stores price of the collateral, if price is paused. CONSIDER ORACLES EVENTUALLY!!! + uint256[] collateralPrices; + // array collateral symbols + string[] collateralSymbols; + // collateral address -> is it enabled + mapping(address => bool) enabledCollaterals; + // Number of decimals needed to get to E18. collateral index -> missing_decimals + uint256[] missingDecimals; + // Total across all collaterals. Accounts for missing_decimals + uint256[] poolCeilings; + //==================== + // Redeem related + //==================== + // user -> block number (collateral independent) + mapping(address => uint256) lastRedeemed; + // 1010000 = $1.01 + uint256 mintPriceThreshold; + // 990000 = $0.99 + uint256 redeemPriceThreshold; + // address -> collateral index -> balance + mapping(address => mapping(uint256 => uint256)) redeemCollateralBalances; + // number of blocks to wait before being able to collectRedemption() + uint256 redemptionDelay; + // collateral index -> balance + uint256[] unclaimedPoolCollateral; + //================ + // Fees related + //================ + // minting fee of a particular collateral index, 1_000_000 = 100% + uint256[] mintingFee; + // redemption fee of a particular collateral index, 1_000_000 = 100% + uint256[] redemptionFee; + //================= + // Pause related + //================= + // whether borrowing collateral by AMO minters is paused for a particular collateral index + bool[] borrowingPaused; + // whether minting is paused for a particular collateral index + bool[] mintPaused; + // whether redeeming is paused for a particular collateral index + bool[] redeemPaused; + } + + /// @notice Struct used for detailed collateral information + struct CollateralInformation { + uint256 index; + string symbol; + address collateralAddress; + bool isEnabled; + uint256 missingDecimals; + uint256 price; + uint256 poolCeiling; + bool mintPaused; + bool redeemPaused; + bool borrowingPaused; + uint256 mintingFee; + uint256 redemptionFee; + } + /** * @notice Returns struct used as a storage for this library * @return uPoolStorage Struct used as a storage @@ -47,97 +116,254 @@ library LibUbiquityPool { } } - /// @notice Struct used as a storage for this library - struct UbiquityPoolStorage { - /* ========== STATE VARIABLES ========== */ - address[] collateralAddresses; - mapping(address => IMetaPool) collateralMetaPools; - mapping(address => uint8) missingDecimals; - mapping(address => uint256) tokenBalances; - mapping(address => bool) collateralRedeemActive; - mapping(address => bool) collateralMintActive; - uint256 mintingFee; - uint256 redemptionFee; - mapping(address => mapping(address => uint256)) redeemCollateralBalances; - mapping(address => uint256) unclaimedPoolCollateral; - mapping(address => uint256) lastRedeemed; - // Pool_ceiling is the total units of collateral that a pool contract can hold - uint256 poolCeiling; - // Stores price of the collateral, if price is paused - uint256 pausedPrice; - // Number of blocks to wait before being able to collectRedemption() - uint256 redemptionDelay; - // Min USD value of UbiquityDollarToken for minting to happen - uint256 dollarFloor; - } - - // Custom Modifiers // + //=========== + // Events + //=========== + + /// @notice Emitted when new AMO minter is added + event AmoMinterAdded(address amoMinterAddress); + /// @notice Emitted when AMO minter is removed + event AmoMinterRemoved(address amoMinterAddress); + /// @notice Emitted on setting a collateral price + event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice); + /// @notice Emitted on enabling/disabling a particular collateral token + event CollateralToggled(uint256 collateralIndex, bool newState); + /// @notice Emitted when fees are updated + event FeesSet( + uint256 collateralIndex, + uint256 newMintFee, + uint256 newRedeemFee + ); + /// @notice Emitted on toggling pause for mint/redeem/borrow + event MRBToggled(uint256 collateralIndex, uint8 toggleIndex); + /// @notice Emitted when new pool ceiling (i.e. max amount of collateral) is set + event PoolCeilingSet(uint256 collateralIndex, uint256 newCeiling); + /// @notice Emitted when mint and redeem price thresholds are updated (1_000_000 = $1.00) + event PriceThresholdsSet( + uint256 newMintPriceThreshold, + uint256 newRedeemPriceThreshold + ); + /// @notice Emitted when a new redemption delay in blocks is set + event RedemptionDelaySet(uint256 redemptionDelay); + + //===================== + // Modifiers + //===================== - /// @notice Checks whether redeem is enabled for the `collateralAddress` token - modifier redeemActive(address collateralAddress) { + /** + * @notice Checks whether collateral token is enabled (i.e. mintable and redeemable) + * @param collateralIndex Collateral token index + */ + modifier collateralEnabled(uint256 collateralIndex) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); require( - ubiquityPoolStorage().collateralRedeemActive[collateralAddress] + poolStorage.enabledCollaterals[ + poolStorage.collateralAddresses[collateralIndex] + ], + "Collateral disabled" ); _; } - /// @notice Checks whether mint is enabled for the `collateralAddress` token - modifier mintActive(address collateralAddress) { - require(ubiquityPoolStorage().collateralMintActive[collateralAddress]); + /** + * @notice Checks whether a caller is the AMO minter address + */ + modifier onlyAmoMinters() { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + require( + poolStorage.amoMinterAddresses[msg.sender], + "Not an AMO Minter" + ); _; } - // User Functions // + //===================== + // Views + //===================== + + /** + * @notice Returns all collateral addresses + * @return All collateral addresses + */ + function allCollaterals() internal view returns (address[] memory) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return poolStorage.collateralAddresses; + } + + /** + * @notice Returns collateral information + * @param collateralAddress Address of the collateral token + * @return returnData Collateral info + */ + function collateralInformation( + address collateralAddress + ) internal view returns (CollateralInformation memory returnData) { + // load the storage + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + // validation + require( + poolStorage.enabledCollaterals[collateralAddress], + "Invalid collateral" + ); + + // get the index + uint256 index = poolStorage.collateralAddressToIndex[collateralAddress]; + + returnData = CollateralInformation( + index, + poolStorage.collateralSymbols[index], + collateralAddress, + poolStorage.enabledCollaterals[collateralAddress], + poolStorage.missingDecimals[index], + poolStorage.collateralPrices[index], + poolStorage.poolCeilings[index], + poolStorage.mintPaused[index], + poolStorage.redeemPaused[index], + poolStorage.borrowingPaused[index], + poolStorage.mintingFee[index], + poolStorage.redemptionFee[index] + ); + } + + /** + * @notice Returns USD value of all collateral tokens held in the pool, in E18 + * @return balanceTally USD value of all collateral tokens + */ + function collateralUsdBalance() + internal + view + returns (uint256 balanceTally) + { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + uint256 collateralTokensCount = poolStorage.collateralAddresses.length; + balanceTally = 0; + for (uint256 i = 0; i < collateralTokensCount; i++) { + balanceTally += freeCollateralBalance(i) + .mul(10 ** poolStorage.missingDecimals[i]) + .mul(poolStorage.collateralPrices[i]) + .div(UBIQUITY_POOL_PRICE_PRECISION); + } + } + + /** + * @notice Returns free collateral balance (i.e. that can be borrowed by AMO minters) + * @param collateralIndex collateral token index + * @return Amount of free collateral + */ + function freeCollateralBalance( + uint256 collateralIndex + ) internal view returns (uint256) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return + IERC20(poolStorage.collateralAddresses[collateralIndex]) + .balanceOf(address(this)) + .sub(poolStorage.unclaimedPoolCollateral[collateralIndex]); + } + + /** + * @notice Returns Dollar value in collateral tokens + * @param collateralIndex collateral token index + * @param dollarAmount Amount of Dollars + * @return Value in collateral tokens + */ + function getDollarInCollateral( + uint256 collateralIndex, + uint256 dollarAmount + ) internal view returns (uint256) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return + dollarAmount + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(10 ** poolStorage.missingDecimals[collateralIndex]) + .div(poolStorage.collateralPrices[collateralIndex]); + } + + /** + * @notice Returns Ubiquity Dollar token USD price (1e6 precision) from Curve Metapool (Ubiquity Dollar, Curve Tri-Pool LP) + * @return dollarPriceUsd USD price of Ubiquity Dollar + */ + function getDollarPriceUsd() + internal + view + returns (uint256 dollarPriceUsd) + { + // get Dollar price from Curve Metapool (18 decimals) + uint256 dollarPriceUsdD18 = LibTWAPOracle.getTwapPrice(); + // convert to 6 decimals + dollarPriceUsd = dollarPriceUsdD18 + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(1e18); + } + + //==================== + // Public functions + //==================== /** - * @notice Mints 1 Ubiquity Dollar for every 1 USD of `collateralAddress` token deposited - * @param collateralAddress address of collateral token being deposited - * @param collateralAmount amount of collateral tokens being deposited - * @param dollarOutMin minimum amount of Ubiquity Dollars that'll be minted, used to set acceptable slippage + * @notice Mints Dollars in exchange for collateral tokens + * @param collateralIndex Collateral token index + * @param dollarAmount Amount of dollars to mint + * @param dollarOutMin Min amount of dollars to mint (slippage protection) + * @param maxCollateralIn Max amount of collateral to send (slippage protection) + * @return totalDollarMint Amount of Dollars minted + * @return collateralNeeded Amount of collateral sent to the pool */ function mintDollar( - address collateralAddress, - uint256 collateralAmount, - uint256 dollarOutMin - ) internal mintActive(collateralAddress) { + uint256 collateralIndex, + uint256 dollarAmount, + uint256 dollarOutMin, + uint256 maxCollateralIn + ) + internal + collateralEnabled(collateralIndex) + returns (uint256 totalDollarMint, uint256 collateralNeeded) + { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - uint256 dollarPriceUSD = getDollarPriceUsd(); + require( - checkCollateralToken(collateralAddress), - "Collateral Token not approved" + poolStorage.mintPaused[collateralIndex] == false, + "Minting is paused" ); + + // prevent unnecessary mints require( - dollarPriceUSD >= poolStorage.dollarFloor, - "Ubiquity Dollar Token value must be 1 USD or greater to mint" + getDollarPriceUsd() >= poolStorage.mintPriceThreshold, + "Dollar price too low" ); - uint8 missingDecimals = poolStorage.missingDecimals[collateralAddress]; - uint256 collateralAmountD18 = missingDecimals > 0 - ? collateralAmount * poolStorage.missingDecimals[collateralAddress] - : collateralAmount; + // get amount of collateral for incoming Dollars + collateralNeeded = getDollarInCollateral(collateralIndex, dollarAmount); - uint256 dollarAmountD18 = calcMintDollarAmount( - collateralAmountD18, - getCollateralPriceCurve3Pool(collateralAddress), - getCurve3PriceUSD() - ); + // subtract the minting fee + totalDollarMint = dollarAmount + .mul( + UBIQUITY_POOL_PRICE_PRECISION.sub( + poolStorage.mintingFee[collateralIndex] + ) + ) + .div(UBIQUITY_POOL_PRICE_PRECISION); + + // check slippages + require((totalDollarMint >= dollarOutMin), "Dollar slippage"); + require((collateralNeeded <= maxCollateralIn), "Collateral slippage"); - dollarAmountD18 = dollarAmountD18.sub(poolStorage.mintingFee); - require(dollarOutMin <= dollarAmountD18, "Slippage limit reached"); - IERC20(collateralAddress).safeTransferFrom( - msg.sender, - address(this), - collateralAmount + // check the pool ceiling + require( + freeCollateralBalance(collateralIndex).add(collateralNeeded) <= + poolStorage.poolCeilings[collateralIndex], + "Pool ceiling" ); - poolStorage.tokenBalances[collateralAddress] = poolStorage - .tokenBalances[collateralAddress] - .add(collateralAmount); + // take collateral first + IERC20(poolStorage.collateralAddresses[collateralIndex]) + .safeTransferFrom(msg.sender, address(this), collateralNeeded); + // mint Dollars IERC20Ubiquity ubiquityDollarToken = IERC20Ubiquity( LibAppStorage.appStorage().dollarTokenAddress ); - ubiquityDollarToken.mint(msg.sender, dollarAmountD18); + ubiquityDollarToken.mint(msg.sender, totalDollarMint); } /** @@ -146,60 +372,66 @@ library LibUbiquityPool { * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param collateralAddress address of collateral token being withdrawn - * @param dollarAmount amount of Ubiquity Dollars being burned - * @param collateralOutMin minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage + * @param collateralIndex Collateral token index being withdrawn + * @param dollarAmount Amount of Ubiquity Dollars being burned + * @param collateralOutMin Minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage + * @return collateralOut Amount of collateral tokens ready for redemption */ function redeemDollar( - address collateralAddress, + uint256 collateralIndex, uint256 dollarAmount, uint256 collateralOutMin - ) internal redeemActive(collateralAddress) { + ) + internal + collateralEnabled(collateralIndex) + returns (uint256 collateralOut) + { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - uint256 dollarPriceUSD = getDollarPriceUsd(); - - require( - checkCollateralToken(collateralAddress), - "Collateral Token not approved" - ); require( - dollarPriceUSD < poolStorage.dollarFloor, - "Ubiquity Dollar Token value must be less than 1 USD to redeem" + poolStorage.redeemPaused[collateralIndex] == false, + "Redeeming is paused" ); - uint256 dollarAmountPrecision = dollarAmount.div( - 10 ** poolStorage.missingDecimals[collateralAddress] - ); - uint256 collateralOut = calcRedeemCollateralAmount( - dollarAmountPrecision, - getCollateralPriceCurve3Pool(collateralAddress), - getCurve3PriceUSD() + // prevent unnecessary redemptions that could adversely affect the Dollar price + require( + getDollarPriceUsd() <= poolStorage.redeemPriceThreshold, + "Dollar price too high" ); - collateralOut = collateralOut.sub(poolStorage.redemptionFee); + uint256 dollarAfterFee = dollarAmount + .mul( + UBIQUITY_POOL_PRICE_PRECISION.sub( + poolStorage.redemptionFee[collateralIndex] + ) + ) + .div(UBIQUITY_POOL_PRICE_PRECISION); + collateralOut = getDollarInCollateral(collateralIndex, dollarAfterFee); + // checks require( collateralOut <= - poolStorage.tokenBalances[collateralAddress].sub( - poolStorage.unclaimedPoolCollateral[collateralAddress] - ), - "Requested amount exceeds balance" + (IERC20(poolStorage.collateralAddresses[collateralIndex])) + .balanceOf(address(this)) + .sub(poolStorage.unclaimedPoolCollateral[collateralIndex]), + "Insufficient pool collateral" ); - require(collateralOutMin <= collateralOut, "Slippage limit reached"); + require(collateralOut >= collateralOutMin, "Collateral slippage"); + // account for the redeem delay poolStorage.redeemCollateralBalances[msg.sender][ - collateralAddress + collateralIndex ] = poolStorage - .redeemCollateralBalances[msg.sender][collateralAddress].add( + .redeemCollateralBalances[msg.sender][collateralIndex].add( collateralOut ); - - poolStorage.unclaimedPoolCollateral[collateralAddress] = poolStorage - .unclaimedPoolCollateral[collateralAddress] + poolStorage.unclaimedPoolCollateral[collateralIndex] = poolStorage + .unclaimedPoolCollateral[collateralIndex] .add(collateralOut); poolStorage.lastRedeemed[msg.sender] = block.number; + + // burn Dollars IERC20Ubiquity ubiquityDollarToken = IERC20Ubiquity( LibAppStorage.appStorage().dollarTokenAddress ); @@ -212,243 +444,297 @@ library LibUbiquityPool { * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param collateralAddress address of the collateral token being collected + * @param collateralIndex Collateral token index being collected + * @return collateralAmount Amount of collateral tokens redeemed */ - function collectRedemption(address collateralAddress) internal { + function collectRedemption( + uint256 collateralIndex + ) internal returns (uint256 collateralAmount) { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + require( - poolStorage.lastRedeemed[msg.sender] + - poolStorage.redemptionDelay >= - block.number, - "Too soon to collect" + poolStorage.redeemPaused[collateralIndex] == false, + "Redeeming is paused" + ); + require( + ( + poolStorage.lastRedeemed[msg.sender].add( + poolStorage.redemptionDelay + ) + ) <= block.number, + "Too soon to collect redemption" ); bool sendCollateral = false; - uint256 collateralAmount = 0; if ( - poolStorage.redeemCollateralBalances[msg.sender][ - collateralAddress - ] > 0 + poolStorage.redeemCollateralBalances[msg.sender][collateralIndex] > + 0 ) { collateralAmount = poolStorage.redeemCollateralBalances[msg.sender][ - collateralAddress + collateralIndex ]; poolStorage.redeemCollateralBalances[msg.sender][ - collateralAddress + collateralIndex ] = 0; - poolStorage.unclaimedPoolCollateral[collateralAddress] = poolStorage - .unclaimedPoolCollateral[collateralAddress] + poolStorage.unclaimedPoolCollateral[collateralIndex] = poolStorage + .unclaimedPoolCollateral[collateralIndex] .sub(collateralAmount); - sendCollateral = true; + } - if (sendCollateral) { - IERC20(collateralAddress).transfer( - msg.sender, - collateralAmount - ); - } + // send out the tokens + if (sendCollateral) { + IERC20(poolStorage.collateralAddresses[collateralIndex]) + .safeTransfer(msg.sender, collateralAmount); } } - // ADMIN FUNCTIONS // + //========================= + // AMO minters functions + //========================= /** - * @notice Admin function for whitelisting a token as collateral - * @param collateralAddress Address of the token being whitelisted - * @param collateralMetaPool 3CRV Metapool for the token being whitelisted + * @notice Allows AMO minters to borrow collateral to make yield in external + * protocols like Compound, Curve, erc... + * @dev Bypasses the gassy mint->redeem cycle for AMOs to borrow collateral + * @param collateralAmount Amount of collateral to borrow */ - function addToken( - address collateralAddress, - IMetaPool collateralMetaPool - ) internal { - require( - collateralAddress != address(0x0) && - address(collateralMetaPool) != address(0x0), - "0 address detected" - ); + function amoMinterBorrow(uint256 collateralAmount) internal onlyAmoMinters { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - uint256 defaultDecimals = 18; - uint256 collateralDecimals = uint256( - IERC20Metadata(collateralAddress).decimals() + + // checks the collateral index of the minter as an additional safety check + uint256 minterCollateralIndex = IDollarAmoMinter(msg.sender) + .collateralIndex(); + + // checks to see if borrowing is paused + require( + poolStorage.borrowingPaused[minterCollateralIndex] == false, + "Borrowing is paused" ); - poolStorage.collateralAddresses.push(collateralAddress); - poolStorage.collateralMetaPools[collateralAddress] = collateralMetaPool; - poolStorage.missingDecimals[collateralAddress] = uint8( - defaultDecimals.sub(collateralDecimals) + // ensure collateral is enabled + require( + poolStorage.enabledCollaterals[ + poolStorage.collateralAddresses[minterCollateralIndex] + ], + "Collateral disabled" ); + + // transfer + IERC20(poolStorage.collateralAddresses[minterCollateralIndex]) + .safeTransfer(msg.sender, collateralAmount); } + //======================== + // Restricted functions + //======================== + /** - * @notice Returns the amount of collateral ready for collecting after redeeming - * @dev Redeem process is split in two steps: - * @dev 1. `redeemDollar()` - * @dev 2. `collectRedemption()` - * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param account Account address for which to check the balance ready to be collected - * @param collateralAddress Collateral token address - * @return Collateral token balance ready to be collected after redeeming + * @notice Adds a new AMO minter + * @param amoMinterAddress AMO minter address */ - function getRedeemCollateralBalances( - address account, - address collateralAddress - ) internal view returns (uint256) { - return - ubiquityPoolStorage().redeemCollateralBalances[account][ - collateralAddress - ]; + function addAmoMinter(address amoMinterAddress) internal { + require(amoMinterAddress != address(0), "Zero address detected"); + + // make sure the AMO Minter has collateralDollarBalance() + uint256 collatValE18 = IDollarAmoMinter(amoMinterAddress) + .collateralDollarBalance(); + require(collatValE18 >= 0, "Invalid AMO"); + + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.amoMinterAddresses[amoMinterAddress] = true; + + emit AmoMinterAdded(amoMinterAddress); } /** - * @notice Admin function to pause and unpause redemption for a specific collateral token - * @param collateralAddress Address of the token being affected - * @param notRedeemPaused True to turn on redemption for token, false to pause redemption of token + * @notice Adds a new collateral token + * @param collateralAddress Collateral token address + * @param poolCeiling Max amount of available tokens for collateral */ - function setRedeemActive( + function addCollateralToken( address collateralAddress, - bool notRedeemPaused + uint256 poolCeiling ) internal { - ubiquityPoolStorage().collateralRedeemActive[ + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + uint256 collateralIndex = poolStorage.collateralAddresses.length; + + // add collateral address to all collaterals + poolStorage.collateralAddresses.push(collateralAddress); + + // for fast collateral address -> collateral idx lookups later + poolStorage.collateralAddressToIndex[ collateralAddress - ] = notRedeemPaused; + ] = collateralIndex; + + // set collateral initially to disabled + poolStorage.enabledCollaterals[collateralAddress] = false; + + // add in the missing decimals + poolStorage.missingDecimals.push( + uint256(18).sub(ERC20(collateralAddress).decimals()) + ); + + // add in the collateral symbols + poolStorage.collateralSymbols.push(ERC20(collateralAddress).symbol()); + + // initialize unclaimed pool collateral + poolStorage.unclaimedPoolCollateral.push(0); + + // initialize paused prices to $1 as a backup + poolStorage.collateralPrices.push(UBIQUITY_POOL_PRICE_PRECISION); + + // set fees to 0 by default + poolStorage.mintingFee.push(0); + poolStorage.redemptionFee.push(0); + + // handle the pauses + poolStorage.mintPaused.push(false); + poolStorage.redeemPaused.push(false); + poolStorage.borrowingPaused.push(false); + + // pool ceiling + poolStorage.poolCeilings.push(poolCeiling); } /** - * @notice Checks whether redeem is enabled for the `collateralAddress` token - * @param collateralAddress Token address to check - * @return Whether redeem is enabled for the `collateralAddress` token + * @notice Removes AMO minter + * @param amoMinterAddress AMO minter address to remove */ - function getRedeemActive( - address collateralAddress - ) internal view returns (bool) { - return ubiquityPoolStorage().collateralRedeemActive[collateralAddress]; + function removeAmoMinter(address amoMinterAddress) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.amoMinterAddresses[amoMinterAddress] = false; + + emit AmoMinterRemoved(amoMinterAddress); } /** - * @notice Checks whether mint is enabled for the `collateralAddress` token - * @param collateralAddress Token address to check - * @return Whether mint is enabled for the `collateralAddress` token + * @notice Sets collateral token price in USD + * @param collateralIndex Collateral token index + * @param newPrice New USD price (precision 1e6) */ - function getMintActive( - address collateralAddress - ) internal view returns (bool) { - return ubiquityPoolStorage().collateralMintActive[collateralAddress]; + function setCollateralPrice( + uint256 collateralIndex, + uint256 newPrice + ) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + // Notice from Frax: CONSIDER ORACLES EVENTUALLY!!! + poolStorage.collateralPrices[collateralIndex] = newPrice; + + emit CollateralPriceSet(collateralIndex, newPrice); } /** - * @notice Admin function to pause and unpause minting for a specific collateral token - * @param collateralAddress Address of the token being affected - * @param notMintPaused True to turn on minting for token, false to pause minting for token + * @notice Sets mint and redeem fees, 1_000_000 = 100% + * @param collateralIndex Collateral token index + * @param newMintFee New mint fee + * @param newRedeemFee New redeem fee */ - function setMintActive( - address collateralAddress, - bool notMintPaused + function setFees( + uint256 collateralIndex, + uint256 newMintFee, + uint256 newRedeemFee ) internal { - ubiquityPoolStorage().collateralMintActive[ - collateralAddress - ] = notMintPaused; - } + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - // CHECK FUNCTIONS // + poolStorage.mintingFee[collateralIndex] = newMintFee; + poolStorage.redemptionFee[collateralIndex] = newRedeemFee; + + emit FeesSet(collateralIndex, newMintFee, newRedeemFee); + } /** - * @notice Checks whether `collateralAddress` token is approved by admin to be used as a collateral - * @param collateralAddress Token address - * @return isCollateral Whether token is approved to be used as a collateral + * @notice Sets max amount of collateral for a particular collateral token + * @param collateralIndex Collateral token index + * @param newCeiling Max amount of collateral */ - function checkCollateralToken( - address collateralAddress - ) internal view returns (bool isCollateral) { - address[] memory collateralAddresses_ = ubiquityPoolStorage() - .collateralAddresses; - for (uint256 i; i < collateralAddresses_.length; ++i) { - if (collateralAddress == collateralAddresses_[i]) { - isCollateral = true; - } - } - } + function setPoolCeiling( + uint256 collateralIndex, + uint256 newCeiling + ) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - // CALC FUNCTIONS // + poolStorage.poolCeilings[collateralIndex] = newCeiling; + + emit PoolCeilingSet(collateralIndex, newCeiling); + } /** - * @notice Returns the amount of dollars to mint - * @param collateralAmountD18 Amount of collateral tokens - * @param collateralPriceCurve3Pool USD price of a single collateral token - * @param curve3PriceUSD USD price from the Curve Tri-Pool (DAI, USDC, USDT) - * @return dollarOut Amount of Ubiquity Dollars to mint + * @notice Sets mint and redeem price thresholds, 1_000_000 = $1.00 + * @param newMintPriceThreshold New mint price threshold + * @param newRedeemPriceThreshold New redeem price threshold */ - function calcMintDollarAmount( - uint256 collateralAmountD18, - uint256 collateralPriceCurve3Pool, - uint256 curve3PriceUSD - ) internal pure returns (uint256 dollarOut) { - dollarOut = collateralAmountD18.mul(collateralPriceCurve3Pool).div( - curve3PriceUSD - ); + function setPriceThresholds( + uint256 newMintPriceThreshold, + uint256 newRedeemPriceThreshold + ) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.mintPriceThreshold = newMintPriceThreshold; + poolStorage.redeemPriceThreshold = newRedeemPriceThreshold; + + emit PriceThresholdsSet(newMintPriceThreshold, newRedeemPriceThreshold); } /** - * @notice Returns the amount of collateral tokens ready for collecting - * @dev Redeem process is split in two steps: + * @notice Sets a redemption delay in blocks + * @dev Redeeming is split in 2 actions: * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` - * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block - * @param dollarAmountD18 Amount of Ubiquity Dollars to redeem - * @param collateralPriceCurve3Pool USD price of a single collateral token - * @param curve3PriceUSD USD price from the Curve Tri-Pool (DAI, USDC, USDT) - * @return collateralOut Amount of collateral tokens ready to be collectable + * @dev `newRedemptionDelay` sets number of blocks that should be mined after which user can call `collectRedemption()` + * @param newRedemptionDelay Redemption delay in blocks */ - function calcRedeemCollateralAmount( - uint256 dollarAmountD18, - uint256 collateralPriceCurve3Pool, - uint256 curve3PriceUSD - ) internal pure returns (uint256 collateralOut) { - uint256 collateralPriceUSD = (collateralPriceCurve3Pool.mul(10e18)).div( - curve3PriceUSD - ); - collateralOut = (dollarAmountD18.mul(10e18)).div(collateralPriceUSD); - } + function setRedemptionDelay(uint256 newRedemptionDelay) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - /** - * @notice Returns Ubiquity Dollar token USD price from Metapool (Ubiquity Dollar, Curve Tri-Pool LP) - * @return dollarPriceUSD USD price of Ubiquity Dollar - */ - function getDollarPriceUsd() - internal - view - returns (uint256 dollarPriceUSD) - { - dollarPriceUSD = LibTWAPOracle.getTwapPrice(); + poolStorage.redemptionDelay = newRedemptionDelay; + + emit RedemptionDelaySet(newRedemptionDelay); } /** - * @notice Returns the latest price of the `collateralAddress` token from Curve Metapool - * @param collateralAddress Collateral token address - * @return collateralPriceCurve3Pool Collateral token price from Curve Metapool + * @notice Toggles (i.e. enables/disables) a particular collateral token + * @param collateralIndex Collateral token index */ - function getCollateralPriceCurve3Pool( - address collateralAddress - ) internal view returns (uint256 collateralPriceCurve3Pool) { - IMetaPool collateralMetaPool = ubiquityPoolStorage() - .collateralMetaPools[collateralAddress]; + function toggleCollateral(uint256 collateralIndex) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - collateralPriceCurve3Pool = collateralMetaPool - .get_price_cumulative_last()[0]; + address collateralAddress = poolStorage.collateralAddresses[ + collateralIndex + ]; + poolStorage.enabledCollaterals[collateralAddress] = !poolStorage + .enabledCollaterals[collateralAddress]; + + emit CollateralToggled( + collateralIndex, + poolStorage.enabledCollaterals[collateralAddress] + ); } /** - * @notice Returns USD price from Tri-Pool (DAI, USDC, USDT) - * @return curve3PriceUSD USD price + * @notice Toggles pause for mint/redeem/borrow methods + * @param collateralIndex Collateral token index + * @param toggleIndex Method index. 0 - toggle mint pause, 1 - toggle redeem pause, 2 - toggle borrow by AMO pause */ - function getCurve3PriceUSD() - internal - view - returns (uint256 curve3PriceUSD) - { - curve3PriceUSD = LibTWAPOracle.consult( - LibAppStorage.appStorage().curve3PoolTokenAddress - ); + function toggleMRB(uint256 collateralIndex, uint8 toggleIndex) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + if (toggleIndex == 0) + poolStorage.mintPaused[collateralIndex] = !poolStorage.mintPaused[ + collateralIndex + ]; + else if (toggleIndex == 1) + poolStorage.redeemPaused[collateralIndex] = !poolStorage + .redeemPaused[collateralIndex]; + else if (toggleIndex == 2) + poolStorage.borrowingPaused[collateralIndex] = !poolStorage + .borrowingPaused[collateralIndex]; + + emit MRBToggled(collateralIndex, toggleIndex); } } diff --git a/packages/contracts/src/dollar/upgradeInitializers/DiamondInit.sol b/packages/contracts/src/dollar/upgradeInitializers/DiamondInit.sol index e5820bfc7..2614821ab 100644 --- a/packages/contracts/src/dollar/upgradeInitializers/DiamondInit.sol +++ b/packages/contracts/src/dollar/upgradeInitializers/DiamondInit.sol @@ -101,11 +101,5 @@ contract DiamondInit is Modifiers { // These arguments are used to execute an arbitrary function using delegatecall // in order to set state variables in the diamond during deployment or an upgrade // More info here: https://eips.ethereum.org/EIPS/eip-2535#diamond-interface - - LibUbiquityPool.UbiquityPoolStorage storage poolStore = LibUbiquityPool - .ubiquityPoolStorage(); - poolStore.mintingFee = 0; - poolStore.redemptionFee = 0; - poolStore.dollarFloor = 1e18; } } diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index cf52ccd6c..4fe660631 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -1,343 +1,758 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import "../DiamondTestSetup.sol"; -import {IMetaPool} from "../../../src/dollar/interfaces/IMetaPool.sol"; -import {MockMetaPool} from "../../../src/dollar/mocks/MockMetaPool.sol"; +import "forge-std/console.sol"; +import {DiamondTestSetup} from "../DiamondTestSetup.sol"; +import {IDollarAmoMinter} from "../../../src/dollar/interfaces/IDollarAmoMinter.sol"; +import {LibUbiquityPool} from "../../../src/dollar/libraries/LibUbiquityPool.sol"; import {MockERC20} from "../../../src/dollar/mocks/MockERC20.sol"; -import {ICurveFactory} from "../../../src/dollar/interfaces/ICurveFactory.sol"; -import {MockCurveFactory} from "../../../src/dollar/mocks/MockCurveFactory.sol"; +import {MockMetaPool} from "../../../src/dollar/mocks/MockMetaPool.sol"; -import {IERC20Ubiquity} from "../../../src/dollar/interfaces/IERC20Ubiquity.sol"; -import {StakingShare} from "../../../src/dollar/core/StakingShare.sol"; -import {BondingShare} from "../../../src/dollar/mocks/MockShareV1.sol"; -import {DollarMintCalculatorFacet} from "../../../src/dollar/facets/DollarMintCalculatorFacet.sol"; -import {UbiquityCreditToken} from "../../../src/dollar/core/UbiquityCreditToken.sol"; +contract MockDollarAmoMinter is IDollarAmoMinter { + function collateralDollarBalance() external pure returns (uint256) { + return 0; + } + + function collateralIndex() external pure returns (uint256) { + return 0; + } +} contract UbiquityPoolFacetTest is DiamondTestSetup { - MockERC20 crvToken; - address curve3CrvToken; - address metaPoolAddress; - address twapOracleAddress; - - IMetaPool metapool; - address stakingMinAccount = address(0x9); - address stakingMaxAccount = address(0x10); - address secondAccount = address(0x4); - address thirdAccount = address(0x5); - address fourthAccount = address(0x6); - address fifthAccount = address(0x7); - address stakingZeroAccount = address(0x8); + MockDollarAmoMinter dollarAmoMinter; + MockERC20 collateralToken; + MockMetaPool curveDollarMetaPool; + MockERC20 curveTriPoolLpToken; + address user = address(1); + + // Events + event AmoMinterAdded(address amoMinterAddress); + event AmoMinterRemoved(address amoMinterAddress); + event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice); + event CollateralToggled(uint256 collateralIndex, bool newState); + event FeesSet( + uint256 collateralIndex, + uint256 newMintFee, + uint256 newRedeemFee + ); + event MRBToggled(uint256 collateralIndex, uint8 toggleIndex); + event PoolCeilingSet(uint256 collateralIndex, uint256 newCeiling); + event PriceThresholdsSet( + uint256 newMintPriceThreshold, + uint256 newRedeemPriceThreshold + ); + event RedemptionDelaySet(uint256 redemptionDelay); function setUp() public override { super.setUp(); - crvToken = new MockERC20("3 CRV", "3CRV", 18); - curve3CrvToken = address(crvToken); - metaPoolAddress = address( - new MockMetaPool(address(dollarToken), curve3CrvToken) - ); - - vm.startPrank(owner); - - twapOracleDollar3PoolFacet.setPool(metaPoolAddress, curve3CrvToken); - - address[7] memory mintings = [ - admin, - address(diamond), - owner, - fourthAccount, - stakingZeroAccount, - stakingMinAccount, - stakingMaxAccount - ]; - - for (uint256 i = 0; i < mintings.length; ++i) { - deal(address(dollarToken), mintings[i], 10000e18); - } - - address[5] memory crvDeal = [ - address(diamond), - owner, - stakingMaxAccount, - stakingMinAccount, - fourthAccount - ]; - vm.stopPrank(); - for (uint256 i; i < crvDeal.length; ++i) { - crvToken.mint(crvDeal[i], 10000e18); - } vm.startPrank(admin); - managerFacet.setStakingShareAddress(address(stakingShare)); - stakingShare.setApprovalForAll(address(diamond), true); - accessControlFacet.grantRole( - GOVERNANCE_TOKEN_MINTER_ROLE, - address(stakingShare) - ); - // vm.stopPrank(); - ICurveFactory curvePoolFactory = ICurveFactory(new MockCurveFactory()); - address curve3CrvBasePool = address( - new MockMetaPool(address(diamond), address(crvToken)) - ); - //vm.prank(admin); - managerFacet.deployStableSwapPool( - address(curvePoolFactory), - curve3CrvBasePool, - curve3CrvToken, - 10, - 50000000 - ); - // - metapool = IMetaPool(managerFacet.stableSwapMetaPoolAddress()); - metapool.transfer(address(stakingFacet), 100e18); - metapool.transfer(secondAccount, 1000e18); - vm.stopPrank(); - vm.prank(owner); - twapOracleDollar3PoolFacet.setPool(address(metapool), curve3CrvToken); - vm.startPrank(admin); + // init collateral token + collateralToken = new MockERC20("COLLATERAL", "CLT", 18); + + // init Curve 3CRV-LP token + curveTriPoolLpToken = new MockERC20("3CRV", "3CRV", 18); - accessControlFacet.grantRole(GOVERNANCE_TOKEN_MANAGER_ROLE, admin); - accessControlFacet.grantRole(CREDIT_NFT_MANAGER_ROLE, address(diamond)); - accessControlFacet.grantRole( - GOVERNANCE_TOKEN_MINTER_ROLE, - address(diamond) + // init Curve Dollar-3CRV LP metapool + curveDollarMetaPool = new MockMetaPool( + address(dollarToken), + address(curveTriPoolLpToken) ); - accessControlFacet.grantRole( - GOVERNANCE_TOKEN_BURNER_ROLE, - address(diamond) + // add collateral token to the pool + uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed + ubiquityPoolFacet.addCollateralToken( + address(collateralToken), + poolCeiling + ); + // enable collateral at index 0 + ubiquityPoolFacet.toggleCollateral(0); + // set mint and redeem fees + ubiquityPoolFacet.setFees( + 0, // collateral index + 10000, // 1% mint fee + 20000 // 2% redeem fee ); - managerFacet.setCreditTokenAddress(address(creditToken)); + // set redemption delay to 2 blocks + ubiquityPoolFacet.setRedemptionDelay(2); + // set mint price threshold to $1.01 and redeem price to $0.99 + ubiquityPoolFacet.setPriceThresholds(1010000, 990000); - vm.stopPrank(); + // init AMO minter + dollarAmoMinter = new MockDollarAmoMinter(); + // add AMO minter + ubiquityPoolFacet.addAmoMinter(address(dollarAmoMinter)); - vm.startPrank(stakingMinAccount); - dollarToken.approve(address(metapool), 10000e18); - crvToken.approve(address(metapool), 10000e18); + // stop being admin vm.stopPrank(); - vm.startPrank(stakingMaxAccount); - dollarToken.approve(address(metapool), 10000e18); - crvToken.approve(address(metapool), 10000e18); - vm.stopPrank(); - vm.startPrank(fourthAccount); - dollarToken.approve(address(metapool), 10000e18); - crvToken.approve(address(metapool), 10000e18); - vm.stopPrank(); + // set metapool for TWAP oracle + vm.prank(owner); + twapOracleDollar3PoolFacet.setPool( + address(curveDollarMetaPool), + address(curveTriPoolLpToken) + ); + + // mint 100 collateral tokens to the user + collateralToken.mint(address(user), 100e18); + // user approves the pool to transfer collateral + vm.prank(user); + collateralToken.approve(address(ubiquityPoolFacet), 100e18); + } + + //===================== + // Modifiers + //===================== - uint256[2] memory amounts_ = [uint256(100e18), uint256(100e18)]; + function testCollateralEnabled_ShouldRevert_IfCollateralIsDisabled() + public + { + // admin disables collateral + vm.prank(admin); + ubiquityPoolFacet.toggleCollateral(0); + + // user tries to mint Dollars + vm.prank(user); + vm.expectRevert("Collateral disabled"); + ubiquityPoolFacet.mintDollar(0, 1, 1, 1); + } - uint256 dyuAD2LP = metapool.calc_token_amount(amounts_, true); + function testOnlyAmoMinters_ShouldRevert_IfCalledNoByAmoMinter() public { + vm.prank(user); + vm.expectRevert("Not an AMO Minter"); + ubiquityPoolFacet.amoMinterBorrow(1); + } - vm.prank(stakingMinAccount); - metapool.add_liquidity( - amounts_, - (dyuAD2LP * 99) / 100, - stakingMinAccount + //===================== + // Views + //===================== + + function testAllCollaterals_ShouldReturnAllCollateralTokenAddresses() + public + { + address[] memory collateralAddresses = ubiquityPoolFacet + .allCollaterals(); + assertEq(collateralAddresses.length, 1); + assertEq(collateralAddresses[0], address(collateralToken)); + } + + function testCollateralInformation_ShouldRevert_IfCollateralIsDisabled() + public + { + // admin disables collateral + vm.prank(admin); + ubiquityPoolFacet.toggleCollateral(0); + + vm.expectRevert("Invalid collateral"); + ubiquityPoolFacet.collateralInformation(address(collateralToken)); + } + + function testCollateralInformation_ShouldReturnCollateralInformation() + public + { + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.index, 0); + assertEq(info.symbol, "CLT"); + assertEq(info.collateralAddress, address(collateralToken)); + assertEq(info.isEnabled, true); + assertEq(info.missingDecimals, 0); + assertEq(info.price, 1_000_000); + assertEq(info.poolCeiling, 50_000e18); + assertEq(info.mintPaused, false); + assertEq(info.redeemPaused, false); + assertEq(info.borrowingPaused, false); + assertEq(info.mintingFee, 10000); + assertEq(info.redemptionFee, 20000); + } + + function testCollateralUsdBalance_ShouldReturnTotalAmountOfCollateralInUsd() + public + { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold ); - vm.prank(stakingMaxAccount); - metapool.add_liquidity( - amounts_, - (dyuAD2LP * 99) / 100, - stakingMaxAccount + // user sends 100 collateral tokens and gets 99 Dollars + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send ); - vm.prank(fourthAccount); - metapool.add_liquidity(amounts_, (dyuAD2LP * 99) / 100, fourthAccount); + uint256 balanceTally = ubiquityPoolFacet.collateralUsdBalance(); + assertEq(balanceTally, 100e18); } - function test_setRedeemActiveShouldWorkIfAdmin() public { + function testFreeCollateralBalance_ShouldReturnCollateralAmountAvailableForBorrowingByAmoMinters() + public + { vm.prank(admin); - ubiquityPoolFacet.setRedeemActive(address(0x333), true); - assertEq(ubiquityPoolFacet.getRedeemActive(address(0x333)), true); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // user sends 100 collateral tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); + + // user redeems 99 Dollars for 97.02 (accounts for 2% redemption fee) collateral tokens + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 90e18 // min collateral out + ); + + uint256 freeCollateralAmount = ubiquityPoolFacet.freeCollateralBalance( + 0 + ); + assertEq(freeCollateralAmount, 2.98e18); + } + + function testGetDollarInCollateral_ShouldReturnAmountOfDollarsWhichShouldBeMintedForInputCollateral() + public + { + uint256 amount = ubiquityPoolFacet.getDollarInCollateral(0, 100e18); + assertEq(amount, 100e18); } - function test_setRedeemActiveShouldFailIfNotAdmin() public { - vm.expectRevert("Manager: Caller is not admin"); - ubiquityPoolFacet.setRedeemActive(address(0x333), true); + function testGetDollarPriceUsd_ShouldReturnDollarPriceInUsd() public { + uint256 dollarPriceUsd = ubiquityPoolFacet.getDollarPriceUsd(); + assertEq(dollarPriceUsd, 1_000_000); } - function test_setMintActiveShouldWorkIfAdmin() public { + //==================== + // Public functions + //==================== + + function testMintDollar_ShouldRevert_IfMintingIsPaused() public { + // admin pauses minting vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(0x333), true); - assertEq(ubiquityPoolFacet.getMintActive(address(0x333)), true); + ubiquityPoolFacet.toggleMRB(0, 0); + + vm.prank(user); + vm.expectRevert("Minting is paused"); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); } - function test_setMintActiveShouldFailIfNotAdmin() public { - vm.expectRevert("Manager: Caller is not admin"); - ubiquityPoolFacet.setMintActive(address(0x333), true); + function testMintDollar_ShouldRevert_IfDollarPriceUsdIsTooLow() public { + vm.prank(user); + vm.expectRevert("Dollar price too low"); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); } - function test_addTokenShouldWorkIfAdmin() public { + function testMintDollar_ShouldRevert_OnDollarAmountSlippage() public { vm.prank(admin); - ubiquityPoolFacet.addToken( - address(dollarToken), - IMetaPool(metaPoolAddress) + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + vm.prank(user); + vm.expectRevert("Dollar slippage"); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 100e18, // min amount of Dollars to mint + 100e18 // max collateral to send ); } - function test_addTokenWithZeroAddressFail() public { - vm.startPrank(admin); - vm.expectRevert("0 address detected"); - ubiquityPoolFacet.addToken(address(0), IMetaPool(metaPoolAddress)); - vm.expectRevert("0 address detected"); - ubiquityPoolFacet.addToken(address(dollarToken), IMetaPool(address(0))); - vm.stopPrank(); + function testMintDollar_ShouldRevert_OnCollateralAmountSlippage() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + vm.prank(user); + vm.expectRevert("Collateral slippage"); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18, // min amount of Dollars to mint + 10e18 // max collateral to send + ); } - function test_addTokenShouldFailIfNotAdmin() public { - vm.expectRevert("Manager: Caller is not admin"); - ubiquityPoolFacet.addToken( - address(dollarToken), - IMetaPool(address(0x444)) + function testMintDollar_ShouldRevert_OnReachingPoolCeiling() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + vm.prank(user); + vm.expectRevert("Pool ceiling"); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 60_000e18, // Dollar amount + 59_000e18, // min amount of Dollars to mint + 60_000e18 // max collateral to send ); } - function test_mintDollarShouldFailWhenSlippageIsReached() public { - MockERC20 collateral = new MockERC20("collateral", "collateral", 18); - collateral.mint(fourthAccount, 10 ether); + function testMintDollar_ShouldMintDollars() public { vm.prank(admin); - ubiquityPoolFacet.addToken(address(collateral), (metapool)); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + // balances before + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(dollarToken.balanceOf(user), 0); + + vm.prank(user); + (uint256 totalDollarMint, uint256 collateralNeeded) = ubiquityPoolFacet + .mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); + assertEq(totalDollarMint, 99e18); + assertEq(collateralNeeded, 100e18); + + // balances after + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); + assertEq(dollarToken.balanceOf(user), 99e18); + } + + function testRedeemDollar_ShouldRevert_IfRedeemingIsPaused() public { + // admin pauses redeeming vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(collateral), true); - vm.startPrank(fourthAccount); - collateral.approve(address(ubiquityPoolFacet), type(uint256).max); - vm.expectRevert("Slippage limit reached"); - ubiquityPoolFacet.mintDollar( - address(collateral), - 10 ether, - 10000 ether + ubiquityPoolFacet.toggleMRB(0, 1); + + vm.prank(user); + vm.expectRevert("Redeeming is paused"); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18 // min collateral out + ); + } + + function testRedeemDollar_ShouldRevert_IfDollarPriceUsdIsTooHigh() public { + vm.prank(user); + vm.expectRevert("Dollar price too high"); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18 // min collateral out ); - vm.stopPrank(); } - function test_mintDollarShouldWork() public { - MockERC20 collateral = new MockERC20("collateral", "collateral", 18); - collateral.mint(fourthAccount, 10 ether); + function testRedeemDollar_ShouldRevert_OnInsufficientPoolCollateral() + public + { vm.prank(admin); - ubiquityPoolFacet.addToken(address(collateral), (metapool)); - assertEq(collateral.balanceOf(fourthAccount), 10 ether); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + vm.prank(user); + vm.expectRevert("Insufficient pool collateral"); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 100e18, // Dollar amount + 90e18 // min collateral out + ); + } + + function testRedeemDollar_ShouldRevert_OnCollateralSlippage() public { vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(collateral), true); - vm.startPrank(fourthAccount); - collateral.approve(address(ubiquityPoolFacet), type(uint256).max); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); - uint256 balanceBefore = dollarToken.balanceOf(fourthAccount); - ubiquityPoolFacet.mintDollar(address(collateral), 1 ether, 0 ether); - assertGt(dollarToken.balanceOf(fourthAccount), balanceBefore); - vm.stopPrank(); + // user sends 100 collateral tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); + + vm.prank(user); + vm.expectRevert("Collateral slippage"); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 100e18, // Dollar amount + 100e18 // min collateral out + ); } - function test_redeemDollarShouldFailWhenDollarIAboveOne() public { - MockERC20 collateral = new MockERC20("collateral", "collateral", 18); - collateral.mint(fourthAccount, 10 ether); + function testRedeemDollar_ShouldRedeemCollateral() public { vm.prank(admin); - ubiquityPoolFacet.addToken(address(collateral), (metapool)); - assertEq(collateral.balanceOf(fourthAccount), 10 ether); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // user sends 100 collateral tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); + + // balances before + assertEq(dollarToken.balanceOf(user), 99e18); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 90e18 // min collateral out + ); + + // balances after + assertEq(dollarToken.balanceOf(user), 0); + } + + function testCollectRedemption_ShouldRevert_IfRedeemingIsPaused() public { + // admin pauses redeeming vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(collateral), true); - vm.startPrank(fourthAccount); - collateral.approve(address(ubiquityPoolFacet), type(uint256).max); + ubiquityPoolFacet.toggleMRB(0, 1); - uint256 balanceBefore = dollarToken.balanceOf(fourthAccount); - ubiquityPoolFacet.mintDollar(address(collateral), 1 ether, 0 ether); - assertGt(dollarToken.balanceOf(fourthAccount), balanceBefore); - vm.stopPrank(); + vm.prank(user); + vm.expectRevert("Redeeming is paused"); + ubiquityPoolFacet.collectRedemption(0); + } + function testCollectRedemption_ShouldRevert_IfNotEnoughBlocksHaveBeenMined() + public + { + vm.prank(user); + vm.expectRevert("Too soon to collect redemption"); + ubiquityPoolFacet.collectRedemption(0); + } + + function testCollectRedemption_ShouldCollectRedemption() public { vm.prank(admin); - ubiquityPoolFacet.setRedeemActive(address(collateral), true); - vm.startPrank(fourthAccount); - vm.expectRevert( - "Ubiquity Dollar Token value must be less than 1 USD to redeem" + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold ); - ubiquityPoolFacet.redeemDollar(address(collateral), 1 ether, 0 ether); - vm.stopPrank(); + + // user sends 100 collateral tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18 // max collateral to send + ); + + // user redeems 99 Dollars for collateral + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 90e18 // min collateral out + ); + + // wait 3 blocks for collecting redemption to become active + vm.roll(3); + + // balances before + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); + assertEq(collateralToken.balanceOf(user), 0); + + vm.prank(user); + uint256 collateralAmount = ubiquityPoolFacet.collectRedemption(0); + assertEq(collateralAmount, 97.02e18); // $99 - 2% redemption fee + + // balances after + assertEq( + collateralToken.balanceOf(address(ubiquityPoolFacet)), + 2.98e18 + ); + assertEq(collateralToken.balanceOf(user), 97.02e18); } - function test_redeemDollarShouldWork() public { - MockERC20 collateral = new MockERC20("collateral", "collateral", 18); - collateral.mint(fourthAccount, 10 ether); + //========================= + // AMO minters functions + //========================= + + function testAmoMinterBorrow_ShouldRevert_IfBorrowingIsPaused() public { + // admin pauses borrowing by AMOs vm.prank(admin); - ubiquityPoolFacet.addToken(address(collateral), (metapool)); - assertEq(collateral.balanceOf(fourthAccount), 10 ether); + ubiquityPoolFacet.toggleMRB(0, 2); + + // Dollar AMO minter tries to borrow collateral + vm.prank(address(dollarAmoMinter)); + vm.expectRevert("Borrowing is paused"); + ubiquityPoolFacet.amoMinterBorrow(1); + } + + function testAmoMinterBorrow_ShouldRevert_IfCollateralIsDisabled() public { + // admin disables collateral vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(collateral), true); - vm.startPrank(fourthAccount); - collateral.approve(address(ubiquityPoolFacet), type(uint256).max); + ubiquityPoolFacet.toggleCollateral(0); + + // Dollar AMO minter tries to borrow collateral + vm.prank(address(dollarAmoMinter)); + vm.expectRevert("Collateral disabled"); + ubiquityPoolFacet.amoMinterBorrow(1); + } + + function testAmoMinterBorrow_ShouldBorrowCollateral() public { + // mint 100 collateral tokens to the pool + collateralToken.mint(address(ubiquityPoolFacet), 100e18); + + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); + assertEq(collateralToken.balanceOf(address(dollarAmoMinter)), 0); + + vm.prank(address(dollarAmoMinter)); + ubiquityPoolFacet.amoMinterBorrow(100e18); + + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(collateralToken.balanceOf(address(dollarAmoMinter)), 100e18); + } + + //======================== + // Restricted functions + //======================== + + function testAddAmoMinter_ShouldRevert_IfAmoMinterIsZeroAddress() public { + vm.startPrank(admin); + + vm.expectRevert("Zero address detected"); + ubiquityPoolFacet.addAmoMinter(address(0)); - ubiquityPoolFacet.mintDollar(address(collateral), 10 ether, 0 ether); - uint256 balanceBefore = dollarToken.balanceOf(fourthAccount); vm.stopPrank(); - MockMetaPool mock = MockMetaPool( - managerFacet.stableSwapMetaPoolAddress() - ); - // set the mock data for meta pool - uint256[2] memory _price_cumulative_last = [ - uint256(100e18), - uint256(42e16) - ]; - uint256 _last_block_timestamp = 120000; - uint256[2] memory _twap_balances = [uint256(100e18), uint256(42e16)]; - uint256[2] memory _dy_values = [uint256(100e18), uint256(42e16)]; - mock.updateMockParams( - _price_cumulative_last, - _last_block_timestamp, - _twap_balances, - _dy_values - ); - twapOracleDollar3PoolFacet.update(); - vm.prank(admin); - ubiquityPoolFacet.setRedeemActive(address(collateral), true); - vm.startPrank(fourthAccount); - ubiquityPoolFacet.redeemDollar(address(collateral), 1 ether, 0 ether); + } + + function testAddAmoMinter_ShouldRevert_IfAmoMinterHasInvalidInterface() + public + { + vm.startPrank(admin); + + vm.expectRevert(); + ubiquityPoolFacet.addAmoMinter(address(1)); - assertLt(dollarToken.balanceOf(fourthAccount), balanceBefore); vm.stopPrank(); } - function test_collectRedemptionShouldWork() public { - MockERC20 collateral = new MockERC20("collateral", "collateral", 18); - collateral.mint(fourthAccount, 10 ether); - vm.prank(admin); - ubiquityPoolFacet.addToken(address(collateral), (metapool)); - assertEq(collateral.balanceOf(fourthAccount), 10 ether); - vm.prank(admin); - ubiquityPoolFacet.setMintActive(address(collateral), true); - vm.startPrank(fourthAccount); - collateral.approve(address(ubiquityPoolFacet), type(uint256).max); + function testAddAmoMinter_ShouldAddAmoMinter() public { + vm.startPrank(admin); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit AmoMinterAdded(address(dollarAmoMinter)); + ubiquityPoolFacet.addAmoMinter(address(dollarAmoMinter)); - ubiquityPoolFacet.mintDollar(address(collateral), 10 ether, 0 ether); - uint256 balanceBefore = dollarToken.balanceOf(fourthAccount); - uint256 balanceCollateralBefore = collateral.balanceOf(fourthAccount); vm.stopPrank(); - MockMetaPool mock = MockMetaPool( - managerFacet.stableSwapMetaPoolAddress() - ); - // set the mock data for meta pool - uint256[2] memory _price_cumulative_last = [ - uint256(100e18), - uint256(42e16) - ]; - uint256 _last_block_timestamp = 120000; - uint256[2] memory _twap_balances = [uint256(100e18), uint256(42e16)]; - uint256[2] memory _dy_values = [uint256(100e18), uint256(42e16)]; - mock.updateMockParams( - _price_cumulative_last, - _last_block_timestamp, - _twap_balances, - _dy_values - ); - twapOracleDollar3PoolFacet.update(); - vm.prank(admin); - ubiquityPoolFacet.setRedeemActive(address(collateral), true); - vm.startPrank(fourthAccount); - ubiquityPoolFacet.redeemDollar(address(collateral), 1 ether, 0 ether); + } + + function testAddCollateralToken_ShouldAddNewTokenAsCollateral() public { + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.index, 0); + assertEq(info.symbol, "CLT"); + assertEq(info.collateralAddress, address(collateralToken)); + assertEq(info.isEnabled, true); + assertEq(info.missingDecimals, 0); + assertEq(info.price, 1_000_000); + assertEq(info.poolCeiling, 50_000e18); + assertEq(info.mintPaused, false); + assertEq(info.redeemPaused, false); + assertEq(info.borrowingPaused, false); + assertEq(info.mintingFee, 10000); + assertEq(info.redemptionFee, 20000); + } + + function testRemoveAmoMinter_ShouldRemoveAmoMinter() public { + vm.startPrank(admin); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit AmoMinterRemoved(address(dollarAmoMinter)); + ubiquityPoolFacet.removeAmoMinter(address(dollarAmoMinter)); + + vm.stopPrank(); + } + + function testSetCollateralPrice_ShouldSetCollateralPriceInUsd() public { + vm.startPrank(admin); + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.price, 1_000_000); + + uint256 newCollateralPrice = 1_100_000; + vm.expectEmit(address(ubiquityPoolFacet)); + emit CollateralPriceSet(0, newCollateralPrice); + ubiquityPoolFacet.setCollateralPrice(0, newCollateralPrice); + + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq(info.price, newCollateralPrice); + + vm.stopPrank(); + } + + function testSetFees_ShouldSetMintAndRedeemFees() public { + vm.startPrank(admin); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit FeesSet(0, 1, 2); + ubiquityPoolFacet.setFees(0, 1, 2); + + vm.stopPrank(); + } + + function testSetPoolCeiling_ShouldSetMaxAmountOfTokensAllowedForCollateral() + public + { + vm.startPrank(admin); + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.poolCeiling, 50_000e18); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit PoolCeilingSet(0, 10_000e18); + ubiquityPoolFacet.setPoolCeiling(0, 10_000e18); + + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq(info.poolCeiling, 10_000e18); + + vm.stopPrank(); + } + + function testSetPriceThresholds_ShouldSetPriceThresholds() public { + vm.startPrank(admin); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit PriceThresholdsSet(1010000, 990000); + ubiquityPoolFacet.setPriceThresholds(1010000, 990000); + + vm.stopPrank(); + } + + function testSetRedemptionDelay_ShouldSetRedemptionDelayInBlocks() public { + vm.startPrank(admin); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit RedemptionDelaySet(2); + ubiquityPoolFacet.setRedemptionDelay(2); + + vm.stopPrank(); + } + + function testToggleCollateral_ShouldToggleCollateral() public { + vm.startPrank(admin); + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.isEnabled, true); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit CollateralToggled(0, false); + ubiquityPoolFacet.toggleCollateral(0); + + vm.expectRevert("Invalid collateral"); + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + + vm.stopPrank(); + } + + function testToggleMRB_ShouldToggleMinting() public { + vm.startPrank(admin); + + uint256 collateralIndex = 0; + uint8 toggleIndex = 0; + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.mintPaused, false); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit MRBToggled(collateralIndex, toggleIndex); + ubiquityPoolFacet.toggleMRB(collateralIndex, toggleIndex); + + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq(info.mintPaused, true); + + vm.stopPrank(); + } + + function testToggleMRB_ShouldToggleRedeeming() public { + vm.startPrank(admin); + + uint256 collateralIndex = 0; + uint8 toggleIndex = 1; + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.redeemPaused, false); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit MRBToggled(collateralIndex, toggleIndex); + ubiquityPoolFacet.toggleMRB(collateralIndex, toggleIndex); + + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq(info.redeemPaused, true); + + vm.stopPrank(); + } + + function testToggleMRB_ShouldToggleBorrowingByAmoMinter() public { + vm.startPrank(admin); + + uint256 collateralIndex = 0; + uint8 toggleIndex = 2; + + LibUbiquityPool.CollateralInformation memory info = ubiquityPoolFacet + .collateralInformation(address(collateralToken)); + assertEq(info.borrowingPaused, false); + + vm.expectEmit(address(ubiquityPoolFacet)); + emit MRBToggled(collateralIndex, toggleIndex); + ubiquityPoolFacet.toggleMRB(collateralIndex, toggleIndex); + + info = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq(info.borrowingPaused, true); - assertLt(dollarToken.balanceOf(fourthAccount), balanceBefore); - ubiquityPoolFacet.collectRedemption(address(collateral)); - assertGt(collateral.balanceOf(fourthAccount), balanceCollateralBefore); vm.stopPrank(); } }