Skip to content

Commit c183a26

Browse files
committed
feat: vault as loan asset
1 parent 5bca61d commit c183a26

File tree

3 files changed

+63
-34
lines changed

3 files changed

+63
-34
lines changed

src/ChainlinkOracle.sol

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

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

2626
/// @inheritdoc IChainlinkOracle
27-
uint256 public immutable VAULT_CONVERSION_SAMPLE;
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;
2834

2935
/// @inheritdoc IChainlinkOracle
3036
AggregatorV3Interface public immutable BASE_FEED_1;
@@ -51,36 +57,51 @@ contract ChainlinkOracle is IChainlinkOracle {
5157
/// - The vault's sample shares quoted as assets and the base feed prices don't overflow when multiplied.
5258
/// - The quote feed prices don't overflow when multiplied.
5359
/// - The vault, if set, is ERC4626-compliant.
54-
/// @param vault Vault. Pass address zero to omit this parameter.
60+
/// @param baseVault Base vault. Pass address zero to omit this parameter.
61+
/// @param baseVaultConversionSample The sample amount of base vault shares used to convert to the underlying asset.
62+
/// Pass 1 if the oracle does not use a vault. Should be chosen such that converting `vaultConversionSample` to
63+
/// assets has enough precision.
64+
/// @param quoteVault Quote vault. Pass address zero to omit this parameter.
65+
/// @param quoteVaultConversionSample The sample amount of quote vault shares used to convert to the underlying
66+
/// asset.
67+
/// Pass 1 if the oracle does not use a vault. Should be chosen such that converting `vaultConversionSample` to
68+
/// assets has enough precision.
5569
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
5670
/// @param baseFeed2 Second base feed. Pass address zero if the price = 1.
5771
/// @param quoteFeed1 First quote feed. Pass address zero if the price = 1.
5872
/// @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.
6273
/// @param baseTokenDecimals Base token decimals.
6374
/// @param quoteTokenDecimals Quote token decimals.
6475
constructor(
65-
IERC4626 vault,
76+
IERC4626 baseVault,
77+
uint256 baseVaultConversionSample,
78+
IERC4626 quoteVault,
79+
uint256 quoteVaultConversionSample,
6680
AggregatorV3Interface baseFeed1,
6781
AggregatorV3Interface baseFeed2,
6882
AggregatorV3Interface quoteFeed1,
6983
AggregatorV3Interface quoteFeed2,
70-
uint256 vaultConversionSample,
7184
uint256 baseTokenDecimals,
7285
uint256 quoteTokenDecimals
7386
) {
7487
// The ERC4626 vault parameter is used to price `VAULT_CONVERSION_SAMPLE` of its shares, so it requires dividing
7588
// by that number, hence the division by `VAULT_CONVERSION_SAMPLE` in the `SCALE_FACTOR` definition.
76-
// Verify that vault = address(0) => vaultConversionSample = 1.
89+
// Verify that vault = address(0) => vaultConversionSample = 1 for each vault.
90+
require(
91+
address(baseVault) != address(0) || baseVaultConversionSample == 1,
92+
ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
93+
);
94+
require(baseVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
7795
require(
78-
address(vault) != address(0) || vaultConversionSample == 1, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
96+
address(quoteVault) != address(0) || quoteVaultConversionSample == 1,
97+
ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
7998
);
80-
require(vaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
99+
require(quoteVaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
81100

82-
VAULT = vault;
83-
VAULT_CONVERSION_SAMPLE = vaultConversionSample;
101+
BASE_VAULT = baseVault;
102+
BASE_VAULT_CONVERSION_SAMPLE = baseVaultConversionSample;
103+
QUOTE_VAULT = quoteVault;
104+
QUOTE_VAULT_CONVERSION_SAMPLE = quoteVaultConversionSample;
84105
BASE_FEED_1 = baseFeed1;
85106
BASE_FEED_2 = baseFeed2;
86107
QUOTE_FEED_1 = quoteFeed1;
@@ -115,16 +136,16 @@ contract ChainlinkOracle is IChainlinkOracle {
115136
** (
116137
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals
117138
- baseFeed1.getDecimals() - baseFeed2.getDecimals()
118-
) / vaultConversionSample;
139+
) * quoteVaultConversionSample / baseVaultConversionSample;
119140
}
120141

121142
/* PRICE */
122143

123144
/// @inheritdoc IOracle
124145
function price() external view returns (uint256) {
125146
return SCALE_FACTOR.mulDiv(
126-
VAULT.getAssets(VAULT_CONVERSION_SAMPLE) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice(),
127-
QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice()
147+
BASE_VAULT.getAssets(BASE_VAULT_CONVERSION_SAMPLE) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice(),
148+
BASE_VAULT.getAssets(QUOTE_VAULT_CONVERSION_SAMPLE) * QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice()
128149
);
129150
}
130151
}

src/interfaces/IChainlinkOracle.sol

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ 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 ERC4626 vault.
14-
function VAULT() external view returns (IERC4626);
13+
/// @notice Returns the address of the base ERC4626 vault.
14+
function BASE_VAULT() external view returns (IERC4626);
1515

1616
/// @notice Returns the vault conversion sample.
17-
function VAULT_CONVERSION_SAMPLE() external view returns (uint256);
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 vault conversion sample.
23+
function QUOTE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
1824

1925
/// @notice Returns the address of the first Chainlink base feed.
2026
function BASE_FEED_1() external view returns (AggregatorV3Interface);

test/ChainlinkOracleTest.sol

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ contract ChainlinkOracleTest is Test {
3333
}
3434

3535
function testOracleWbtcUsdc() public {
36-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 1, 8, 6);
36+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
3737
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
3838
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData();
3939
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData();
@@ -45,7 +45,7 @@ contract ChainlinkOracleTest is Test {
4545
}
4646

4747
function testOracleUsdcWbtc() public {
48-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 1, 6, 8);
48+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
4949
(, int256 baseAnswer,,,) = usdcUsdFeed.latestRoundData();
5050
(, int256 firstQuoteAnswer,,,) = wBtcBtcFeed.latestRoundData();
5151
(, int256 secondQuoteAnswer,,,) = btcUsdFeed.latestRoundData();
@@ -57,58 +57,56 @@ contract ChainlinkOracleTest is Test {
5757
}
5858

5959
function testOracleWbtcEth() public {
60-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, wBtcBtcFeed, btcEthFeed, feedZero, feedZero, 1, 8, 18);
60+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, wBtcBtcFeed, btcEthFeed, feedZero, feedZero, 8, 18);
6161
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
6262
(, int256 secondBaseAnswer,,,) = btcEthFeed.latestRoundData();
6363
assertEq(oracle.price(), (uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 18 - 8 - 8 - 18)));
6464
}
6565

