Skip to content

Commit 9635b2b

Browse files
committed
refactor: repo
1 parent 0908598 commit 9635b2b

File tree

9 files changed

+314
-52
lines changed

9 files changed

+314
-52
lines changed

src/morpho-chainlink-v1/ChainlinkOracle.sol

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,10 @@ contract ChainlinkOracle is IChainlinkOracle {
2121
/* IMMUTABLES */
2222

2323
/// @inheritdoc IChainlinkOracle
24-
IERC4626 public immutable BASE_VAULT;
24+
IERC4626 public immutable VAULT;
2525

2626
/// @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;
27+
uint256 public immutable VAULT_CONVERSION_SAMPLE;
3428

3529
/// @inheritdoc IChainlinkOracle
3630
AggregatorV3Interface public immutable BASE_FEED_1;
@@ -57,52 +51,36 @@ contract ChainlinkOracle is IChainlinkOracle {
5751
/// - The vault's sample shares quoted as assets and the base feed prices don't overflow when multiplied.
5852
/// - The quote feed prices don't overflow when multiplied.
5953
/// - 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.
54+
/// @param vault Vault. Pass address zero to omit this parameter.
6555
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
6656
/// @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.
7257
/// @param quoteFeed1 First quote feed. Pass address zero if the price = 1.
7358
/// @param quoteFeed2 Second quote feed. Pass address zero if the price = 1.
59+
/// @param vaultConversionSample The sample amount of vault shares used to convert to the underlying asset.
60+
/// Pass 1 if the oracle does not use a vault. Should be chosen such that converting `vaultConversionSample` to
61+
/// assets has enough precision.
62+
/// @param baseTokenDecimals Base token decimals.
7463
/// @param quoteTokenDecimals Quote token decimals.
7564
constructor(
76-
IERC4626 baseVault,
77-
uint256 baseVaultConversionSample,
65+
IERC4626 vault,
7866
AggregatorV3Interface baseFeed1,
7967
AggregatorV3Interface baseFeed2,
80-
uint256 baseTokenDecimals,
81-
IERC4626 quoteVault,
82-
uint256 quoteVaultConversionSample,
8368
AggregatorV3Interface quoteFeed1,
8469
AggregatorV3Interface quoteFeed2,
70+
uint256 vaultConversionSample,
71+
uint256 baseTokenDecimals,
8572
uint256 quoteTokenDecimals
8673
) {
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-
);
74+
// The ERC4626 vault parameter is used to price `VAULT_CONVERSION_SAMPLE` of its shares, so it requires dividing
75+
// by that number, hence the division by `VAULT_CONVERSION_SAMPLE` in the `SCALE_FACTOR` definition.
76+
// Verify that vault = address(0) => vaultConversionSample = 1.
9577
require(
96-
address(quoteVault) != address(0) || quoteVaultConversionSample == 1,
97-
ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
78+
address(vault) != address(0) || vaultConversionSample == 1, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
9879
);
99-
require(baseVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
100-
require(quoteVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
80+
require(vaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
10181

102-
BASE_VAULT = baseVault;
103-
BASE_VAULT_CONVERSION_SAMPLE = baseVaultConversionSample;
104-
QUOTE_VAULT = quoteVault;
105-
QUOTE_VAULT_CONVERSION_SAMPLE = quoteVaultConversionSample;
82+
VAULT = vault;
83+
VAULT_CONVERSION_SAMPLE = vaultConversionSample;
10684
BASE_FEED_1 = baseFeed1;
10785
BASE_FEED_2 = baseFeed2;
10886
QUOTE_FEED_1 = quoteFeed1;
@@ -137,16 +115,16 @@ contract ChainlinkOracle is IChainlinkOracle {
137115
** (
138116
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals
139117
- baseFeed1.getDecimals() - baseFeed2.getDecimals()
140-
) * quoteVaultConversionSample / baseVaultConversionSample;
118+
) / vaultConversionSample;
141119
}
142120

143121
/* PRICE */
144122

145123
/// @inheritdoc IOracle
146124
function price() external view returns (uint256) {
147125
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()
126+
VAULT.getAssets(VAULT_CONVERSION_SAMPLE) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice(),
127+
QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice()
150128
);
151129
}
152130
}

src/morpho-chainlink-v1/interfaces/IChainlinkOracle.sol

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,11 @@ import {IOracle} from "../../../lib/morpho-blue/src/interfaces/IOracle.sol";
1010
/// @custom:contact [email protected]
1111
/// @notice Interface of ChainlinkOracle.
1212
interface IChainlinkOracle is IOracle {
13-
/// @notice Returns the address of the base ERC4626 vault.
14-
function BASE_VAULT() external view returns (IERC4626);
13+
/// @notice Returns the address of the ERC4626 vault.
14+
function VAULT() external view returns (IERC4626);
1515

16-
/// @notice Returns the base vault conversion sample.
17-
function BASE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
18-
19-
/// @notice Returns the address of the quote ERC4626 vault.
20-
function QUOTE_VAULT() external view returns (IERC4626);
21-
22-
/// @notice Returns the quote vault conversion sample.
23-
function QUOTE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
16+
/// @notice Returns the vault conversion sample.
17+
function VAULT_CONVERSION_SAMPLE() external view returns (uint256);
2418

2519
/// @notice Returns the address of the first Chainlink base feed.
2620
function BASE_FEED_1() external view returns (AggregatorV3Interface);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.5.0;
3+
4+
/// @dev From
5+
/// https://github.com/smartcontractkit/chainlink/blob/master/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol
6+
interface AggregatorV3Interface {
7+
function decimals() external view returns (uint8);
8+
9+
function description() external view returns (string memory);
10+
11+
function version() external view returns (uint256);
12+
13+
function getRoundData(uint80 _roundId)
14+
external
15+
view
16+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
17+
18+
function latestRoundData()
19+
external
20+
view
21+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
22+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity >=0.5.0;
3+
4+
import {IERC4626} from "./IERC4626.sol";
5+
import {AggregatorV3Interface} from "./AggregatorV3Interface.sol";
6+
import {IOracle} from "../../../lib/morpho-blue/src/interfaces/IOracle.sol";
7+
8+
/// @title IChainlinkOracle
9+
/// @author Morpho Labs
10+
/// @custom:contact [email protected]
11+
/// @notice Interface of ChainlinkOracle.
12+
interface IChainlinkOracle is IOracle {
13+
/// @notice Returns the address of the base ERC4626 vault.
14+
function BASE_VAULT() external view returns (IERC4626);
15+
16+
/// @notice Returns the base vault conversion sample.
17+
function BASE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
18+
19+
/// @notice Returns the address of the quote ERC4626 vault.
20+
function QUOTE_VAULT() external view returns (IERC4626);
21+
22+
/// @notice Returns the quote vault conversion sample.
23+
function QUOTE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
24+
25+
/// @notice Returns the address of the first Chainlink base feed.
26+
function BASE_FEED_1() external view returns (AggregatorV3Interface);
27+
28+
/// @notice Returns the address of the second Chainlink base feed.
29+
function BASE_FEED_2() external view returns (AggregatorV3Interface);
30+
31+
/// @notice Returns the address of the first Chainlink quote feed.
32+
function QUOTE_FEED_1() external view returns (AggregatorV3Interface);
33+
34+
/// @notice Returns the address of the second Chainlink quote feed.
35+
function QUOTE_FEED_2() external view returns (AggregatorV3Interface);
36+
37+
/// @notice Returns the price scale factor, calculated at contract creation.
38+
function SCALE_FACTOR() external view returns (uint256);
39+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity >=0.5.0;
3+
4+
interface IERC4626 {
5+
function convertToAssets(uint256) external view returns (uint256);
6+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.0;
3+
4+
import {AggregatorV3Interface} from "../interfaces/AggregatorV3Interface.sol";
5+
6+
import {ErrorsLib} from "./ErrorsLib.sol";
7+
8+
/// @title ChainlinkDataFeedLib
9+
/// @author Morpho Labs
10+
/// @custom:contact [email protected]
11+
/// @notice Library exposing functions to interact with a Chainlink-compliant feed.
12+
library ChainlinkDataFeedLib {
13+
/// @dev Performs safety checks and returns the latest price of a `feed`.
14+
/// @dev When `feed` is the address zero, returns 1.
15+
/// @dev Notes on safety checks:
16+
/// - L2s are not supported.
17+
/// - Staleness is not checked because it's assumed that the Chainlink feed keeps its promises on this.
18+
/// - The price is not checked to be in the min/max bounds because it's assumed that the Chainlink feed keeps its
19+
/// promises on this.
20+
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
21+
if (address(feed) == address(0)) return 1;
22+
23+
(, int256 answer,,,) = feed.latestRoundData();
24+
require(answer >= 0, ErrorsLib.NEGATIVE_ANSWER);
25+
26+
return uint256(answer);
27+
}
28+
29+
/// @dev Returns the number of decimals of a `feed`.
30+
/// @dev When `feed` is the address zero, returns 0.
31+
function getDecimals(AggregatorV3Interface feed) internal view returns (uint256) {
32+
if (address(feed) == address(0)) return 0;
33+
34+
return feed.decimals();
35+
}
36+
}

0 commit comments

Comments
 (0)