From 9690335bf10903bc269686bcde73553758337f30 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 11:42:14 +0200 Subject: [PATCH 01/15] start oracle separation --- contracts/contracts/oracle/OracleRouter.sol | 28 +++++++++------ contracts/contracts/vault/VaultAdmin.sol | 7 ++-- contracts/contracts/vault/VaultCore.sol | 5 ++- contracts/contracts/vault/VaultStorage.sol | 38 +++++++++++++++++++++ contracts/deploy/051_oeth.js | 6 ++-- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index ca2df3345b..1071f63781 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -8,7 +8,6 @@ import { Helpers } from "../utils/Helpers.sol"; abstract contract OracleRouterBase is IOracle { uint256 constant MIN_DRIFT = uint256(70000000); uint256 constant MAX_DRIFT = uint256(130000000); - address constant FIXED_PRICE = 0x0000000000000000000000000000000000000001; /** * @dev The price feed contract to use for a particular asset. @@ -22,11 +21,8 @@ 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"); (, int256 _iprice, , , ) = AggregatorV3Interface(_feed) .latestRoundData(); @@ -87,17 +83,29 @@ contract OracleRouter is OracleRouterBase { ) { // Chainlink: CVX/USD return address(0xd962fC30A72A84cE50161031391756Bf2876Af5D); - } else if ( - asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) - ) { - // FIXED_PRICE: frxETH/ETH - return address(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); + 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 2d14088b2f..c1fc279bed 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -10,7 +10,6 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { StableMath } from "../utils/StableMath.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; import "./VaultStorage.sol"; contract VaultAdmin is VaultStorage { @@ -181,7 +180,7 @@ contract VaultAdmin is VaultStorage { // Verify that our oracle supports the asset // slither-disable-next-line unused-return - IOracle(priceProvider).price(_asset); + oraclePrice(_asset); emit AssetSupported(_asset); } @@ -459,7 +458,7 @@ contract VaultAdmin is VaultStorage { * @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); + uint256 price = oraclePrice(asset); require(price >= MINT_MINIMUM_ORACLE, "Asset price below peg"); if (price > 1e8) { price = 1e8; @@ -475,7 +474,7 @@ contract VaultAdmin is VaultStorage { * @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); + uint256 price = oraclePrice(asset); if (price < 1e8) { price = 1e8; } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 73c09a07b7..d1718a547d 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -16,7 +16,6 @@ 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 { IBuyback } from "../interfaces/IBuyback.sol"; import { IBasicToken } from "../interfaces/IBasicToken.sol"; @@ -72,7 +71,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 price = oraclePrice(_asset) * 1e10; uint256 unitPrice = _toUnitPrice(price, _asset); if (unitPrice > 1e18) { unitPrice = 1e18; @@ -575,7 +574,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 price = oraclePrice(allAssets[i]) * 1e10; uint256 unitPrice = _toUnitPrice(price, allAssets[i]); // Never give out more than one // base token per unit of OUSD diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 186b7fab21..80b23e7c41 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -13,6 +13,7 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; @@ -143,4 +144,41 @@ contract VaultStorage is Initializable, Governable { sstore(position, newImpl) } } + + /** + * + */ + function oraclePrice(address asset) internal view returns (uint256 price) { + if ( + // frxETH + asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) + ) { + price = 1e18; + } else if ( + // WETH/ETH + asset == address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) + ) { + price = 1e18; + } else if ( + // rETH/ETH + asset == address(0xae78736Cd615f374D3085123A210448E74Fc6393) + ) { + // feed + // 0xF3272CAfe65b190e76caAF483db13424a3e23dD2 + } else if ( + // cbETH/ETH + asset == address(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) + ) { + // feed + // 0xF017fcB346A1885194689bA23Eff2fE6fA5C483b + } else if ( + // stETH/ETH + asset == address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) + ) { + // feed + // 0x86392dC19c0b719886221c78AB11eb8Cf5c52812 + } + //price = IOracle(priceProvider).price(_asset) * 1e10; + + } } diff --git a/contracts/deploy/051_oeth.js b/contracts/deploy/051_oeth.js index 70e875a3cc..193872e438 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"); From 645bfd5fe42b531747c6d34c57aa5a9384287160 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 11:54:27 +0200 Subject: [PATCH 02/15] add slither ignores --- contracts/contracts/strategies/Generalized4626Strategy.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index abc471556f..d9c74f4e58 100644 --- a/contracts/contracts/strategies/Generalized4626Strategy.sol +++ b/contracts/contracts/strategies/Generalized4626Strategy.sol @@ -39,6 +39,7 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { require(_amount > 0, "Must deposit something"); require(_asset == address(assetToken), "Unexpected asset address"); + // slither-disable-next-line constable-states IERC4626(platformAddress).deposit(_amount, address(this)); emit Deposit(_asset, address(shareToken), _amount); } @@ -68,6 +69,7 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { require(_recipient != address(0), "Must specify recipient"); require(_asset == address(assetToken), "Unexpected asset address"); + // slither-disable-next-line constable-states IERC4626(platformAddress).withdraw(_amount, _recipient, address(this)); emit Withdrawal(_asset, address(shareToken), _amount); } From 607dcc2a5106b799550eead50a5beee59bd12476 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:03:07 +0200 Subject: [PATCH 03/15] create a suggestion of how to check for prices in the Vault --- contracts/contracts/oracle/OracleRouter.sol | 15 +++++ contracts/contracts/vault/VaultStorage.sol | 71 ++++++++++++++------- contracts/deploy/001_core.js | 3 + 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index cbae64fab4..e57cb84a28 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -83,6 +83,21 @@ contract OracleRouter is OracleRouterBase { ) { // Chainlink: CVX/USD return address(0xd962fC30A72A84cE50161031391756Bf2876Af5D); + } else if ( + asset == address(0xae78736Cd615f374D3085123A210448E74Fc6393) + ) { + // Chainlink: rETH/ETH + return address(0x536218f9E9Eb48863970252233c8F271f554C2d0); + } else if ( + asset == address(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) + ) { + // Chainlink: cbETH/ETH + return address(0xF017fcB346A1885194689bA23Eff2fE6fA5C483b); + } else if ( + asset == address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) + ) { + // Chainlink: stETH/ETH + return address(0x86392dC19c0b719886221c78AB11eb8Cf5c52812); } else { revert("Asset not available"); } diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index e7f0fb6e4a..99d7473afa 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -14,6 +14,7 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; import { IOracle } from "../interfaces/IOracle.sol"; +import { Helpers } from "../utils/Helpers.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; @@ -129,6 +130,9 @@ 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_DRIFT = 0.7e18; + uint256 constant MAX_DRIFT = 1.3e18; + /** * @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 @@ -146,39 +150,58 @@ contract VaultStorage is Initializable, Governable { } /** - * + * @dev asset is pegged to a unit - be it USD or ETH + * @param _asset address of the asset + */ + function isUnitPegged(address _asset) internal view returns (bool) { + string memory symbol = Helpers.getSymbol(_asset); + bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); + return + symbolHash == keccak256(abi.encodePacked("DAI")) || + symbolHash == keccak256(abi.encodePacked("USDC")) || + symbolHash == keccak256(abi.encodePacked("USDT")) || + symbolHash == keccak256(abi.encodePacked("frxETH")) || + symbolHash == keccak256(abi.encodePacked("WETH")) || + symbolHash == keccak256(abi.encodePacked("stETH")); + } + + /** + * @dev asset was pegged to a unit and accrues value in such manner + * that it's price only increases + * @param _asset address of the asset + */ + function isPeggedWithPositiveExchange(address _asset) internal view returns (bool) { + string memory symbol = Helpers.getSymbol(_asset); + bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); + return + // increases in price + symbolHash == keccak256(abi.encodePacked("cbETH")) || + // increases in price + symbolHash == keccak256(abi.encodePacked("rETH")); + } + + /** + * @dev returns the oracle price of the asset and hardcodes 1:1 those who + * don't have oracles. + * @param _asset address of the asset */ function oraclePrice(address asset) internal view returns (uint256 price) { if ( // frxETH - asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) - ) { - price = 1e18; - } else if ( + asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) || // WETH/ETH asset == address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) ) { price = 1e18; - } else if ( - // rETH/ETH - asset == address(0xae78736Cd615f374D3085123A210448E74Fc6393) - ) { - // feed - // 0xF3272CAfe65b190e76caAF483db13424a3e23dD2 - } else if ( - // cbETH/ETH - asset == address(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) - ) { - // feed - // 0xF017fcB346A1885194689bA23Eff2fE6fA5C483b - } else if ( - // stETH/ETH - asset == address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) - ) { - // feed - // 0x86392dC19c0b719886221c78AB11eb8Cf5c52812 + } else { + price = IOracle(priceProvider).price(asset) * 1e10; } - //price = IOracle(priceProvider).price(_asset) * 1e10; + if (isUnitPegged(asset)) { + require(price <= MAX_DRIFT, "Vault: Price exceeds max"); + require(price >= MIN_DRIFT, "Vault: Price under min"); + } else if (isPeggedWithPositiveExchange(asset)) { + require(price >= MIN_DRIFT, "Vault: Price under min"); + } } } diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 3cf8884d22..682710c159 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -487,15 +487,18 @@ const configureVault = async (harvesterProxy) => { await ethers.getContract("VaultProxy") ).address ); + console.error("111") // Set up supported assets for Vault await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.DAI, 0) ); + console.error("2222") log("Added DAI asset to Vault"); await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.USDT, 0) ); log("Added USDT asset to Vault"); + console.error("333") await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.USDC, 0) ); From c442f64b105bc7d2638d76b2a0ee525ebec56bc0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:05:43 +0200 Subject: [PATCH 04/15] remove logs --- contracts/deploy/001_core.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 682710c159..3cf8884d22 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -487,18 +487,15 @@ const configureVault = async (harvesterProxy) => { await ethers.getContract("VaultProxy") ).address ); - console.error("111") // Set up supported assets for Vault await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.DAI, 0) ); - console.error("2222") log("Added DAI asset to Vault"); await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.USDT, 0) ); log("Added USDT asset to Vault"); - console.error("333") await withConfirmation( cVault.connect(sGovernor).supportAsset(assetAddresses.USDC, 0) ); From 252f40b7f1482ca80a30a514482610de564b9b18 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:07:54 +0200 Subject: [PATCH 05/15] better name --- contracts/contracts/vault/VaultStorage.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 99d7473afa..8264cc9f2e 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -170,7 +170,7 @@ contract VaultStorage is Initializable, Governable { * that it's price only increases * @param _asset address of the asset */ - function isPeggedWithPositiveExchange(address _asset) internal view returns (bool) { + function isUnitPeggedWithPositiveExchange(address _asset) internal view returns (bool) { string memory symbol = Helpers.getSymbol(_asset); bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); return @@ -200,7 +200,7 @@ contract VaultStorage is Initializable, Governable { if (isUnitPegged(asset)) { require(price <= MAX_DRIFT, "Vault: Price exceeds max"); require(price >= MIN_DRIFT, "Vault: Price under min"); - } else if (isPeggedWithPositiveExchange(asset)) { + } else if (isUnitPeggedWithPositiveExchange(asset)) { require(price >= MIN_DRIFT, "Vault: Price under min"); } } From 2b2f56e6750c275df00bdb0a05b14325766867cc Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:09:20 +0200 Subject: [PATCH 06/15] minor refactor --- contracts/contracts/vault/VaultCore.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index e5db657382..bafed9fe1f 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -71,8 +71,7 @@ contract VaultCore is VaultStorage { require(_amount > 0, "Amount must be greater than 0"); uint256 units = _toUnits(_amount, _asset); - uint256 price = oraclePrice(_asset) * 1e10; - uint256 unitPrice = _toUnitPrice(price, _asset); + uint256 unitPrice = _toUnitPrice(_asset); if (unitPrice > 1e18) { unitPrice = 1e18; } @@ -574,8 +573,7 @@ contract VaultCore is VaultStorage { // Calculate totalOutputRatio uint256 totalOutputRatio = 0; for (uint256 i = 0; i < assetCount; i++) { - uint256 price = oraclePrice(allAssets[i]) * 1e10; - uint256 unitPrice = _toUnitPrice(price, allAssets[i]); + uint256 unitPrice = _toUnitPrice(allAssets[i]); // Never give out more than one // base token per unit of OUSD if (unitPrice < 1e18) { @@ -665,6 +663,7 @@ contract VaultCore is VaultStorage { returns (uint256) { UnitConversion conversion = assets[_asset].unitConversion; + uint256 price = oraclePrice(_asset) * 1e10; if (conversion == UnitConversion.DECIMALS) { return _price; } else if (conversion == UnitConversion.GETEXCHANGERATE) { From a9649b65cb31dc5c07df64b72de32808c99a3975 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:09:54 +0200 Subject: [PATCH 07/15] minor refactor v2 --- contracts/contracts/vault/VaultCore.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index bafed9fe1f..d28edbceef 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -657,7 +657,7 @@ contract VaultCore is VaultStorage { } } - function _toUnitPrice(uint256 _price, address _asset) + function _toUnitPrice(address _asset) internal view returns (uint256) @@ -665,11 +665,11 @@ contract VaultCore is VaultStorage { UnitConversion conversion = assets[_asset].unitConversion; uint256 price = oraclePrice(_asset) * 1e10; if (conversion == UnitConversion.DECIMALS) { - return _price; + return price; } else if (conversion == UnitConversion.GETEXCHANGERATE) { uint256 exchangeRate = IGetExchangeRateToken(_asset) .getExchangeRate(); - return (_price * 1e18) / exchangeRate; + return (price * 1e18) / exchangeRate; } else { require(false, "Unsupported conversion type"); } From 70e7b2fd59d490a766d5849139e6b7f9b3a74815 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 15:58:17 +0200 Subject: [PATCH 08/15] move unit pricing to a separate function --- contracts/contracts/oracle/OracleRouter.sol | 15 +++++ contracts/contracts/vault/VaultCore.sol | 60 +++++++++++++------- contracts/contracts/vault/VaultStorage.sol | 61 --------------------- 3 files changed, 56 insertions(+), 80 deletions(-) diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index e57cb84a28..6b31ab1af9 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -8,6 +8,7 @@ import { Helpers } from "../utils/Helpers.sol"; abstract contract OracleRouterBase is IOracle { uint256 constant MIN_DRIFT = uint256(70000000); uint256 constant MAX_DRIFT = uint256(130000000); + address constant FIXED_PRICE = 0x0000000000000000000000000000000000000001; /** * @dev The price feed contract to use for a particular asset. @@ -24,6 +25,7 @@ abstract contract OracleRouterBase is IOracle { function price(address asset) external view virtual override returns (uint256) { address _feed = feed(asset); 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); @@ -98,6 +100,16 @@ contract OracleRouter is OracleRouterBase { ) { // Chainlink: stETH/ETH return address(0x86392dC19c0b719886221c78AB11eb8Cf5c52812); + } else if ( + asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) + ) { + // FIXED_PRICE: frxETH/ETH + return address(FIXED_PRICE); + } else if ( + asset == address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) + ) { + // FIXED_PRICE: WETH/ETH + return address(FIXED_PRICE); } else { revert("Asset not available"); } @@ -114,6 +126,9 @@ contract OETHOracleRouter is OracleRouter { */ 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(); diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index d28edbceef..5155667303 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -17,6 +17,7 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import { StableMath } from "../utils/StableMath.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"; @@ -71,11 +72,7 @@ contract VaultCore is VaultStorage { require(_amount > 0, "Amount must be greater than 0"); uint256 units = _toUnits(_amount, _asset); - uint256 unitPrice = _toUnitPrice(_asset); - if (unitPrice > 1e18) { - unitPrice = 1e18; - } - require(unitPrice >= MINT_MINIMUM_ORACLE, "Asset price below peg"); + uint256 unitPrice = _toUnitPrice(_asset, true); uint256 priceAdjustedDeposit = (units * unitPrice) / 1e18; if (_minimumOusdAmount > 0) { @@ -573,12 +570,7 @@ contract VaultCore is VaultStorage { // Calculate totalOutputRatio uint256 totalOutputRatio = 0; for (uint256 i = 0; i < assetCount; i++) { - uint256 unitPrice = _toUnitPrice(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); } @@ -657,22 +649,52 @@ contract VaultCore is VaultStorage { } } - function _toUnitPrice(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; - uint256 price = oraclePrice(_asset) * 1e10; - 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"); } + + 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) { diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 8264cc9f2e..92c0f9ea85 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -13,8 +13,6 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; -import { Helpers } from "../utils/Helpers.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; @@ -130,9 +128,6 @@ 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_DRIFT = 0.7e18; - uint256 constant MAX_DRIFT = 1.3e18; - /** * @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 @@ -148,60 +143,4 @@ contract VaultStorage is Initializable, Governable { sstore(position, newImpl) } } - - /** - * @dev asset is pegged to a unit - be it USD or ETH - * @param _asset address of the asset - */ - function isUnitPegged(address _asset) internal view returns (bool) { - string memory symbol = Helpers.getSymbol(_asset); - bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); - return - symbolHash == keccak256(abi.encodePacked("DAI")) || - symbolHash == keccak256(abi.encodePacked("USDC")) || - symbolHash == keccak256(abi.encodePacked("USDT")) || - symbolHash == keccak256(abi.encodePacked("frxETH")) || - symbolHash == keccak256(abi.encodePacked("WETH")) || - symbolHash == keccak256(abi.encodePacked("stETH")); - } - - /** - * @dev asset was pegged to a unit and accrues value in such manner - * that it's price only increases - * @param _asset address of the asset - */ - function isUnitPeggedWithPositiveExchange(address _asset) internal view returns (bool) { - string memory symbol = Helpers.getSymbol(_asset); - bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); - return - // increases in price - symbolHash == keccak256(abi.encodePacked("cbETH")) || - // increases in price - symbolHash == keccak256(abi.encodePacked("rETH")); - } - - /** - * @dev returns the oracle price of the asset and hardcodes 1:1 those who - * don't have oracles. - * @param _asset address of the asset - */ - function oraclePrice(address asset) internal view returns (uint256 price) { - if ( - // frxETH - asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) || - // WETH/ETH - asset == address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) - ) { - price = 1e18; - } else { - price = IOracle(priceProvider).price(asset) * 1e10; - } - - if (isUnitPegged(asset)) { - require(price <= MAX_DRIFT, "Vault: Price exceeds max"); - require(price >= MIN_DRIFT, "Vault: Price under min"); - } else if (isUnitPeggedWithPositiveExchange(asset)) { - require(price >= MIN_DRIFT, "Vault: Price under min"); - } - } } From 6b457289f709882750d6554ddb762d63b613fdf3 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 16:11:19 +0200 Subject: [PATCH 09/15] refactor --- contracts/contracts/vault/VaultAdmin.sol | 33 +++----- contracts/contracts/vault/VaultCore.sol | 88 ---------------------- contracts/contracts/vault/VaultStorage.sol | 88 ++++++++++++++++++++++ 3 files changed, 100 insertions(+), 109 deletions(-) diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 7640145c59..eee3663862 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -180,7 +180,7 @@ contract VaultAdmin is VaultStorage { // Verify that our oracle supports the asset // slither-disable-next-line unused-return - oraclePrice(_asset); + _toUnitPrice(_asset, true); emit AssetSupported(_asset); } @@ -452,34 +452,25 @@ contract VaultAdmin is VaultStorage { ****************************************/ /** - * @dev Returns the total price in 18 digit USD for a given asset. - * Never goes above 1, since that is how we price mints + * @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 uint256 USD price of 1 of the asset, in 18 decimal fixed + * @return price uint256: unit (USD / ETH) price of 1 unit of the asset, in 18 decimal fixed */ - function priceUSDMint(address asset) external view returns (uint256) { - uint256 price = oraclePrice(asset); - require(price >= MINT_MINIMUM_ORACLE, "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); + function priceUnitMint(address asset) external view returns (uint256 price) { + uint256 units = _toUnits(1e18, asset); + price = _toUnitPrice(asset, true); } /** - * @dev Returns the total price in 18 digit USD for a given asset. + * @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 uint256 USD price of 1 of the asset, in 18 decimal fixed + * @return price uint256: (USD / ETH) price of 1 unit of the asset, in 18 decimal fixed */ - function priceUSDRedeem(address asset) external view returns (uint256) { - uint256 price = oraclePrice(asset); - if (price < 1e8) { - price = 1e8; - } - // Price from Oracle is returned with 8 decimals so scale to 18 - return price.scaleBy(18, 8); + function priceUnitRedeem(address asset) external view returns (uint256 price) { + uint256 units = _toUnits(1e18, asset); + price = _toUnitPrice(asset, false); } /*************************************** diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 5155667303..5fc36744f2 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -17,10 +17,8 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import { StableMath } from "../utils/StableMath.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"; import "./VaultStorage.sol"; contract VaultCore is VaultStorage { @@ -617,92 +615,6 @@ contract VaultCore is VaultStorage { return assets[_asset].isSupported; } - /** - * @dev Convert a quantity of a token into 1e18 fixed decimal "units" - * in the underlying base (USD/ETH) used by the vault. - * Price is not taken into account, only quantity. - * - * Examples of this conversion: - * - * - 1e18 DAI becomes 1e18 units (same decimals) - * - 1e6 USDC becomes 1e18 units (decimal conversion) - * - 1e18 rETH becomes 1.2e18 units (exchange rate conversion) - * - * @param _raw Quantity of asset - * @param _asset Core Asset address - * @return value 1e18 normalized quantity of units - */ - function _toUnits(uint256 _raw, address _asset) - internal - view - returns (uint256) - { - UnitConversion conversion = assets[_asset].unitConversion; - if (conversion == UnitConversion.DECIMALS) { - return _raw.scaleBy(18, _getDecimals(_asset)); - } else if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - return (_raw * exchangeRate) / 1e18; - } else { - require(false, "Unsupported conversion type"); - } - } - - /** - * @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 price) - { - UnitConversion conversion = assets[_asset].unitConversion; - price = IOracle(priceProvider).price(asset) * 1e10; - - if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - price = (price * 1e18) / exchangeRate; - } else if (conversion != UnitConversion.DECIMALS){ - require(false, "Unsupported conversion type"); - } - - 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) { - uint256 decimals = decimalsCache[_asset]; - require(decimals > 0, "Decimals Not Cached"); - return decimals; - } - /** * @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 92c0f9ea85..79426ad158 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -13,11 +13,13 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; import "../utils/Helpers.sol"; import { StableMath } from "../utils/StableMath.sol"; +import { IGetExchangeRateToken } from "../interfaces/IGetExchangeRateToken.sol"; contract VaultStorage is Initializable, Governable { using SafeMath for uint256; @@ -143,4 +145,90 @@ contract VaultStorage is Initializable, Governable { sstore(position, newImpl) } } + + /** + * @dev Convert a quantity of a token into 1e18 fixed decimal "units" + * in the underlying base (USD/ETH) used by the vault. + * Price is not taken into account, only quantity. + * + * Examples of this conversion: + * + * - 1e18 DAI becomes 1e18 units (same decimals) + * - 1e6 USDC becomes 1e18 units (decimal conversion) + * - 1e18 rETH becomes 1.2e18 units (exchange rate conversion) + * + * @param _raw Quantity of asset + * @param _asset Core Asset address + * @return value 1e18 normalized quantity of units + */ + function _toUnits(uint256 _raw, address _asset) + internal + view + returns (uint256) + { + UnitConversion conversion = assets[_asset].unitConversion; + if (conversion == UnitConversion.DECIMALS) { + return _raw.scaleBy(18, _getDecimals(_asset)); + } else if (conversion == UnitConversion.GETEXCHANGERATE) { + uint256 exchangeRate = IGetExchangeRateToken(_asset) + .getExchangeRate(); + return (_raw * exchangeRate) / 1e18; + } else { + require(false, "Unsupported conversion type"); + } + } + + /** + * @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 price) + { + UnitConversion conversion = assets[_asset].unitConversion; + price = IOracle(priceProvider).price(_asset) * 1e10; + + if (conversion == UnitConversion.GETEXCHANGERATE) { + uint256 exchangeRate = IGetExchangeRateToken(_asset) + .getExchangeRate(); + price = (price * 1e18) / exchangeRate; + } else if (conversion != UnitConversion.DECIMALS){ + require(false, "Unsupported conversion type"); + } + + 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) { + uint256 decimals = decimalsCache[_asset]; + require(decimals > 0, "Decimals Not Cached"); + return decimals; + } } From 22d52bc5c09ad4cb81e60df55fb97103a5d77275 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 16:17:30 +0200 Subject: [PATCH 10/15] better comment --- contracts/contracts/vault/VaultAdmin.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index eee3663862..6c33ff9a9d 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -455,7 +455,7 @@ contract VaultAdmin is VaultStorage { * @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 of 1 unit of the asset, in 18 decimal fixed + * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed */ function priceUnitMint(address asset) external view returns (uint256 price) { uint256 units = _toUnits(1e18, asset); @@ -466,7 +466,7 @@ contract VaultAdmin is VaultStorage { * @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: (USD / ETH) price of 1 unit of the asset, in 18 decimal fixed + * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed */ function priceUnitRedeem(address asset) external view returns (uint256 price) { uint256 units = _toUnits(1e18, asset); From 7989fe4eae42ccf6d2ca75032343c187f96806d0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 17:02:27 +0200 Subject: [PATCH 11/15] refactor Vault contract and correct price calculation in priceUnit(Mint/Redeem) functions --- contracts/contracts/oracle/OracleRouter.sol | 18 ++- contracts/contracts/vault/VaultAdmin.sol | 29 +---- contracts/contracts/vault/VaultCore.sol | 129 ++++++++++++++++++++ contracts/contracts/vault/VaultStorage.sol | 87 ------------- 4 files changed, 146 insertions(+), 117 deletions(-) diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index 6b31ab1af9..e3ea226fce 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -22,7 +22,13 @@ 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 virtual override returns (uint256) { + function price(address asset) + external + view + virtual + override + returns (uint256) + { address _feed = feed(asset); require(_feed != address(0), "Asset not available"); require(_feed != FIXED_PRICE, "Fixed price feeds not supported"); @@ -119,12 +125,18 @@ contract OracleRouter is OracleRouterBase { 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 + * 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) { + function price(address asset) + external + view + virtual + override + returns (uint256) + { address _feed = feed(asset); if (_feed == FIXED_PRICE) { return 1e8; diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 6c33ff9a9d..27be8572a1 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -10,6 +10,7 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { StableMath } from "../utils/StableMath.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; import "./VaultStorage.sol"; contract VaultAdmin is VaultStorage { @@ -180,7 +181,7 @@ contract VaultAdmin is VaultStorage { // Verify that our oracle supports the asset // slither-disable-next-line unused-return - _toUnitPrice(_asset, true); + IOracle(priceProvider).price(_asset) * 1e10; emit AssetSupported(_asset); } @@ -447,32 +448,6 @@ contract VaultAdmin is VaultStorage { IERC20(_asset).safeTransfer(governor(), _amount); } - /*************************************** - Pricing - ****************************************/ - - /** - * @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 priceUnitMint(address asset) external view returns (uint256 price) { - uint256 units = _toUnits(1e18, asset); - price = _toUnitPrice(asset, true); - } - - /** - * @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 priceUnitRedeem(address asset) external view returns (uint256 price) { - uint256 units = _toUnits(1e18, asset); - price = _toUnitPrice(asset, false); - } - /*************************************** Strategies Admin ****************************************/ diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 5fc36744f2..b9e752ceab 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -17,6 +17,7 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import { StableMath } from "../utils/StableMath.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 "./VaultStorage.sol"; @@ -579,10 +580,138 @@ contract VaultCore is VaultStorage { } } + /*************************************** + Pricing + ****************************************/ + + /** + * @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 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(1e18.scaleBy(_getDecimals(asset), 18), asset); + price = _toUnitPrice(asset, true) * units; + } + + /** + * @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 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(1e18.scaleBy(_getDecimals(asset), 18), asset); + price = _toUnitPrice(asset, false) * units; + } + /*************************************** Utils ****************************************/ + /** + * @dev Convert a quantity of a token into 1e18 fixed decimal "units" + * in the underlying base (USD/ETH) used by the vault. + * Price is not taken into account, only quantity. + * + * Examples of this conversion: + * + * - 1e18 DAI becomes 1e18 units (same decimals) + * - 1e6 USDC becomes 1e18 units (decimal conversion) + * - 1e18 rETH becomes 1.2e18 units (exchange rate conversion) + * + * @param _raw Quantity of asset + * @param _asset Core Asset address + * @return value 1e18 normalized quantity of units + */ + function _toUnits(uint256 _raw, address _asset) + internal + view + returns (uint256) + { + UnitConversion conversion = assets[_asset].unitConversion; + if (conversion == UnitConversion.DECIMALS) { + return _raw.scaleBy(18, _getDecimals(_asset)); + } else if (conversion == UnitConversion.GETEXCHANGERATE) { + uint256 exchangeRate = IGetExchangeRateToken(_asset) + .getExchangeRate(); + return (_raw * exchangeRate) / 1e18; + } else { + require(false, "Unsupported conversion type"); + } + } + + /** + * @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 price) + { + UnitConversion conversion = assets[_asset].unitConversion; + price = IOracle(priceProvider).price(_asset) * 1e10; + + if (conversion == UnitConversion.GETEXCHANGERATE) { + uint256 exchangeRate = IGetExchangeRateToken(_asset) + .getExchangeRate(); + price = (price * 1e18) / exchangeRate; + } else if (conversion != UnitConversion.DECIMALS) { + require(false, "Unsupported conversion type"); + } + + 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) { + uint256 decimals = decimalsCache[_asset]; + require(decimals > 0, "Decimals Not Cached"); + return decimals; + } + /** * @dev Return the number of assets supported by the Vault. */ diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 79426ad158..96cf93b188 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -13,7 +13,6 @@ import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; @@ -145,90 +144,4 @@ contract VaultStorage is Initializable, Governable { sstore(position, newImpl) } } - - /** - * @dev Convert a quantity of a token into 1e18 fixed decimal "units" - * in the underlying base (USD/ETH) used by the vault. - * Price is not taken into account, only quantity. - * - * Examples of this conversion: - * - * - 1e18 DAI becomes 1e18 units (same decimals) - * - 1e6 USDC becomes 1e18 units (decimal conversion) - * - 1e18 rETH becomes 1.2e18 units (exchange rate conversion) - * - * @param _raw Quantity of asset - * @param _asset Core Asset address - * @return value 1e18 normalized quantity of units - */ - function _toUnits(uint256 _raw, address _asset) - internal - view - returns (uint256) - { - UnitConversion conversion = assets[_asset].unitConversion; - if (conversion == UnitConversion.DECIMALS) { - return _raw.scaleBy(18, _getDecimals(_asset)); - } else if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - return (_raw * exchangeRate) / 1e18; - } else { - require(false, "Unsupported conversion type"); - } - } - - /** - * @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 price) - { - UnitConversion conversion = assets[_asset].unitConversion; - price = IOracle(priceProvider).price(_asset) * 1e10; - - if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - price = (price * 1e18) / exchangeRate; - } else if (conversion != UnitConversion.DECIMALS){ - require(false, "Unsupported conversion type"); - } - - 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) { - uint256 decimals = decimalsCache[_asset]; - require(decimals > 0, "Decimals Not Cached"); - return decimals; - } } From 4bdcfa0f7a391f0d094ebaf6dae2e525b0fc0314 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 11 Apr 2023 23:45:25 +0200 Subject: [PATCH 12/15] add range checks --- contracts/contracts/vault/VaultCore.sol | 11 +++++++++-- contracts/contracts/vault/VaultStorage.sol | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index b9e752ceab..fdfd2fd988 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -599,7 +599,7 @@ contract VaultCore is VaultStorage { * to 1e18 and ignore calling `_toUnits` since we need to consider assets * with the exchange rate */ - uint256 units = _toUnits(1e18.scaleBy(_getDecimals(asset), 18), asset); + uint256 units = _toUnits(uint256(1e18).scaleBy(_getDecimals(asset), 18), asset); price = _toUnitPrice(asset, true) * units; } @@ -618,7 +618,7 @@ contract VaultCore is VaultStorage { * to 1e18 and ignore calling `_toUnits` since we need to consider assets * with the exchange rate */ - uint256 units = _toUnits(1e18.scaleBy(_getDecimals(asset), 18), asset); + uint256 units = _toUnits(uint256(1e18).scaleBy(_getDecimals(asset), 18), asset); price = _toUnitPrice(asset, false) * units; } @@ -688,6 +688,13 @@ contract VaultCore is VaultStorage { 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. diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 96cf93b188..b47432749d 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -129,6 +129,9 @@ 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; + /** * @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 From 2e78e4bae61dd52862f4d0c142da1aaab1d2a3f0 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 12 Apr 2023 10:00:47 -0400 Subject: [PATCH 13/15] Prettier and address cleanup --- contracts/contracts/oracle/OracleRouter.sol | 70 +++++++-------------- contracts/contracts/vault/VaultCore.sol | 12 +++- 2 files changed, 33 insertions(+), 49 deletions(-) diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index e3ea226fce..af2eac6a2e 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -58,64 +58,42 @@ 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(0xae78736Cd615f374D3085123A210448E74Fc6393) - ) { + return 0xd962fC30A72A84cE50161031391756Bf2876Af5D; + } else if (asset == 0xae78736Cd615f374D3085123A210448E74Fc6393) { // Chainlink: rETH/ETH - return address(0x536218f9E9Eb48863970252233c8F271f554C2d0); - } else if ( - asset == address(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) - ) { + return 0x536218f9E9Eb48863970252233c8F271f554C2d0; + } else if (asset == 0xBe9895146f7AF43049ca1c1AE358B0541Ea49704) { // Chainlink: cbETH/ETH - return address(0xF017fcB346A1885194689bA23Eff2fE6fA5C483b); - } else if ( - asset == address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) - ) { + return 0xF017fcB346A1885194689bA23Eff2fE6fA5C483b; + } else if (asset == 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) { // Chainlink: stETH/ETH - return address(0x86392dC19c0b719886221c78AB11eb8Cf5c52812); - } else if ( - asset == address(0x5E8422345238F34275888049021821E8E08CAa1f) - ) { + return 0x86392dC19c0b719886221c78AB11eb8Cf5c52812; + } else if (asset == 0x5E8422345238F34275888049021821E8E08CAa1f) { // FIXED_PRICE: frxETH/ETH - return address(FIXED_PRICE); - } else if ( - asset == address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) - ) { + return FIXED_PRICE; + } else if (asset == 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) { // FIXED_PRICE: WETH/ETH - return address(FIXED_PRICE); + return FIXED_PRICE; } else { revert("Asset not available"); } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index fdfd2fd988..d48a51a4f8 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -599,7 +599,10 @@ contract VaultCore is VaultStorage { * 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); + uint256 units = _toUnits( + uint256(1e18).scaleBy(_getDecimals(asset), 18), + asset + ); price = _toUnitPrice(asset, true) * units; } @@ -618,7 +621,10 @@ contract VaultCore is VaultStorage { * 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); + uint256 units = _toUnits( + uint256(1e18).scaleBy(_getDecimals(asset), 18), + asset + ); price = _toUnitPrice(asset, false) * units; } @@ -689,7 +695,7 @@ contract VaultCore is VaultStorage { } /* At this stage the price is already adjusted to the unit - * so the price checks are agnostic to underlying asset being + * 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"); From c4751b979409a156d76b2707b6c5fdbd3d5849ea Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 12 Apr 2023 10:04:51 -0400 Subject: [PATCH 14/15] Small cleanup --- contracts/contracts/vault/VaultAdmin.sol | 2 +- contracts/contracts/vault/VaultCore.sol | 1 + contracts/contracts/vault/VaultStorage.sol | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index 27be8572a1..0bd97b753d 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -181,7 +181,7 @@ contract VaultAdmin is VaultStorage { // Verify that our oracle supports the asset // slither-disable-next-line unused-return - IOracle(priceProvider).price(_asset) * 1e10; + IOracle(priceProvider).price(_asset); emit AssetSupported(_asset); } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index d48a51a4f8..e846c1946f 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -20,6 +20,7 @@ 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"; import "./VaultStorage.sol"; contract VaultCore is VaultStorage { diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index b47432749d..d43e77db37 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -18,7 +18,6 @@ import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; import "../utils/Helpers.sol"; import { StableMath } from "../utils/StableMath.sol"; -import { IGetExchangeRateToken } from "../interfaces/IGetExchangeRateToken.sol"; contract VaultStorage is Initializable, Governable { using SafeMath for uint256; From db8fa95aadc3dc3a8f1b44ea8f4715e3d9f71e19 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 12 Apr 2023 10:22:27 -0400 Subject: [PATCH 15/15] Add back in missing constant --- contracts/contracts/vault/VaultStorage.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 62c69c3520..e0e490aaf6 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -130,6 +130,7 @@ contract VaultStorage is Initializable, Governable { 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