Skip to content

Commit f72e33f

Browse files
committed
feat: add factozized option for oracles for ERC4626
1 parent c05a52e commit f72e33f

File tree

4 files changed

+90
-12
lines changed

4 files changed

+90
-12
lines changed

lib/morpho-blue

src/ChainlinkOracle.sol

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ pragma solidity 0.8.19;
44
import {IOracle} from "morpho-blue/interfaces/IOracle.sol";
55

66
import {AggregatorV3Interface, ChainlinkDataFeedLib} from "./libraries/ChainlinkDataFeedLib.sol";
7+
import {ERC4626, VaultDataFeedLib} from "./libraries/VaultDataFeedLib.sol";
78

89
/// @title ChainlinkOracle
910
/// @author Morpho Labs
1011
/// @custom:contact [email protected]
1112
/// @notice Morpho Blue oracle using Chainlink-compliant feeds.
1213
contract ChainlinkOracle is IOracle {
14+
using VaultDataFeedLib for ERC4626;
1315
using ChainlinkDataFeedLib for AggregatorV3Interface;
1416

1517
/* IMMUTABLES */
1618

19+
/// @notice Vault.
20+
ERC4626 public immutable VAULT;
21+
/// @notice Vault decimals.
22+
uint256 public immutable VAULT_DECIMALS;
1723
/// @notice First base feed.
1824
AggregatorV3Interface public immutable BASE_FEED_1;
1925
/// @notice Second base feed.
@@ -27,20 +33,29 @@ contract ChainlinkOracle is IOracle {
2733

2834
/* CONSTRUCTOR */
2935

36+
/// @param vault Vault. Pass address zero to omit this parameter.
3037
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
3138
/// @param baseFeed2 Second base feed. Pass address zero if the price = 1.
3239
/// @param quoteFeed1 First quote feed. Pass address zero if the price = 1.
3340
/// @param quoteFeed2 Second quote feed. Pass address zero if the price = 1.
3441
/// @param baseTokenDecimals Base token decimals.
3542
/// @param quoteTokenDecimals Quote token decimals.
3643
constructor(
44+
ERC4626 vault,
3745
AggregatorV3Interface baseFeed1,
3846
AggregatorV3Interface baseFeed2,
3947
AggregatorV3Interface quoteFeed1,
4048
AggregatorV3Interface quoteFeed2,
4149
uint256 baseTokenDecimals,
4250
uint256 quoteTokenDecimals
4351
) {
52+
VAULT = vault;
53+
// TODO: adapt this
54+
// This scale factor is defined similarly to the scale factor of the ChainlinkOracle, except:
55+
// - the oracle only has one base feed and one quote feed
56+
// - it is used to price a full unit of the vault shares, so it requires dividing by that number, hence the
57+
// `VAULT_DECIMALS` subtraction
58+
VAULT_DECIMALS = VAULT.getDecimals();
4459
BASE_FEED_1 = baseFeed1;
4560
BASE_FEED_2 = baseFeed2;
4661
QUOTE_FEED_1 = quoteFeed1;
@@ -62,15 +77,15 @@ contract ChainlinkOracle is IOracle {
6277
SCALE_FACTOR = 10
6378
** (
6479
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseFeed1.getDecimals()
65-
- baseFeed2.getDecimals() - baseTokenDecimals
80+
- baseFeed2.getDecimals() - baseTokenDecimals - VAULT_DECIMALS
6681
);
6782
}
6883

6984
/* PRICE */
7085

7186
/// @inheritdoc IOracle
7287
function price() external view returns (uint256) {
73-
return (BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR)
88+
return (VAULT.getAssets(10 ** VAULT_DECIMALS) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR)
7489
/ (QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice());
7590
}
7691
}

src/libraries/VaultDataFeedLib.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import {AggregatorV3Interface} from "../interfaces/AggregatorV3Interface.sol";
5+
6+
import {ErrorsLib} from "./ErrorsLib.sol";
7+
8+
interface ERC4626 {
9+
function convertToAssets(uint256) external view returns (uint256);
10+
function decimals() external view returns (uint256);
11+
}
12+
13+
/// @title ChainlinkDataFeedLib
14+
/// @author Morpho Labs
15+
/// @custom:contact [email protected]
16+
/// @notice Library exposing functions to interact with a Chainlink-compliant feed.
17+
library VaultDataFeedLib {
18+
/// @dev Converts `shares` into the corresponding assets on the `vault`.
19+
/// @dev When `vault` is the address zero, returns 1.
20+
function getAssets(ERC4626 vault, uint256 shares) internal view returns (uint256) {
21+
if (address(vault) == address(0)) return 1;
22+
23+
return vault.convertToAssets(shares);
24+
}
25+
26+
/// @dev Returns the number of decimals of a `vault`, seen as an ERC20.
27+
/// @dev When `vault` is the address zero, returns 0.
28+
function getDecimals(ERC4626 vault) internal view returns (uint256) {
29+
if (address(vault) == address(0)) return 0;
30+
31+
return vault.decimals();
32+
}
33+
}

test/ChainlinkOracleTest.sol

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ AggregatorV3Interface constant stEthEthFeed = AggregatorV3Interface(0x86392dC19c
2020
AggregatorV3Interface constant usdcEthFeed = AggregatorV3Interface(0x986b5E1e1755e3C2440e960477f25201B0a8bbD4);
2121
// 8 decimals of precision
2222
AggregatorV3Interface constant ethUsdFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
23+
// 18 decimals of precision
24+
AggregatorV3Interface constant daiEthFeed = AggregatorV3Interface(0x773616E4d11A78F511299002da57A0a94577F1f4);
25+
26+
ERC4626 constant vaultZero = ERC4626(address(0));
27+
ERC4626 constant sDaiVault = ERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA);
2328

2429
contract FakeAggregator {
2530
int256 public answer;
@@ -43,7 +48,7 @@ contract ChainlinkOracleTest is Test {
4348
}
4449

4550
function testOracleWbtcUsdc() public {
46-
ChainlinkOracle oracle = new ChainlinkOracle(wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
51+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
4752
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
4853
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData();
4954
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData();
@@ -55,7 +60,7 @@ contract ChainlinkOracleTest is Test {
5560
}
5661

5762
function testOracleUsdcWbtc() public {
58-
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
63+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
5964
(, int256 baseAnswer,,,) = usdcUsdFeed.latestRoundData();
6065
(, int256 firstQuoteAnswer,,,) = wBtcBtcFeed.latestRoundData();
6166
(, int256 secondQuoteAnswer,,,) = btcUsdFeed.latestRoundData();
@@ -67,51 +72,76 @@ contract ChainlinkOracleTest is Test {
6772
}
6873

6974
function testOracleWbtcEth() public {
70-
ChainlinkOracle oracle = new ChainlinkOracle(wBtcBtcFeed, btcEthFeed, feedZero, feedZero, 8, 18);
75+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero,wBtcBtcFeed, btcEthFeed, feedZero, feedZero, 8, 18);
7176
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
7277
(, int256 secondBaseAnswer,,,) = btcEthFeed.latestRoundData();
7378
assertEq(oracle.price(), (uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 18 - 8 - 8 - 18)));
7479
}
7580

7681
function testOracleStEthUsdc() public {
77-
ChainlinkOracle oracle = new ChainlinkOracle(stEthEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
82+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, stEthEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
7883
(, int256 baseAnswer,,,) = stEthEthFeed.latestRoundData();
7984
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
8085
assertEq(oracle.price(), uint256(baseAnswer) * 10 ** (36 + 18 + 6 - 18 - 18) / uint256(quoteAnswer));
8186
}
8287

8388
function testOracleEthUsd() public {
84-
ChainlinkOracle oracle = new ChainlinkOracle(ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
89+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
8590
(, int256 expectedPrice,,,) = ethUsdFeed.latestRoundData();
8691
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 - 18 - 8));
8792
}
8893

8994
function testOracleStEthEth() public {
90-
ChainlinkOracle oracle = new ChainlinkOracle(stEthEthFeed, feedZero, feedZero, feedZero, 18, 18);
95+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, stEthEthFeed, feedZero, feedZero, feedZero, 18, 18);
9196
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
9297
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18));
9398
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
9499
}
95100

96101
function testOracleEthStEth() public {
97-
ChainlinkOracle oracle = new ChainlinkOracle(feedZero, feedZero, stEthEthFeed, feedZero, 18, 18);
102+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, feedZero, feedZero, stEthEthFeed, feedZero, 18, 18);
98103
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
99104
assertEq(oracle.price(), 10 ** (36 + 18 + 18 - 18) / uint256(expectedPrice));
100105
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
101106
}
102107

