From c84e84b5c181ef20a0606e1bcb039614af81b32e Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 16 Feb 2024 19:39:34 +0100 Subject: [PATCH] feat: add factory --- src/ChainlinkOracleFactory.sol | 111 +++++++++++++++++++++ src/interfaces/IChainlinkOracleFactory.sol | 52 ++++++++++ test/ChainlinkOracleFactoryTest.sol | 94 +++++++++++++++++ test/ChainlinkOracleTest.sol | 34 +------ test/helpers/BaseTest.sol | 34 +++++++ 5 files changed, 294 insertions(+), 31 deletions(-) create mode 100644 src/ChainlinkOracleFactory.sol create mode 100644 src/interfaces/IChainlinkOracleFactory.sol create mode 100644 test/ChainlinkOracleFactoryTest.sol create mode 100644 test/helpers/BaseTest.sol diff --git a/src/ChainlinkOracleFactory.sol b/src/ChainlinkOracleFactory.sol new file mode 100644 index 0000000..f8607eb --- /dev/null +++ b/src/ChainlinkOracleFactory.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.21; + +import {IChainlinkOracle} from "./interfaces/IChainlinkOracle.sol"; +import {IChainlinkOracleFactory} from "./interfaces/IChainlinkOracleFactory.sol"; +import {AggregatorV3Interface} from "./libraries/ChainlinkDataFeedLib.sol"; +import {IERC4626} from "./libraries/VaultLib.sol"; + +import {ChainlinkOracle} from "./ChainlinkOracle.sol"; + +/// @title ChainlinkOracleFactory +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice This contract allows to create Chainlink oracles, and to index them easily. +contract ChainlinkOracleFactory is IChainlinkOracleFactory { + /// @notice Emitted when a new Chainlink oracle is created. + /// @param oracle The address of the Chainlink oracle. + /// @param caller The caller of the function. + /// @param baseVault Base vault. + /// @param baseVaultConversionSample The sample amount of base vault shares used to convert to underlying. + /// @param quoteVault Quote vault. + /// @param quoteVaultConversionSample The sample amount of quote vault shares used to convert to underlying. + /// @param salt The salt used for the MetaMorpho vault's CREATE2 address. + event CreateChainlinkOracle( + address oracle, + address caller, + address baseVault, + uint256 baseVaultConversionSample, + address quoteVault, + uint256 quoteVaultConversionSample, + bytes32 salt + ); + + /// @notice Emitted when a new Chainlink oracle is created. + /// @param oracle The address of the Chainlink oracle. + /// @param baseFeed1 First base feed. + /// @param baseFeed2 Second base feed. + /// @param quoteFeed1 First quote feed. + /// @param quoteFeed2 Second quote feed. + /// @param baseTokenDecimals Base token decimals. + /// @param quoteTokenDecimals Quote token decimals. + event CreateChainlinkOracleFeeds( + address oracle, + address baseFeed1, + address baseFeed2, + address quoteFeed1, + address quoteFeed2, + uint256 baseTokenDecimals, + uint256 quoteTokenDecimals + ); + + /* STORAGE */ + + /// @inheritdoc IChainlinkOracleFactory + mapping(address => bool) public isChainlinkOracle; + + /* EXTERNAL */ + + /// @inheritdoc IChainlinkOracleFactory + function createChainlinkOracle( + IERC4626 baseVault, + uint256 baseVaultConversionSample, + IERC4626 quoteVault, + uint256 quoteVaultConversionSample, + AggregatorV3Interface baseFeed1, + AggregatorV3Interface baseFeed2, + AggregatorV3Interface quoteFeed1, + AggregatorV3Interface quoteFeed2, + uint256 baseTokenDecimals, + uint256 quoteTokenDecimals, + bytes32 salt + ) external returns (IChainlinkOracle oracle) { + oracle = IChainlinkOracle( + address( + new ChainlinkOracle{salt: salt}( + baseVault, + baseVaultConversionSample, + quoteVault, + quoteVaultConversionSample, + baseFeed1, + baseFeed2, + quoteFeed1, + quoteFeed2, + baseTokenDecimals, + quoteTokenDecimals + ) + ) + ); + + isChainlinkOracle[address(oracle)] = true; + + emit CreateChainlinkOracle( + address(oracle), + msg.sender, + address(baseVault), + baseVaultConversionSample, + address(quoteVault), + quoteVaultConversionSample, + salt + ); + emit CreateChainlinkOracleFeeds( + address(oracle), + address(baseFeed1), + address(baseFeed2), + address(quoteFeed1), + address(quoteFeed2), + baseTokenDecimals, + quoteTokenDecimals + ); + } +} diff --git a/src/interfaces/IChainlinkOracleFactory.sol b/src/interfaces/IChainlinkOracleFactory.sol new file mode 100644 index 0000000..93c0382 --- /dev/null +++ b/src/interfaces/IChainlinkOracleFactory.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import {IChainlinkOracle} from "./IChainlinkOracle.sol"; +import {IERC4626} from "../libraries/VaultLib.sol"; +import {AggregatorV3Interface} from "../libraries/ChainlinkDataFeedLib.sol"; + +/// @title IChainlinkOracleFactory +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Interface of Chainkink Oracle's factory. +interface IChainlinkOracleFactory { + /// @notice Whether a Chainlink oracle vault was created with the factory. + function isChainlinkOracle(address target) external view returns (bool); + + /// @dev Here is the list of assumptions that guarantees the oracle behaves as expected: + /// - Feeds are either Chainlink-compliant or the address zero. + /// - Feeds have the same behavioral assumptions as Chainlink's. + /// - Feeds are set in the correct order. + /// - Decimals passed as argument are correct. + /// - The vault's sample shares quoted as assets and the base feed prices don't overflow when multiplied. + /// - The quote feed prices don't overflow when multiplied. + /// - The vault, if set, is ERC4626-compliant. + /// @param baseVault Base vault. Pass address zero to omit this parameter. + /// @param baseVaultConversionSample The sample amount of base vault shares used to convert to underlying. + /// Pass 1 if the base asset is not a vault. Should be chosen such that converting `baseVaultConversionSample` to + /// assets has enough precision. + /// @param quoteVault Quote vault. Pass address zero to omit this parameter. + /// @param quoteVaultConversionSample The sample amount of quote vault shares used to convert to underlying. + /// Pass 1 if the base asset is not a vault. Should be chosen such that converting `quoteVaultConversionSample` to + /// assets has enough precision. + /// @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. + /// @param salt The salt to use for the MetaMorpho vault's CREATE2 address. + function createChainlinkOracle( + IERC4626 baseVault, + uint256 baseVaultConversionSample, + IERC4626 quoteVault, + uint256 quoteVaultConversionSample, + AggregatorV3Interface baseFeed1, + AggregatorV3Interface baseFeed2, + AggregatorV3Interface quoteFeed1, + AggregatorV3Interface quoteFeed2, + uint256 baseTokenDecimals, + uint256 quoteTokenDecimals, + bytes32 salt + ) external returns (IChainlinkOracle oracle); +} diff --git a/test/ChainlinkOracleFactoryTest.sol b/test/ChainlinkOracleFactoryTest.sol new file mode 100644 index 0000000..6eeea0d --- /dev/null +++ b/test/ChainlinkOracleFactoryTest.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./helpers/BaseTest.sol"; + +import "../src/ChainlinkOracleFactory.sol"; + +import {ChainlinkDataFeedLib} from "../src/libraries/ChainlinkDataFeedLib.sol"; + +contract ChainlinkOracleFactoryTest is BaseTest { + using ChainlinkDataFeedLib for AggregatorV3Interface; + + ChainlinkOracleFactory factory; + + function setUp() public override { + super.setUp(); + + factory = new ChainlinkOracleFactory(); + } + + function testDeploySDaiUsdcOracle(bytes32 salt) public { + bytes32 initCodeHash = hashInitCode( + type(ChainlinkOracle).creationCode, + abi.encode(sDaiVault, 1e18, vaultZero, 1, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6) + ); + address expectedAddress = computeCreate2Address(salt, initCodeHash, address(factory)); + + assertFalse(factory.isChainlinkOracle(expectedAddress), "isChainlinkOracle"); + + // vm.expectEmit(address(factory)); + // emit ChainlinkOracleFactory.CreateChainlinkOracle( + // expectedAddress, address(this), address(sDaiVault), 1e18, address(vaultZero), 1, salt + // ); + // emit ChainlinkOracleFactory.CreateChainlinkOracleFeeds( + // expectedAddress, address(daiEthFeed), address(feedZero), address(usdcEthFeed), address(feedZero), 8, 6 + // ); + IChainlinkOracle oracle = factory.createChainlinkOracle( + sDaiVault, 1e18, vaultZero, 1, daiEthFeed, feedZero, usdcEthFeed, feedZero, 18, 6, salt + ); + + assertEq(expectedAddress, address(oracle), "computeCreate2Address"); + + assertTrue(factory.isChainlinkOracle(address(oracle)), "isChainlinkOracle"); + + uint256 scaleFactor = 10 ** (36 + 6 + 18 - 18 - 18 - 18); + + assertEq(address(oracle.BASE_VAULT()), address(sDaiVault), "BASE_VAULT"); + assertEq(oracle.BASE_VAULT_CONVERSION_SAMPLE(), 1e18, "BASE_VAULT_CONVERSION_SAMPLE"); + assertEq(address(oracle.QUOTE_VAULT()), address(vaultZero), "QUOTE_VAULT"); + assertEq(oracle.QUOTE_VAULT_CONVERSION_SAMPLE(), 1, "QUOTE_VAULT_CONVERSION_SAMPLE"); + assertEq(address(oracle.BASE_FEED_1()), address(daiEthFeed), "BASE_FEED_1"); + assertEq(address(oracle.BASE_FEED_2()), address(feedZero), "BASE_FEED_2"); + assertEq(address(oracle.QUOTE_FEED_1()), address(usdcEthFeed), "QUOTE_FEED_1"); + assertEq(address(oracle.QUOTE_FEED_2()), address(feedZero), "QUOTE_FEED_2"); + assertEq(oracle.SCALE_FACTOR(), scaleFactor, "SCALE_FACTOR"); + } + + function testDeployUsdcSDaiOracle(bytes32 salt) public { + bytes32 initCodeHash = hashInitCode( + type(ChainlinkOracle).creationCode, + abi.encode(vaultZero, 1, sDaiVault, 1e18, usdcEthFeed, feedZero, daiEthFeed, feedZero, 6, 18) + ); + address expectedAddress = computeCreate2Address(salt, initCodeHash, address(factory)); + + assertFalse(factory.isChainlinkOracle(expectedAddress), "isChainlinkOracle"); + + // vm.expectEmit(address(factory)); + // emit ChainlinkOracleFactory.CreateChainlinkOracle( + // expectedAddress, address(this), address(vaultZero), 1, address(sDaiVault), 1e18, salt + // ); + // emit ChainlinkOracleFactory.CreateChainlinkOracleFeeds( + // expectedAddress, address(usdcEthFeed), address(feedZero), address(daiEthFeed), address(feedZero), 6, 18 + // ); + IChainlinkOracle oracle = factory.createChainlinkOracle( + vaultZero, 1, sDaiVault, 1e18, usdcEthFeed, feedZero, daiEthFeed, feedZero, 6, 18, salt + ); + + assertEq(expectedAddress, address(oracle), "computeCreate2Address"); + + assertTrue(factory.isChainlinkOracle(address(oracle)), "isChainlinkOracle"); + + uint256 scaleFactor = 10 ** (36 + 18 + 18 + 0 - 6 - 18 - 0) * 1e18; + + assertEq(address(oracle.BASE_VAULT()), address(vaultZero), "BASE_VAULT"); + assertEq(oracle.BASE_VAULT_CONVERSION_SAMPLE(), 1, "BASE_VAULT_CONVERSION_SAMPLE"); + assertEq(address(oracle.QUOTE_VAULT()), address(sDaiVault), "QUOTE_VAULT"); + assertEq(oracle.QUOTE_VAULT_CONVERSION_SAMPLE(), 1e18, "QUOTE_VAULT_CONVERSION_SAMPLE"); + assertEq(address(oracle.BASE_FEED_1()), address(usdcEthFeed), "BASE_FEED_1"); + assertEq(address(oracle.BASE_FEED_2()), address(feedZero), "BASE_FEED_2"); + assertEq(address(oracle.QUOTE_FEED_1()), address(daiEthFeed), "QUOTE_FEED_1"); + assertEq(address(oracle.QUOTE_FEED_2()), address(feedZero), "QUOTE_FEED_2"); + assertEq(oracle.SCALE_FACTOR(), scaleFactor, "SCALE_FACTOR"); + } +} diff --git a/test/ChainlinkOracleTest.sol b/test/ChainlinkOracleTest.sol index 413acda..4eb80cc 100644 --- a/test/ChainlinkOracleTest.sol +++ b/test/ChainlinkOracleTest.sol @@ -1,38 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "../lib/forge-std/src/Test.sol"; -import "../src/ChainlinkOracle.sol"; -import "../src/libraries/ErrorsLib.sol"; -import "./mocks/ChainlinkAggregatorMock.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); -// 18 decimals of precision -AggregatorV3Interface constant daiEthFeed = AggregatorV3Interface(0x773616E4d11A78F511299002da57A0a94577F1f4); - -IERC4626 constant vaultZero = IERC4626(address(0)); -IERC4626 constant sDaiVault = IERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA); - -contract ChainlinkOracleTest is Test { - using Math for uint256; +import "./helpers/BaseTest.sol"; - function setUp() public { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); - } +contract ChainlinkOracleTest is BaseTest { + using Math for uint256; function testOracleWbtcUsdc() public { ChainlinkOracle oracle = diff --git a/test/helpers/BaseTest.sol b/test/helpers/BaseTest.sol new file mode 100644 index 0000000..9073938 --- /dev/null +++ b/test/helpers/BaseTest.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../../lib/forge-std/src/Test.sol"; +import "../../src/ChainlinkOracle.sol"; +import "../../src/libraries/ErrorsLib.sol"; +import "../mocks/ChainlinkAggregatorMock.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); +// 18 decimals of precision +AggregatorV3Interface constant daiEthFeed = AggregatorV3Interface(0x773616E4d11A78F511299002da57A0a94577F1f4); + +IERC4626 constant vaultZero = IERC4626(address(0)); +IERC4626 constant sDaiVault = IERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA); + +contract BaseTest is Test { + function setUp() public virtual { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + } +}