Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chainlink for ERC4626 factorized #16

Merged
merged 27 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f72e33f
feat: add factozized option for oracles for ERC4626
QGarchery Sep 22, 2023
08373a0
refactor: more coherent file names
QGarchery Oct 9, 2023
fac4ba1
docs: update comments for the refactor
QGarchery Oct 9, 2023
fd65d54
refactor: rename ERC4626 interface
QGarchery Oct 10, 2023
2103045
Merge remote-tracking branch 'origin/feat/chainlink' into feat/chainl…
QGarchery Oct 10, 2023
3617143
feat: allow to pass the vault decimals directly
QGarchery Oct 11, 2023
bc39f3f
refactor: rename vault precision
QGarchery Oct 12, 2023
1d948b2
ci: add formatter
MerlinEgalite Oct 12, 2023
e1345a0
ci: renaming
MerlinEgalite Oct 12, 2023
884ff3f
chore: update submodule url
MerlinEgalite Oct 12, 2023
da7bf03
chore: gpl v2 license
MerlinEgalite Oct 12, 2023
7537430
docs: add in readme
MerlinEgalite Oct 12, 2023
ed0022f
ci: apply suggestins
MerlinEgalite Oct 12, 2023
89da264
chore: remove .git
MerlinEgalite Oct 12, 2023
ea67472
Merge pull request #22 from morpho-labs/chore/update-blue-submodule
MathisGD Oct 12, 2023
ecc618b
chore: update file name
MerlinEgalite Oct 12, 2023
ee391d1
Merge pull request #23 from morpho-labs/chore/correct-license
MathisGD Oct 13, 2023
0fcd315
Merge pull request #21 from morpho-labs/ci/formatter
MathisGD Oct 13, 2023
e6175d5
style: rename vault decimals
QGarchery Oct 13, 2023
4eb24d6
feat: use just the vault sample
QGarchery Oct 14, 2023
fb3c9a5
Merge pull request #27 from morpho-labs/feat/chainlink-lone-sample
MathisGD Oct 15, 2023
741561e
Merge pull request #19 from morpho-labs/feat/chainlink-vault-decimals
MathisGD Oct 15, 2023
3d9d218
fix: prune unused getDecimals
QGarchery Oct 15, 2023
437f44e
feat: verify that vault = 0 => vaultConversionSample = 1
MathisGD Oct 15, 2023
c77b350
fix: error message
MathisGD Oct 16, 2023
896ee33
chore: fmt
MathisGD Oct 16, 2023
4084d69
docs: minor improvements
MathisGD Oct 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/ChainlinkOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ pragma solidity 0.8.19;
import {IOracle} from "morpho-blue/interfaces/IOracle.sol";

import {AggregatorV3Interface, ChainlinkDataFeedLib} from "./libraries/ChainlinkDataFeedLib.sol";
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
import {IERC4626, VaultLib} from "./libraries/VaultLib.sol";

