diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index 3e4a1121e7..af2eac6a2e 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -22,12 +22,16 @@ abstract contract OracleRouterBase is IOracle { * @param asset address of the asset * @return uint256 USD price of 1 of the asset, in 8 decimal fixed */ - function price(address asset) external view override returns (uint256) { + function price(address asset) + external + view + virtual + override + returns (uint256) + { address _feed = feed(asset); - if (_feed == FIXED_PRICE) { - return 1e8; - } require(_feed != address(0), "Asset not available"); + require(_feed != FIXED_PRICE, "Fixed price feeds not supported"); (, int256 _iprice, , , ) = AggregatorV3Interface(_feed) .latestRoundData(); uint256 _price = uint256(_iprice); @@ -54,50 +58,74 @@ contract OracleRouter is OracleRouterBase { * @param asset address of the asset */ function feed(address asset) internal pure override returns (address) { - if (asset == address(0x6B175474E89094C44Da98b954EedeAC495271d0F)) { + if (asset == 0x6B175474E89094C44Da98b954EedeAC495271d0F) { // Chainlink: DAI/USD - return address(0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9); - } else if ( - asset == address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) - ) { + return 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; + } else if (asset == 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) { // Chainlink: USDC/USD - return address(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); - } else if ( - asset == address(0xdAC17F958D2ee523a2206206994597C13D831ec7) - ) { + return 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; + } else if (asset == 0xdAC17F958D2ee523a2206206994597C13D831ec7) { // Chainlink: USDT/USD - return address(0x3E7d1eAB13ad0104d2750B8863b489D65364e32D); - } else if ( - asset == address(0xc00e94Cb662C3520282E6f5717214004A7f26888) - ) { + return 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D; + } else if (asset == 0xc00e94Cb662C3520282E6f5717214004A7f26888) { // Chainlink: COMP/USD - return address(0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5); - } else if ( - asset == address(0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9) - ) { + return 0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5; + } else if (asset == 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9) { // Chainlink: AAVE/USD - return address(0x547a514d5e3769680Ce22B2361c10Ea13619e8a9); - } else if ( - asset == address(0xD533a949740bb3306d119CC777fa900bA034cd52) - ) { + return 0x547a514d5e3769680Ce22B2361c10Ea13619e8a9; + } else if (asset == 0xD533a949740bb3306d119CC777fa900bA034cd52) { // Chainlink: CRV/USD - return address(0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f); - } else if ( - asset == address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B) - ) { + return 0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f; + } else if (asset == 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B) { // Chainlink: CVX/USD - return address(0xd962fC30A72A84cE50161031391756Bf2876Af5D); - } else if ( - asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) - ) { + return 0xd962fC30A72A84cE50161031391756Bf2876Af5D; + } else if (asset == 0xae78736Cd615f374D3085123A210448E74Fc6393) { + // Chainlink: rETH/ETH + return 0x536218f9E9Eb48863970252233c8F271f554C2d0; + } else if (asset == 0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) { + // Chainlink: cbETH/ETH + return 0xF017fcB346A1885194689bA23Eff2fE6fA5C483b; + } else if (asset == 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) { + // Chainlink: stETH/ETH + return 0x86392dC19c0b719886221c78AB11eb8Cf5c52812; + } else if (asset == 0x5E8422345238F34275888049021821E8E08CAa1f) { // FIXED_PRICE: frxETH/ETH - return address(FIXED_PRICE); + return FIXED_PRICE; + } else if (asset == 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) { + // FIXED_PRICE: WETH/ETH + return FIXED_PRICE; } else { revert("Asset not available"); } } } +contract OETHOracleRouter is OracleRouter { + /** + * @notice Returns the total price in 8 digit USD for a given asset. + * This implementation does not (!) do range checks as the + * parent OracleRouter does. + * @param asset address of the asset + * @return uint256 USD price of 1 of the asset, in 8 decimal fixed + */ + function price(address asset) + external + view + virtual + override + returns (uint256) + { + address _feed = feed(asset); + if (_feed == FIXED_PRICE) { + return 1e8; + } + require(_feed != address(0), "Asset not available"); + (, int256 _iprice, , , ) = AggregatorV3Interface(_feed) + .latestRoundData(); + return uint256(_iprice); + } +} + contract OracleRouterDev is OracleRouterBase { mapping(address => address) public assetToFeed; diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 9b671a06a0..0bd97b753d 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -448,44 +448,6 @@ contract VaultAdmin is VaultStorage { IERC20(_asset).safeTransfer(governor(), _amount); } - /*************************************** - Pricing - ****************************************/ - - /** - * @dev Returns the total price in 18 digit USD for a given asset. - * Never goes above 1, since that is how we price mints - * @param asset address of the asset - * @return uint256 USD price of 1 of the asset, in 18 decimal fixed - */ - function priceUSDMint(address asset) external view returns (uint256) { - uint256 price = IOracle(priceProvider).price(asset); - require( - price >= uint256(MINT_MINIMUM_UNIT_PRICE).scaleBy(8, 18), - "Asset price below peg" - ); - if (price > 1e8) { - price = 1e8; - } - // Price from Oracle is returned with 8 decimals so scale to 18 - return price.scaleBy(18, 8); - } - - /** - * @dev Returns the total price in 18 digit USD for a given asset. - * Never goes below 1, since that is how we price redeems - * @param asset Address of the asset - * @return uint256 USD price of 1 of the asset, in 18 decimal fixed - */ - function priceUSDRedeem(address asset) external view returns (uint256) { - uint256 price = IOracle(priceProvider).price(asset); - if (price < 1e8) { - price = 1e8; - } - // Price from Oracle is returned with 8 decimals so scale to 18 - return price.scaleBy(18, 8); - } - /*************************************** Strategies Admin ****************************************/ diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 66fc4e0481..e846c1946f 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -16,8 +16,8 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import { StableMath } from "../utils/StableMath.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; import { IVault } from "../interfaces/IVault.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; import { IBuyback } from "../interfaces/IBuyback.sol"; import { IBasicToken } from "../interfaces/IBasicToken.sol"; import { IGetExchangeRateToken } from "../interfaces/IGetExchangeRateToken.sol"; @@ -72,12 +72,7 @@ contract VaultCore is VaultStorage { require(_amount > 0, "Amount must be greater than 0"); uint256 units = _toUnits(_amount, _asset); - uint256 price = IOracle(priceProvider).price(_asset) * 1e10; - uint256 unitPrice = _toUnitPrice(price, _asset); - if (unitPrice > 1e18) { - unitPrice = 1e18; - } - require(unitPrice >= MINT_MINIMUM_UNIT_PRICE, "Asset price below peg"); + uint256 unitPrice = _toUnitPrice(_asset, true); uint256 priceAdjustedDeposit = (units * unitPrice) / 1e18; if (_minimumOusdAmount > 0) { @@ -575,13 +570,7 @@ contract VaultCore is VaultStorage { // Calculate totalOutputRatio uint256 totalOutputRatio = 0; for (uint256 i = 0; i < assetCount; i++) { - uint256 price = IOracle(priceProvider).price(allAssets[i]) * 1e10; - uint256 unitPrice = _toUnitPrice(price, allAssets[i]); - // Never give out more than one - // base token per unit of OUSD - if (unitPrice < 1e18) { - unitPrice = 1e18; - } + uint256 unitPrice = _toUnitPrice(allAssets[i], false); uint256 ratio = assetUnits[i].mul(unitPrice).div(totalUnits); totalOutputRatio = totalOutputRatio.add(ratio); } @@ -593,40 +582,56 @@ contract VaultCore is VaultStorage { } /*************************************** - Utils + Pricing ****************************************/ /** - * @dev Return the number of assets supported by the Vault. - */ - function getAssetCount() public view returns (uint256) { - return allAssets.length; - } - - /** - * @dev Return all asset addresses in order + * @dev Returns the total price in 18 digit units for a given asset. + * Never goes above 1, since that is how we price mints. + * @param asset address of the asset + * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed */ - function getAllAssets() external view returns (address[] memory) { - return allAssets; - } - - /** - * @dev Return the number of strategies active on the Vault. - */ - function getStrategyCount() external view returns (uint256) { - return allStrategies.length; + function priceUnitMint(address asset) + external + view + returns (uint256 price) + { + /* need to supply 1 asset unit in asset's decimals and can not just hard-code + * to 1e18 and ignore calling `_toUnits` since we need to consider assets + * with the exchange rate + */ + uint256 units = _toUnits( + uint256(1e18).scaleBy(_getDecimals(asset), 18), + asset + ); + price = _toUnitPrice(asset, true) * units; } /** - * @dev Return the array of all strategies + * @dev Returns the total price in 18 digit unit for a given asset. + * Never goes below 1, since that is how we price redeems + * @param asset Address of the asset + * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed */ - function getAllStrategies() external view returns (address[] memory) { - return allStrategies; + function priceUnitRedeem(address asset) + external + view + returns (uint256 price) + { + /* need to supply 1 asset unit in asset's decimals and can not just hard-code + * to 1e18 and ignore calling `_toUnits` since we need to consider assets + * with the exchange rate + */ + uint256 units = _toUnits( + uint256(1e18).scaleBy(_getDecimals(asset), 18), + asset + ); + price = _toUnitPrice(asset, false) * units; } - function isSupportedAsset(address _asset) external view returns (bool) { - return assets[_asset].isSupported; - } + /*************************************** + Utils + ****************************************/ /** * @dev Convert a quantity of a token into 1e18 fixed decimal "units" @@ -660,21 +665,59 @@ contract VaultCore is VaultStorage { } } - function _toUnitPrice(uint256 _price, address _asset) + /** + * @dev Returns asset's unit price accounting for different asset types + * and takes into account the context in which that price exists - + * - mint or redeem. + * + * Note: since we are returning the price of the unit and not the one of the + * asset (see comment above how 1 rETH exchanges for 1.2 units) we need + * to make the Oracle price adjustment as well since we are pricing the + * units and not the assets. + * + * The price also snaps to a "full unit price" in case a mint or redeem + * action would be unfavourable to the protocol. + * + */ + function _toUnitPrice(address _asset, bool isMint) internal view - returns (uint256) + returns (uint256 price) { UnitConversion conversion = assets[_asset].unitConversion; - if (conversion == UnitConversion.DECIMALS) { - return _price; - } else if (conversion == UnitConversion.GETEXCHANGERATE) { + price = IOracle(priceProvider).price(_asset) * 1e10; + + if (conversion == UnitConversion.GETEXCHANGERATE) { uint256 exchangeRate = IGetExchangeRateToken(_asset) .getExchangeRate(); - return (_price * 1e18) / exchangeRate; - } else { + price = (price * 1e18) / exchangeRate; + } else if (conversion != UnitConversion.DECIMALS) { require(false, "Unsupported conversion type"); } + + /* At this stage the price is already adjusted to the unit + * so the price checks are agnostic to underlying asset being + * pegged to a USD or to an ETH or having a custom exchange rate. + */ + require(price <= MAX_UNIT_PRICE_DRIFT, "Vault: Price exceeds max"); + require(price >= MIN_UNIT_PRICE_DRIFT, "Vault: Price under min"); + + if (isMint) { + /* Never price a normalized unit price for more than one + * unit of OETH/OUSD when minting. + */ + if (price > 1e18) { + price = 1e18; + } + require(price >= MINT_MINIMUM_ORACLE, "Asset price below peg"); + } else { + /* Never give out more than 1 normalized unit amount of assets + * for one unit of OETH/OUSD when redeeming. + */ + if (price < 1e18) { + price = 1e18; + } + } } function _getDecimals(address _asset) internal view returns (uint256) { @@ -683,6 +726,38 @@ contract VaultCore is VaultStorage { return decimals; } + /** + * @dev Return the number of assets supported by the Vault. + */ + function getAssetCount() public view returns (uint256) { + return allAssets.length; + } + + /** + * @dev Return all asset addresses in order + */ + function getAllAssets() external view returns (address[] memory) { + return allAssets; + } + + /** + * @dev Return the number of strategies active on the Vault. + */ + function getStrategyCount() external view returns (uint256) { + return allStrategies.length; + } + + /** + * @dev Return the array of all strategies + */ + function getAllStrategies() external view returns (address[] memory) { + return allStrategies; + } + + function isSupportedAsset(address _asset) external view returns (bool) { + return assets[_asset].isSupported; + } + /** * @dev Falldown to the admin implementation * @notice This is a catch all for all functions not declared in core diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index c86ea339b6..e0e490aaf6 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -128,6 +128,10 @@ contract VaultStorage is Initializable, Governable { // Cheaper to read decimals locally than to call out each time mapping(address => uint256) internal decimalsCache; // TODO: Move to Asset struct + uint256 constant MIN_UNIT_PRICE_DRIFT = 0.7e18; + uint256 constant MAX_UNIT_PRICE_DRIFT = 1.3e18; + uint256 constant MINT_MINIMUM_ORACLE = 99800000; + /** * @dev set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/deploy/051_oeth.js b/contracts/deploy/051_oeth.js index 25d2e20e3c..a2e460a3a1 100644 --- a/contracts/deploy/051_oeth.js +++ b/contracts/deploy/051_oeth.js @@ -63,7 +63,7 @@ const deployCore = async ({ // Proxies await deployWithConfirmation("OETHVaultProxy"); - await deployWithConfirmation("OracleRouter"); + await deployWithConfirmation("OETHOracleRouter"); // Main contracts const dOETH = await deployWithConfirmation("OETH"); @@ -80,7 +80,7 @@ const deployCore = async ({ const cOETHProxy = await ethers.getContract("OETHProxy"); const cVaultProxy = await ethers.getContract("OETHVaultProxy"); const cOETH = await ethers.getContractAt("OETH", cOETHProxy.address); - const cOracleRouter = await ethers.getContract("OracleRouter"); + const cOETHOracleRouter = await ethers.getContract("OETHOracleRouter"); const cVault = await ethers.getContractAt("OETHVault", cVaultProxy.address); // Need to call the initializer on the Vault then upgraded it to the actual @@ -95,7 +95,7 @@ const deployCore = async ({ await withConfirmation( cVault .connect(sDeployer) - .initialize(cOracleRouter.address, cOETHProxy.address) + .initialize(cOETHOracleRouter.address, cOETHProxy.address) ); console.log("Initialized OETHVault");