Skip to content

Commit 35d7992

Browse files
committed
Merge branch 'feat/chainlink' of github.com:morpho-labs/morpho-blue-oracles into merlin-review
2 parents 7abc4f2 + 0007653 commit 35d7992

12 files changed

+204
-259
lines changed

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
[submodule "lib/forge-std"]
22
path = lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4-
[submodule "lib/chainlink"]
5-
path = lib/chainlink
6-
url = https://github.com/smartcontractkit/chainlink
74
[submodule "lib/morpho-blue"]
85
path = lib/morpho-blue
96
url = https://github.com/morpho-labs/morpho-blue

lib/chainlink

Lines changed: 0 additions & 1 deletion
This file was deleted.

lib/morpho-blue

src/ChainlinkOracle.sol

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.19;
3+
4+
import {IOracle} from "morpho-blue/interfaces/IOracle.sol";
5+
6+
import {AggregatorV3Interface, ChainlinkDataFeedLib} from "./libraries/ChainlinkDataFeedLib.sol";
7+
8+
contract ChainlinkOracle is IOracle {
9+
using ChainlinkDataFeedLib for AggregatorV3Interface;
10+
11+
/* CONSTANT */
12+
13+
/// @notice First base feed.
14+
AggregatorV3Interface public immutable BASE_FEED_1;
15+
/// @notice Second base feed.
16+
AggregatorV3Interface public immutable BASE_FEED_2;
17+
/// @notice First quote feed.
18+
AggregatorV3Interface public immutable QUOTE_FEED_1;
19+
/// @notice Second quote feed.
20+
AggregatorV3Interface public immutable QUOTE_FEED_2;
21+
/// @notice Price scale factor, computed at contract creation.
22+
uint256 public immutable SCALE_FACTOR;
23+
24+
/* CONSTRUCTOR */
25+
26+
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
27+
/// @param baseFeed2 Second base feed. Pass address zero if the price = 1.
28+
/// @param quoteFeed1 First quote feed. Pass address zero if the price = 1.
29+
/// @param quoteFeed2 Second quote feed. Pass address zero if the price = 1.
30+
/// @param baseTokenDecimals Base token decimals.
31+
/// @param quoteTokenDecimals Quote token decimals.
32+
constructor(
33+
AggregatorV3Interface baseFeed1,
34+
AggregatorV3Interface baseFeed2,
35+
AggregatorV3Interface quoteFeed1,
36+
AggregatorV3Interface quoteFeed2,
37+
uint256 baseTokenDecimals,
38+
uint256 quoteTokenDecimals
39+
) {
40+
BASE_FEED_1 = baseFeed1;
41+
BASE_FEED_2 = baseFeed2;
42+
QUOTE_FEED_1 = quoteFeed1;
43+
QUOTE_FEED_2 = quoteFeed2;
44+
SCALE_FACTOR = 10
45+
** (
46+
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseFeed1.getDecimals()
47+
- baseFeed2.getDecimals() - baseTokenDecimals
48+
);
49+
}
50+
51+
/* PRICE */
52+
53+
/// @inheritdoc IOracle
54+
function price() external view returns (uint256) {
55+
return (BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR)
56+
/ (QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice());
57+
}
58+
}

src/chainlink/OracleFourFeeds.sol

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/chainlink/OracleTwoFeeds.sol

Lines changed: 0 additions & 55 deletions
This file was deleted.
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.8.0;
3+
4+
/// @dev From
5+
/// https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/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+
}

src/chainlink/libraries/DataFeedLib.sol renamed to src/libraries/ChainlinkDataFeedLib.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.0;
33

4-
import {AggregatorV3Interface} from "chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
4+
import {AggregatorV3Interface} from "../interfaces/AggregatorV3Interface.sol";
55

66
import {ErrorsLib} from "./ErrorsLib.sol";
77