/// @title ChainlinkOracle
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Morpho Blue oracle using Chainlink-compliant feeds.
contract ChainlinkOracle is IOracle {
using VaultLib for IERC4626;
using ChainlinkDataFeedLib for AggregatorV3Interface;

/* IMMUTABLES */

/// @notice Vault.
IERC4626 public immutable VAULT;
/// @notice Vault decimals.
uint256 public immutable VAULT_DECIMALS;
/// @notice First base feed.
AggregatorV3Interface public immutable BASE_FEED_1;
/// @notice Second base feed.
Expand All @@ -27,20 +33,27 @@ contract ChainlinkOracle is IOracle {

/* CONSTRUCTOR */

/// @param vault Vault. Pass address zero to omit this parameter.
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
/// @param baseFeed2 Second base feed. Pass address zero if the price = 1.
/// @param quoteFeed1 First quote feed. Pass address zero if the price = 1.
/// @param quoteFeed2 Second quote feed. Pass address zero if the price = 1.
/// @param baseTokenDecimals Base token decimals.
/// @param quoteTokenDecimals Quote token decimals.
constructor(
IERC4626 vault,
AggregatorV3Interface baseFeed1,
AggregatorV3Interface baseFeed2,
AggregatorV3Interface quoteFeed1,
AggregatorV3Interface quoteFeed2,
uint256 baseTokenDecimals,
uint256 quoteTokenDecimals
) {
// The vault parameter is used for ERC4626 tokens, to price its shares.
// It is used to price a full unit of the vault shares, so it requires dividing by that number, hence the
// `VAULT_DECIMALS` subtraction in the following `SCALE_FACTOR` definition.
VAULT = vault;
VAULT_DECIMALS = VAULT.getDecimals();
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
BASE_FEED_1 = baseFeed1;
BASE_FEED_2 = baseFeed2;
QUOTE_FEED_1 = quoteFeed1;
Expand All @@ -62,15 +75,15 @@ contract ChainlinkOracle is IOracle {
SCALE_FACTOR = 10
** (
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals
- baseFeed1.getDecimals() - baseFeed2.getDecimals()
- baseFeed1.getDecimals() - baseFeed2.getDecimals() - VAULT_DECIMALS
);
}

/* PRICE */

/// @inheritdoc IOracle
function price() external view returns (uint256) {
return (BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR)
return (VAULT.getAssets(10 ** VAULT_DECIMALS) * BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR)
/ (QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice());
}
}
7 changes: 7 additions & 0 deletions src/interfaces/IERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IERC4626 {
function convertToAssets(uint256) external view returns (uint256);
function decimals() external view returns (uint256);
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}
26 changes: 26 additions & 0 deletions src/libraries/VaultLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {IERC4626} from "../interfaces/IERC4626.sol";

/// @title ChainlinkDataFeedLib
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Library exposing functions to price shares of an ERC4626 vault.
library VaultLib {
/// @dev Converts `shares` into the corresponding assets on the `vault`.
/// @dev When `vault` is the address zero, returns 1.
function getAssets(IERC4626 vault, uint256 shares) internal view returns (uint256) {
if (address(vault) == address(0)) return 1;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

return vault.convertToAssets(shares);
}

/// @dev Returns the number of decimals of a `vault`, seen as an ERC20.
/// @dev When `vault` is the address zero, returns 0.
function getDecimals(IERC4626 vault) internal view returns (uint256) {
if (address(vault) == address(0)) return 0;

return vault.decimals();
}
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}
48 changes: 39 additions & 9 deletions test/ChainlinkOracleTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ AggregatorV3Interface constant stEthEthFeed = AggregatorV3Interface(0x86392dC19c
AggregatorV3Interface constant usdcEthFeed = AggregatorV3Interface(0x986b5E1e1755e3C2440e960477f25201B0a8bbD4);
// 8 decimals of precision
AggregatorV3Interface constant ethUsdFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
// 18 decimals of precision
AggregatorV3Interface constant daiEthFeed = AggregatorV3Interface(0x773616E4d11A78F511299002da57A0a94577F1f4);

IERC4626 constant vaultZero = IERC4626(address(0));
IERC4626 constant sDaiVault = IERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA);

contract ChainlinkOracleTest is Test {
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
}

function testOracleWbtcUsdc() public {
ChainlinkOracle oracle = new ChainlinkOracle(wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData();
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData();
Expand All @@ -40,7 +45,7 @@ contract ChainlinkOracleTest is Test {
}

function testOracleUsdcWbtc() public {
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
(, int256 baseAnswer,,,) = usdcUsdFeed.latestRoundData();
(, int256 firstQuoteAnswer,,,) = wBtcBtcFeed.latestRoundData();
(, int256 secondQuoteAnswer,,,) = btcUsdFeed.latestRoundData();
Expand All @@ -52,51 +57,76 @@ contract ChainlinkOracleTest is Test {
}

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

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

function testOracleEthUsd() public {
ChainlinkOracle oracle = new ChainlinkOracle(ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
(, int256 expectedPrice,,,) = ethUsdFeed.latestRoundData();
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 - 18 - 8));
}

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

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

function testOracleUsdcUsd() public {
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
assertApproxEqRel(oracle.price(), 1e36 / 1e6, 0.01 ether);
}

function testNegativeAnswer(int256 price) public {
price = bound(price, type(int256).min, -1);
ChainlinkAggregatorMock aggregator = new ChainlinkAggregatorMock();
ChainlinkOracle oracle =
new ChainlinkOracle(AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
new ChainlinkOracle(vaultZero, AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
aggregator.setAnwser(price);
vm.expectRevert(bytes(ErrorsLib.NEGATIVE_ANSWER));
oracle.price();
}

function testSDaiEthOracle() public {
ChainlinkOracle oracle = new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, feedZero, feedZero, 18, 18);
(, int256 expectedPrice,,,) = daiEthFeed.latestRoundData();
assertEq(
oracle.price(),
sDaiVault.convertToAssets(1e18) * uint256(expectedPrice) * 10 ** (36 + 18 + 0 - 18 - 18 - 18)
);
}

function testSDaiUsdcOracle() public {
ChainlinkOracle oracle = new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
(, int256 baseAnswer,,,) = daiEthFeed.latestRoundData();
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
assertEq(
oracle.price(),
sDaiVault.convertToAssets(1e18) * uint256(baseAnswer) * 10 ** (36 + 6 + 18 - 18 - 18 - 18)
/ uint256(quoteAnswer)
);
// DAI has 12 more decimals than USDC.
uint256 expectedPrice = 10 ** (36 - 12);
// Admit a 50% interest gain before breaking this test.
uint256 deviation = 0.5 ether;
assertApproxEqRel(oracle.price(), expectedPrice, deviation);
}
}