-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/chainlink' of github.com:morpho-labs/morpho-blue-o…
…racles into merlin-review
- Loading branch information
Showing
12 changed files
with
204 additions
and
259 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,6 @@ | ||
[submodule "lib/forge-std"] | ||
path = lib/forge-std | ||
url = https://github.com/foundry-rs/forge-std | ||
[submodule "lib/chainlink"] | ||
path = lib/chainlink | ||
url = https://github.com/smartcontractkit/chainlink | ||
[submodule "lib/morpho-blue"] | ||
path = lib/morpho-blue | ||
url = https://github.com/morpho-labs/morpho-blue |
Submodule chainlink
deleted from
6163d1
Submodule morpho-blue
updated
from 7d60ca to fa7e34
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.19; | ||
|
||
import {IOracle} from "morpho-blue/interfaces/IOracle.sol"; | ||
|
||
import {AggregatorV3Interface, ChainlinkDataFeedLib} from "./libraries/ChainlinkDataFeedLib.sol"; | ||
|
||
contract ChainlinkOracle is IOracle { | ||
using ChainlinkDataFeedLib for AggregatorV3Interface; | ||
|
||
/* CONSTANT */ | ||
|
||
/// @notice First base feed. | ||
AggregatorV3Interface public immutable BASE_FEED_1; | ||
/// @notice Second base feed. | ||
AggregatorV3Interface public immutable BASE_FEED_2; | ||
/// @notice First quote feed. | ||
AggregatorV3Interface public immutable QUOTE_FEED_1; | ||
/// @notice Second quote feed. | ||
AggregatorV3Interface public immutable QUOTE_FEED_2; | ||
/// @notice Price scale factor, computed at contract creation. | ||
uint256 public immutable SCALE_FACTOR; | ||
|
||
/* CONSTRUCTOR */ | ||
|
||
/// @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( | ||
AggregatorV3Interface baseFeed1, | ||
AggregatorV3Interface baseFeed2, | ||
AggregatorV3Interface quoteFeed1, | ||
AggregatorV3Interface quoteFeed2, | ||
uint256 baseTokenDecimals, | ||
uint256 quoteTokenDecimals | ||
) { | ||
BASE_FEED_1 = baseFeed1; | ||
BASE_FEED_2 = baseFeed2; | ||
QUOTE_FEED_1 = quoteFeed1; | ||
QUOTE_FEED_2 = quoteFeed2; | ||
SCALE_FACTOR = 10 | ||
** ( | ||
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseFeed1.getDecimals() | ||
- baseFeed2.getDecimals() - baseTokenDecimals | ||
); | ||
} | ||
|
||
/* PRICE */ | ||
|
||
/// @inheritdoc IOracle | ||
function price() external view returns (uint256) { | ||
return (BASE_FEED_1.getPrice() * BASE_FEED_2.getPrice() * SCALE_FACTOR) | ||
/ (QUOTE_FEED_1.getPrice() * QUOTE_FEED_2.getPrice()); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
/// @dev From | ||
/// https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol. | ||
interface AggregatorV3Interface { | ||
function decimals() external view returns (uint8); | ||
|
||
function description() external view returns (string memory); | ||
|
||
function version() external view returns (uint256); | ||
|
||
function getRoundData(uint80 _roundId) | ||
external | ||
view | ||
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); | ||
|
||
function latestRoundData() | ||
external | ||
view | ||
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,17 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.0; | ||
|
||
import {AggregatorV3Interface} from "chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; | ||
import {AggregatorV3Interface} from "../interfaces/AggregatorV3Interface.sol"; | ||
|
||
import {ErrorsLib} from "./ErrorsLib.sol"; | ||
|
||
/// @title DataFeedLib | ||
/// @title ChainlinkDataFeedLib | ||
/// @author Morpho Labs | ||
/// @custom:contact [email protected] | ||
/// @notice Library exposing functions to interact with a Chainlink-compliant feed. | ||
library DataFeedLib { | ||
library ChainlinkDataFeedLib { | ||
/// @dev Returns the latest price of a `feed`. | ||
/// @dev Performing some security checks and returns the latest price of a feed. | ||
/// @dev Performs some safety checks and returns the latest price of a feed. | ||
/// @dev When `feed` is the address zero, returns 1. | ||
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) { | ||
if (address(feed) == address(0)) return 1; | ||
|
@@ -20,7 +20,8 @@ library DataFeedLib { | |
return uint256(answer); | ||
} | ||
|
||
/// @dev Returns `feed.decimals()` when `feed` is not the address zero, else returns 0. | ||
/// @dev Returns the number of decimals of a feed. | ||
/// @dev When `feed` is the address zero, returns 0. | ||
function getDecimals(AggregatorV3Interface feed) internal view returns (uint256) { | ||
if (address(feed) == address(0)) return 0; | ||
return feed.decimals(); | ||
|
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.0; | ||
|
||
import "forge-std/Test.sol"; | ||
import "src/ChainlinkOracle.sol"; | ||
import "src/libraries/ErrorsLib.sol"; | ||
|
||
AggregatorV3Interface constant feedZero = AggregatorV3Interface(address(0)); | ||
// 8 decimals of precision | ||
AggregatorV3Interface constant btcUsdFeed = AggregatorV3Interface(0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c); | ||
// 8 decimals of precision | ||
AggregatorV3Interface constant usdcUsdFeed = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); | ||
// 18 decimals of precision | ||
AggregatorV3Interface constant btcEthFeed = AggregatorV3Interface(0xdeb288F737066589598e9214E782fa5A8eD689e8); | ||
// 8 decimals of precision | ||
AggregatorV3Interface constant wBtcBtcFeed = AggregatorV3Interface(0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23); | ||
// 18 decimals of precision | ||
AggregatorV3Interface constant stEthEthFeed = AggregatorV3Interface(0x86392dC19c0b719886221c78AB11eb8Cf5c52812); | ||
// 18 decimals of precision | ||
AggregatorV3Interface constant usdcEthFeed = AggregatorV3Interface(0x986b5E1e1755e3C2440e960477f25201B0a8bbD4); | ||
// 8 decimals of precision | ||
AggregatorV3Interface constant ethUsdFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); | ||
|
||
contract FakeAggregator { | ||
int256 public answer; | ||
|
||
function setAnwser(int256 newAnswer) external { | ||
answer = newAnswer; | ||
} | ||
|
||
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { | ||
return (0, answer, 0, 0, 0); | ||
} | ||
|
||
function decimals() external pure returns (uint256) { | ||
return 8; | ||
} | ||
} | ||
|
||
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); | ||
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData(); | ||
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData(); | ||
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData(); | ||
assertEq( | ||
oracle.price(), | ||
(uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 8 + 6 - 8 - 8 - 8)) | ||
/ uint256(quoteAnswer) | ||
); | ||
} | ||
|
||
function testOracleUsdcWbtc() public { | ||
ChainlinkOracle oracle = new ChainlinkOracle(usdcUsdFeed, feedZero, wBtcBtcFeed, btcUsdFeed, 6, 8); | ||
(, int256 baseAnswer,,,) = usdcUsdFeed.latestRoundData(); | ||
(, int256 firstQuoteAnswer,,,) = wBtcBtcFeed.latestRoundData(); | ||
(, int256 secondQuoteAnswer,,,) = btcUsdFeed.latestRoundData(); | ||
assertEq( | ||
oracle.price(), | ||
(uint256(baseAnswer) * 10 ** (36 + 8 + 8 + 8 - 6 - 8)) | ||
/ (uint256(firstQuoteAnswer) * uint256(secondQuoteAnswer)) | ||
); | ||
} | ||
|
||
function testOracleWbtcEth() public { | ||
ChainlinkOracle oracle = new ChainlinkOracle(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); | ||
(, 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); | ||
(, 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); | ||
(, 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); | ||
(, 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); | ||
assertApproxEqRel(oracle.price(), 1e36 / 1e6, 0.01 ether); | ||
} | ||
|
||
function testNegativeAnswer(int256 price) public { | ||
price = bound(price, type(int256).min, -1); | ||
FakeAggregator aggregator = new FakeAggregator(); | ||
ChainlinkOracle oracle = | ||
new ChainlinkOracle(AggregatorV3Interface(address(aggregator)), feedZero, feedZero, feedZero, 18, 0); | ||
aggregator.setAnwser(price); | ||
vm.expectRevert(bytes(ErrorsLib.NEGATIVE_ANSWER)); | ||
oracle.price(); | ||
} | ||
} |
Oops, something went wrong.