8-
/// @title DataFeedLib
8+
/// @title ChainlinkDataFeedLib
99
/// @author Morpho Labs
1010
/// @custom:contact [email protected]
1111
/// @notice Library exposing functions to interact with a Chainlink-compliant feed.
12-
library DataFeedLib {
12+
library ChainlinkDataFeedLib {
1313
/// @dev Returns the latest price of a `feed`.
14-
/// @dev Performing some security checks and returns the latest price of a feed.
14+
/// @dev Performs some safety checks and returns the latest price of a feed.
1515
/// @dev When `feed` is the address zero, returns 1.
1616
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
1717
if (address(feed) == address(0)) return 1;
@@ -20,7 +20,8 @@ library DataFeedLib {
2020
return uint256(answer);
2121
}
2222

23-
/// @dev Returns `feed.decimals()` when `feed` is not the address zero, else returns 0.
23+
/// @dev Returns the number of decimals of a feed.
24+
/// @dev When `feed` is the address zero, returns 0.
2425
function getDecimals(AggregatorV3Interface feed) internal view returns (uint256) {
2526
if (address(feed) == address(0)) return 0;
2627
return feed.decimals();
File renamed without changes.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import "forge-std/Test.sol";
5+
import "src/ChainlinkOracle.sol";
6+
import "src/libraries/ErrorsLib.sol";
7+
8+
AggregatorV3Interface constant feedZero = AggregatorV3Interface(address(0));
9+
// 8 decimals of precision
10+
AggregatorV3Interface constant btcUsdFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c);
11+
// 8 decimals of precision
12+
AggregatorV3Interface constant usdcUsdFeed = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6);
13+
// 18 decimals of precision
14+
AggregatorV3Interface constant btcEthFeed = AggregatorV3Interface(0xdeb288F737066589598e9214E782fa5A8eD689e8);
15+
// 8 decimals of precision
16+
AggregatorV3Interface constant wBtcBtcFeed = AggregatorV3Interface(0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23);
17+
// 18 decimals of precision
18+
AggregatorV3Interface constant stEthEthFeed = AggregatorV3Interface(0x86392dC19c0b719886221c78AB11eb8Cf5c52812);
19+
// 18 decimals of precision
20+
AggregatorV3Interface constant usdcEthFeed = AggregatorV3Interface(0x986b5E1e1755e3C2440e960477f25201B0a8bbD4);
21+
// 8 decimals of precision
22+
AggregatorV3Interface constant ethUsdFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
23+
24+
contract FakeAggregator {
25+
int256 public answer;
26+
27+
function setAnwser(int256 newAnswer) external {
28+
answer = newAnswer;
29+
}
30+
31+
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
32+
return (0, answer, 0, 0, 0);
33+
}
34+
35+
function decimals() external pure returns (uint256) {
36+
return 8;
37+
}
38+
}
39+
40+
contract ChainlinkOracleTest is Test {
41+
function setUp() public {
42+
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
43+
}
44+
45+
function testOracleWbtcUsdc() public {
46+
ChainlinkOracle oracle = new ChainlinkOracle(wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 8, 6);
47+
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
48+
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData();
49+
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData();
50+
assertEq(
51+
oracle.price(),
52+
(uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 8 + 6 - 8 - 8 - 8))
53+
/ uint256(quoteAnswer)
54+
);
55+
}
56+
57+
function testOracleUsdcWbtc() public {
58+
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8);
59+
(, int256 baseAnswer,,,) = usdcUsdFeed.latestRoundData();
60+
(, int256 firstQuoteAnswer,,,) = wBtcBtcFeed.latestRoundData();
61+
(, int256 secondQuoteAnswer,,,) = btcUsdFeed.latestRoundData();
62+
assertEq(
63+
oracle.price(),
64+
(uint256(baseAnswer) * 10 ** (36 + 8 + 8 + 8 - 6 - 8))
65+
/ (uint256(firstQuoteAnswer) * uint256(secondQuoteAnswer))
66+
);
67+
}
68+
69+
function testOracleWbtcEth() public {
70+
ChainlinkOracle oracle = new ChainlinkOracle(wBtcBtcFeed, btcEthFeed, feedZero, feedZero, 8, 18);
71+
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
72+
(, int256 secondBaseAnswer,,,) = btcEthFeed.latestRoundData();
73+
assertEq(oracle.price(), (uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 18 - 8 - 8 - 18)));
74+
}
75+
76+
function testOracleStEthUsdc() public {
77+
ChainlinkOracle oracle = new ChainlinkOracle(stEthEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6);
78+
(, int256 baseAnswer,,,) = stEthEthFeed.latestRoundData();
79+
(, int256 quoteAnswer,,,) = usdcEthFeed.latestRoundData();
80+
assertEq(oracle.price(), uint256(baseAnswer) * 10 ** (36 + 18 + 6 - 18 - 18) / uint256(quoteAnswer));
81+
}
82+
83+
function testOracleEthUsd() public {
84+
ChainlinkOracle oracle = new ChainlinkOracle(ethUsdFeed, feedZero, feedZero, feedZero, 18, 0);
85+
(, int256 expectedPrice,,,) = ethUsdFeed.latestRoundData();
86+
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 - 18 - 8));
87+
}
88+
89+
function testOracleStEthEth() public {
90+
ChainlinkOracle oracle = new ChainlinkOracle(stEthEthFeed, feedZero, feedZero, feedZero, 18, 18);
91+
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
92+
assertEq(oracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18));
93+
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
94+
}
95+
96+
function testOracleEthStEth() public {
97+
ChainlinkOracle oracle = new ChainlinkOracle(feedZero, feedZero, stEthEthFeed, feedZero, 18, 18);
98+
(, int256 expectedPrice,,,) = stEthEthFeed.latestRoundData();
99+
assertEq(oracle.price(), 10 ** (36 + 18 + 18 - 18) / uint256(expectedPrice));
100+
assertApproxEqRel(oracle.price(), 1e36, 0.01 ether);
101+
}
102+
103+
function testOracleUsdcUsd() public {
104+
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, feedZero, feedZero, 6, 0);
105+
assertApproxEqRel(oracle.price(), 1e36 / 1e6, 0.01 ether);
106+
}
107+
108+
function testNegativeAnswer(int256 price) public {
109+
price = bound(price, type(int256).min, -1);
110+
FakeAggregator aggregator = new FakeAggregator();
111+
ChainlinkOracle oracle =
112+
new ChainlinkOracle(AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0);
113+
aggregator.setAnwser(price);
114+
vm.expectRevert(bytes(ErrorsLib.NEGATIVE_ANSWER));
115+
oracle.price();
116+
}
117+
}

0 commit comments

Comments
 (0)