6666
function testOracleStEthUsdc() public {
67-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, stEthEthFeed, feedZero, usdcEthFeed, feedZero, 1, 18, 6);
67+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, stEthEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
6868
(, int256 baseAnswer,,,) = stEthEthFeed.latestRoundData();
6969
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
7070
assertEq(oracle.price(), uint256(baseAnswer) * 10 ** (36 + 18 + 6 - 18 - 18) / uint256(quoteAnswer));
7171
}
7272

7373
function testOracleEthUsd() public {
74-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, ethUsdFeed, feedZero, feedZero, feedZero, 1, 18, 0);
74+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
7575
(, int256 expectedPrice,,,) = ethUsdFeed.latestRoundData();
7676
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 - 18 - 8));
7777
}
7878

7979
function testOracleStEthEth() public {
80-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, stEthEthFeed, feedZero, feedZero, feedZero, 1, 18, 18);
80+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, stEthEthFeed, feedZero, feedZero, feedZero, 18, 18);
8181
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
8282
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18));
8383
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
8484
}
8585

8686
function testOracleEthStEth() public {
87-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, feedZero, feedZero, stEthEthFeed, feedZero, 1, 18, 18);
87+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, feedZero, feedZero, stEthEthFeed, feedZero, 18, 18);
8888
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
8989
assertEq(oracle.price(), 10 ** (36 + 18 + 18 - 18) / uint256(expectedPrice));
9090
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
9191
}
9292

9393
function testOracleUsdcUsd() public {
94-
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, feedZero, feedZero, 1, 6, 0);
94+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
9595
assertApproxEqRel(oracle.price(), 1e36 / 1e6, 0.01 ether);
9696
}
9797

9898
function testNegativeAnswer(int256 price) public {
9999
price = bound(price, type(int256).min, -1);
100100
ChainlinkAggregatorMock aggregator = new ChainlinkAggregatorMock();
101-
ChainlinkOracle oracle = new ChainlinkOracle(
102-
vaultZero, AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 1, 18, 0
103-
);
101+
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, 1, vaultZero, 1, AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
104102
aggregator.setAnwser(price);
105103
vm.expectRevert(bytes(ErrorsLib.NEGATIVE_ANSWER));
106104
oracle.price();
107105
}
108106

109107
function testSDaiEthOracle() public {
110108
ChainlinkOracle oracle =
111-
new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, feedZero, feedZero, 10 ** 18, 18, 18);
109+
new ChainlinkOracle(sDaiVault, 10 ** 18, vaultZero, 1, daiEthFeed, feedZero, feedZero, feedZero, 18, 18);
112110
(, int256 expectedPrice,,,) = daiEthFeed.latestRoundData();
113111
assertEq(
114112
oracle.price(),
@@ -118,7 +116,7 @@ contract ChainlinkOracleTest is Test {
118116

119117
function testSDaiUsdcOracle() public {
120118
ChainlinkOracle oracle =
121-
new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, usdcEthFeed, feedZero, 10 ** 18, 18, 6);
119+
new ChainlinkOracle(sDaiVault, 10 ** 18, vaultZero, 1, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
122120
(, int256 baseAnswer,,,) = daiEthFeed.latestRoundData();
123121
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
124122
assertEq(
@@ -135,13 +133,17 @@ contract ChainlinkOracleTest is Test {
135133

136134
function testConstructorZeroVaultConversionSample() public {
137135
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO));
138-
new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, usdcEthFeed, feedZero, 0, 18, 6);
136+
new ChainlinkOracle(sDaiVault, 0, vaultZero, 1, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
137+
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO));
138+
new ChainlinkOracle(vaultZero, 1, sDaiVault, 0, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
139139
}
140140

141141
function testConstructorVaultZeroNonOneSample(uint256 vaultConversionSample) public {
142142
vaultConversionSample = bound(vaultConversionSample, 2, type(uint256).max);
143143

144144
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE));
145-
new ChainlinkOracle(vaultZero, daiEthFeed, feedZero, usdcEthFeed, feedZero, vaultConversionSample, 18, 6);
145+
new ChainlinkOracle(vaultZero, 0, vaultZero, 1, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
146+
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE));
147+
new ChainlinkOracle(vaultZero, 1, vaultZero, 0, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
146148
}
147149
}

0 commit comments

Comments
 (0)