|
| 1 | +// SPDX-License-Identifier: GPL-2.0-or-later |
| 2 | +pragma solidity 0.8.21; |
| 3 | + |
| 4 | +import {IChainlinkOracle} from "./interfaces/IChainlinkOracle.sol"; |
| 5 | +import {IOracle} from "../../lib/morpho-blue/src/interfaces/IOracle.sol"; |
| 6 | + |
| 7 | +import {AggregatorV3Interface, ChainlinkDataFeedLib} from "./libraries/ChainlinkDataFeedLib.sol"; |
| 8 | +import {IERC4626, VaultLib} from "./libraries/VaultLib.sol"; |
| 9 | +import {ErrorsLib} from "./libraries/ErrorsLib.sol"; |
| 10 | +import {Math} from "../../lib/openzeppelin-contracts/contracts/utils/math/Math.sol"; |
| 11 | + |
| 12 | +/// @title ChainlinkOracle |
| 13 | +/// @author Morpho Labs |
| 14 | +/// @custom:contact [email protected] |
| 15 | +/// @notice Morpho Blue oracle using Chainlink-compliant feeds. |
| 16 | +contract ChainlinkOracle is IChainlinkOracle { |
| 17 | + using Math for uint256; |
| 18 | + using VaultLib for IERC4626; |
| 19 | + using ChainlinkDataFeedLib for AggregatorV3Interface; |
| 20 | + |
| 21 | + /* IMMUTABLES */ |
| 22 | + |
| 23 | + /// @inheritdoc IChainlinkOracle |
| 24 | + IERC4626 public immutable BASE_VAULT; |
| 25 | + |
| 26 | + /// @inheritdoc IChainlinkOracle |
| 27 | + uint256 public immutable BASE_VAULT_CONVERSION_SAMPLE; |
| 28 | + |
| 29 | + /// @inheritdoc IChainlinkOracle |
| 30 | + IERC4626 public immutable QUOTE_VAULT; |
| 31 | + |
| 32 | + /// @inheritdoc IChainlinkOracle |
| 33 | + uint256 public immutable QUOTE_VAULT_CONVERSION_SAMPLE; |
| 34 | + |
| 35 | + /// @inheritdoc IChainlinkOracle |
| 36 | + AggregatorV3Interface public immutable BASE_FEED_1; |
| 37 | + |
| 38 | + /// @inheritdoc IChainlinkOracle |
| 39 | + AggregatorV3Interface public immutable BASE_FEED_2; |
| 40 | + |
| 41 | + /// @inheritdoc IChainlinkOracle |
| 42 | + AggregatorV3Interface public immutable QUOTE_FEED_1; |
| 43 | + |
| 44 | + /// @inheritdoc IChainlinkOracle |
| 45 | + AggregatorV3Interface public immutable QUOTE_FEED_2; |
| 46 | + |
| 47 | + /// @inheritdoc IChainlinkOracle |
| 48 | + uint256 public immutable SCALE_FACTOR; |
| 49 | + |
| 50 | + /* CONSTRUCTOR */ |
| 51 | + |
| 52 | + /// @dev Here is the list of assumptions that guarantees the oracle behaves as expected: |
| 53 | + /// - Feeds are either Chainlink-compliant or the address zero. |
| 54 | + /// - Feeds have the same behavioral assumptions as Chainlink's. |
| 55 | + /// - Feeds are set in the correct order. |
| 56 | + /// - Decimals passed as argument are correct. |
| 57 | + /// - The vault's sample shares quoted as assets and the base feed prices don't overflow when multiplied. |
| 58 | + /// - The quote feed prices don't overflow when multiplied. |
| 59 | + /// - The vault, if set, is ERC4626-compliant. |
| 60 | + /// @dev The base asset is the collateral token and the quote asset is the loan token. |
| 61 | + /// @param baseVault Base vault. Pass address zero to omit this parameter. |
| 62 | + /// @param baseVaultConversionSample The sample amount of base vault shares used to convert to underlying. |
| 63 | + /// Pass 1 if the base asset is not a vault. Should be chosen such that converting `baseVaultConversionSample` to |
| 64 | + /// assets has enough precision. |
| 65 | + /// @param baseFeed1 First base feed. Pass address zero if the price = 1. |
| 66 | + /// @param baseFeed2 Second base feed. Pass address zero if the price = 1. |
| 67 | + /// @param baseTokenDecimals Base token decimals. |
| 68 | + /// @param quoteVault Quote vault. Pass address zero to omit this parameter. |
| 69 | + /// @param quoteVaultConversionSample The sample amount of quote vault shares used to convert to underlying. |
| 70 | + /// Pass 1 if the quote asset is not a vault. Should be chosen such that converting `quoteVaultConversionSample` to |
| 71 | + /// assets has enough precision. |
| 72 | + /// @param quoteFeed1 First quote feed. Pass address zero if the price = 1. |
| 73 | + /// @param quoteFeed2 Second quote feed. Pass address zero if the price = 1. |
| 74 | + /// @param quoteTokenDecimals Quote token decimals. |
| 75 | + constructor( |
| 76 | + IERC4626 baseVault, |
| 77 | + uint256 baseVaultConversionSample, |
| 78 | + AggregatorV3Interface baseFeed1, |
| 79 | + AggregatorV3Interface baseFeed2, |
| 80 | + uint256 baseTokenDecimals, |
| 81 | + IERC4626 quoteVault, |
| 82 | + uint256 quoteVaultConversionSample, |
| 83 | + AggregatorV3Interface quoteFeed1, |
| 84 | + AggregatorV3Interface quoteFeed2, |
| 85 | + uint256 quoteTokenDecimals |
| 86 | + ) { |
| 87 | + // The ERC4626 vaults parameter is used to price their respective conversion samples of their respective shares, |
| 88 | + // so it requires multiplying by `QUOTE_VAULT_CONVERSION_SAMPLE` and dividing `BASE_VAULT_CONVERSION_SAMPLE` by |
| 89 | + // the `SCALE_FACTOR` definition. |
| 90 | + // Verify that vault = address(0) => vaultConversionSample = 1 for each vault. |
| 91 | + require( |
| 92 | + address(baseVault) != address(0) || baseVaultConversionSample == 1, |
| 93 | + ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE |
| 94 | + ); |
| 95 | + require( |
| 96 | + address(quoteVault) != address(0) || quoteVaultConversionSample == 1, |
| 97 | + ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE |
| 98 | + ); |
| 99 | + require(baseVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO); |
| 100 | + require(quoteVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO); |
| 101 | + |
| 102 | + BASE_VAULT = baseVault; |
| 103 | + BASE_VAULT_CONVERSION_SAMPLE = baseVaultConversionSample; |
| 104 | + QUOTE_VAULT = quoteVault; |
| 105 | + QUOTE_VAULT_CONVERSION_SAMPLE = quoteVaultConversionSample; |
| 106 | + BASE_FEED_1 = baseFeed1; |
| 107 | + BASE_FEED_2 = baseFeed2; |
| 108 | + QUOTE_FEED_1 = quoteFeed1; |
| 109 | + QUOTE_FEED_2 = quoteFeed2; |
| 110 | + |
| 111 | + // In the following comment, we explain the general case (where we assume that no feed is the address zero) |
| 112 | + // how to scale the output price as Morpho Blue expects, given the input feed prices. |
| 113 | + // Similar explanations would hold in the case where some of the feeds are the address zero. |
| 114 | + |
| 115 | + // Let B1, B2, Q1, Q2, C be 5 assets, each respectively having dB1, dB2, dQ1, dQ2, dC decimals. |
| 116 | + // Let pB1 and pB2 be the base prices, and pQ1 and pQ2 the quote prices, so that: |
| 117 | + // - pB1 is the quantity of 1e(dB2) assets B2 that can be exchanged for 1e(dB1) assets B1. |
| 118 | + // - pB2 is the quantity of 1e(dC) assets C that can be exchanged for 1e(dB2) assets B2. |
| 119 | + // - pQ1 is the quantity of 1e(dQ2) assets Q2 that can be exchanged for 1e(dQ1) assets Q1. |
| 120 | + // - pQ2 is the quantity of 1e(dC) assets C that can be exchanged for 1e(dQ2) assets B2. |
| 121 | + |
| 122 | + // Morpho Blue expects `price()` to be the quantity of 1 asset Q1 that can be exchanged for 1 asset B1, |
| 123 | + // scaled by 1e36: |
| 124 | + // 1e36 * (pB1 * 1e(dB2 - dB1)) * (pB2 * 1e(dC - dB2)) / ((pQ1 * 1e(dQ2 - dQ1)) * (pQ2 * 1e(dC - dQ2))) |
| 125 | + // = 1e36 * (pB1 * 1e(-dB1) * pB2) / (pQ1 * 1e(-dQ1) * pQ2) |
| 126 | + |
| 127 | + // Let fpB1, fpB2, fpQ1, fpQ2 be the feed precision of the respective prices pB1, pB2, pQ1, pQ2. |
| 128 | + // Chainlink feeds return pB1 * 1e(fpB1), pB2 * 1e(fpB2), pQ1 * 1e(fpQ1) and pQ2 * 1e(fpQ2). |
| 129 | + |
| 130 | + // Based on the implementation of `price()` below, the value of `SCALE_FACTOR` should thus satisfy: |
| 131 | + // (pB1 * 1e(fpB1)) * (pB2 * 1e(fpB2)) * SCALE_FACTOR / ((pQ1 * 1e(fpQ1)) * (pQ2 * 1e(fpQ2))) |
| 132 | + // = 1e36 * (pB1 * 1e(-dB1) * pB2) / (pQ1 * 1e(-dQ1) * pQ2) |
| 133 | + |
| 134 | + // So SCALE_FACTOR = 1e36 * 1e(-dB1) * 1e(dQ1) * 1e(-fpB1) * 1e(-fpB2) * 1e(fpQ1) * 1e(fpQ2) |
| 135 | + // = 1e(36 + dQ1 + fpQ1 + fpQ2 - dB1 - fpB1 - fpB2) |
| 136 | + SCALE_FACTOR = 10 |
| 137 | + ** ( |
| 138 | + 36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals |
| 139 | + - baseFeed1.getDecimals() - baseFeed2.getDecimals() |
| 140 | + ) * quoteVaultConversionSample / baseVaultConversionSample; |
| 141 | + } |
| 142 | + |
| 143 | + /* PRICE */ |
| 144 | + |
| 145 | + /// @inheritdoc IOracle |
| 146 | + function price() external view returns (uint256) { |
| 147 | + return SCALE_FACTOR.mulDiv( |
| 148 | + BASE_VAULT.getAssets(BASE_VAULT_CONVERSION_SAMPLE) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice(), |
| 149 | + QUOTE_VAULT.getAssets(QUOTE_VAULT_CONVERSION_SAMPLE) * QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice() |
| 150 | + ); |
| 151 | + } |
| 152 | +} |
0 commit comments