103108
function testOracleUsdcUsd() public {
104-
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
109+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
105110
assertApproxEqRel(oracle.price(), 1e36 / 1e6, 0.01 ether);
106111
}
107112

108113
function testNegativeAnswer(int256 price) public {
109114
price = bound(price, type(int256).min, -1);
110115
FakeAggregator aggregator = new FakeAggregator();
111116
ChainlinkOracle oracle =
112-
new ChainlinkOracle(AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
117+
new ChainlinkOracle(vaultZero, AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
113118
aggregator.setAnwser(price);
114119
vm.expectRevert(bytes(ErrorsLib.NEGATIVE_ANSWER));
115120
oracle.price();
116121
}
122+
123+
function testSDaiEthOracle() public {
124+
ChainlinkOracle oracle = new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, feedZero, feedZero, 18, 18);
125+
(, int256 expectedPrice,,,) = daiEthFeed.latestRoundData();
126+
assertEq(
127+
oracle.price(),
128+
sDaiVault.convertToAssets(1e18) * uint256(expectedPrice) * 10 ** (36 + 18 + 0 - 18 - 18 - 18)
129+
);
130+
}
131+
132+
function testSDaiUsdcOracle() public {
133+
ChainlinkOracle oracle = new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
134+
(, int256 baseAnswer,,,) = daiEthFeed.latestRoundData();
135+
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
136+
assertEq(
137+
oracle.price(),
138+
sDaiVault.convertToAssets(1e18) * uint256(baseAnswer) * 10 ** (36 + 6 + 18 - 18 - 18 - 18)
139+
/ uint256(quoteAnswer)
140+
);
141+
// DAI has 12 more decimals than USDC.
142+
uint256 expectedPrice = 10 ** (36 - 12);
143+
// Admit a 50% interest gain before breaking this test.
144+
uint256 deviation = 0.5 ether;
145+
assertApproxEqRel(oracle.price(), expectedPrice, deviation);
146+
}
117147
}

0 commit comments

Comments
 (0)