diff --git a/.env.example b/.env.example index 2286b26..ea5060c 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,5 @@ MELLOW_VAULTS=["0x5fD13359Ba15A84B76f7F87568309040176167cd"] MELLOW_QUOTE_ASSETS=["0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"] YN_PRICE_FEEDS=["YNETH/ETH"] YN_VIEWERS=["0x0365a6eF790e05EEe386B57326e5Ceaf5B10899e"] +LEVEL_PRICE_FEEDS=["lvlUSD/USDC"] +LEVEL_ORACLES=["0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"] diff --git a/contracts/levelpricefeed/CloneFactory.sol b/contracts/levelpricefeed/CloneFactory.sol new file mode 100644 index 0000000..7606f35 --- /dev/null +++ b/contracts/levelpricefeed/CloneFactory.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.8.20; + +import "./levelPriceFeed.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +/// @title Factory for creating levelPriceFeed contract clones. +/// @notice This contract will create a levelPriceFeed clone and map its address to the clone creator. +/// @dev Cloning is done with OpenZeppelin's Clones contract. +contract CloneFactory { + event levelPriceFeedCloneCreated( + address _levelPriceFeedCloneAddress + ); + + mapping (address => address) public levelPriceFeedCloneAddresses; + address public implementationAddress; + + /// @param _implementationAddress Address of implementation contract to be cloned. + constructor(address _implementationAddress) { + implementationAddress = _implementationAddress; + } + + /// @notice Create clone of levelPriceFeed contract and initialize it. + /// @dev Clone method returns address of created clone. + /// @param _collateralAssetOracle Address of collateral asset's oracle contract. + /// @param _priceFeedBase Base asset of PriceFeed, should be set to asset symbol ticker. + /// @param _priceFeedQuote Quote asset of PriceFeed, should be set to asset symbol ticker. + function createLevelPriceFeed( + address _collateralAssetOracle, + string calldata _priceFeedBase, + string calldata _priceFeedQuote + ) external { + address levelPriceFeedCloneAddress = Clones.clone(implementationAddress); + levelPriceFeed(levelPriceFeedCloneAddress).initialize( + _collateralAssetOracle, + _priceFeedBase, + _priceFeedQuote + ); + levelPriceFeedCloneAddresses[msg.sender] = levelPriceFeedCloneAddress; + emit levelPriceFeedCloneCreated(levelPriceFeedCloneAddress); + } +} diff --git a/contracts/levelpricefeed/levelPriceFeed.sol b/contracts/levelpricefeed/levelPriceFeed.sol new file mode 100644 index 0000000..8da0db7 --- /dev/null +++ b/contracts/levelpricefeed/levelPriceFeed.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +/// @title Contract for retreiving a lvlUSD's exchange rate value with chainlink's AggregatorV3Interface +/// implemented. +/// @author Ojo Network (https://docs.ojo.network/) +contract levelPriceFeed is Initializable, AggregatorV3Interface { + uint8 private priceFeedDecimals; + + string private priceFeedBase; + + string private priceFeedQuote; + + address public collateralAssetOracle; + + uint80 constant DEFAULT_ROUND = 1; + + uint256 constant DEFAULT_VERSION = 1; + + uint256 internal constant INT256_MAX = uint256(type(int256).max); + + error GetRoundDataCanBeOnlyCalledWithLatestRound(uint80 requestedRoundId); + + /// @notice Initialize clone of this contract. + /// @dev This function is used in place of a constructor in proxy contracts. + /// @param _collateralAssetOracle Address of collateral asset's oracle contract. + /// @param _priceFeedBase Base asset of PriceFeed. + /// @param _priceFeedQuote Quote asset of PriceFeed. + function initialize( + address _collateralAssetOracle, + string calldata _priceFeedBase, + string calldata _priceFeedQuote + ) external initializer { + collateralAssetOracle = _collateralAssetOracle; + priceFeedBase = _priceFeedBase; + priceFeedQuote = _priceFeedQuote; + + AggregatorV3Interface collateralAssetOracle_ = AggregatorV3Interface(collateralAssetOracle); + priceFeedDecimals = collateralAssetOracle_.decimals(); + } + + /// @notice Amount of decimals price is denominated in. + function decimals() external view returns (uint8) { + return priceFeedDecimals; + } + + /// @notice Asset pair that this proxy is tracking. + function description() external view returns (string memory) { + return string(abi.encodePacked(priceFeedBase, "/", priceFeedQuote)); + } + + /// @notice Version always returns 1. + function version() external view returns (uint256) { + return DEFAULT_VERSION; + } + + /// @dev Latest round always returns 1 since this contract does not support rounds. + function latestRound() public pure returns (uint80) { + return DEFAULT_ROUND; + } + + /// @notice Calculates exchange rate from the levelViewer contract from a specified round. + /// @dev Even though rounds are not utilized in this contract getRoundData is implemented for contracts + /// that still rely on it. Function will revert if specified round is not the latest round. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price in USD of asset this contract is tracking. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + if (_roundId != latestRound()) { + revert GetRoundDataCanBeOnlyCalledWithLatestRound(_roundId); + } + return latestRoundData(); + } + + /// @notice Calculates exchange rate from the levelViewer contract. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price of priceFeedBase quoted by priceFeedQuote. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + roundId = latestRound(); + + AggregatorV3Interface collateralAssetOracle_ = AggregatorV3Interface(collateralAssetOracle); + (, int256 collateralAssetAnswer, , uint256 collateralAssetUpdatedAt, ) = collateralAssetOracle_ + .latestRoundData(); + uint8 collateralDecimals = collateralAssetOracle_.decimals(); + answer = (collateralAssetAnswer < int256(10**collateralDecimals)) ? + collateralAssetAnswer : int256(10**collateralDecimals); + + // These values are equal after chainlink’s OCR update + startedAt = collateralAssetUpdatedAt; + updatedAt = startedAt; + + // roundId is always equal to answeredInRound + answeredInRound = roundId; + + return ( + roundId, + answer, + startedAt, + updatedAt, + answeredInRound + ); + } +} diff --git a/mainnet_chains.json b/mainnet_chains.json index 039755c..302ce73 100644 --- a/mainnet_chains.json +++ b/mainnet_chains.json @@ -12,10 +12,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0xd285A4F0Ad1BB6b1Db8cD3dD839E9f423938ef9E", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Optimism", @@ -30,10 +32,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Base", @@ -48,10 +52,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Ethereum", @@ -66,9 +72,11 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "0xc2E105535132E588b5D1764A0b9472e5537FA9cD", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c", "cloneFactoryQuoted": "", "cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" } ] diff --git a/scripts/createLevelPriceFeeds.ts b/scripts/createLevelPriceFeeds.ts new file mode 100644 index 0000000..7ab7e72 --- /dev/null +++ b/scripts/createLevelPriceFeeds.ts @@ -0,0 +1,60 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactory from '../artifacts/contracts/levelpricefeed/CloneFactory.sol/CloneFactory.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main() { + const evmChains = JSON.parse(process.env.EVM_CHAINS!); + const levelPriceFeeds = JSON.parse(process.env.LEVEL_PRICE_FEEDS!); + const levelOracles = JSON.parse(process.env.LEVEL_ORACLES!); + + if (levelPriceFeeds.length !== levelOracles.length) { + throw new Error('unequal amount of levelOracles associated with levelPriceFeeds'); + } + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let chains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + chains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of chains) { + if (evmChains.includes(chain.name)) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const cloneFactoryLevelContract = new ethers.Contract(chain.cloneFactoryLevel, CloneFactory.abi, wallet) + let i = 0 + for (const levelPriceFeed of levelPriceFeeds) { + console.log(`Deploying ${levelPriceFeed} price feed on ${chain.name}`); + try { + const [baseAsset, quoteAsset] = levelPriceFeed.split('/'); + + console.log("baseAsset", baseAsset) + console.log("quoteAsset", quoteAsset) + const tx = await cloneFactoryLevelContract.createLevelPriceFeed(levelOracles[i], baseAsset, quoteAsset); + console.log(`Transaction sent: ${tx.hash}`); + + const receipt = await tx.wait(); + console.log(`Transaction mined: ${receipt.transactionHash}`); + } catch (error) { + console.error(`Failed to deploy ${levelPriceFeed} on ${chain.name}:`, error); + } + i += 1 + } + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployLevelCloneFactory.ts b/scripts/deployLevelCloneFactory.ts new file mode 100644 index 0000000..2371af6 --- /dev/null +++ b/scripts/deployLevelCloneFactory.ts @@ -0,0 +1,38 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactoryQuoted from '../artifacts/contracts/levelpricefeed/CloneFactory.sol/CloneFactory.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main () { + const evmChains = JSON.parse(process.env.EVM_CHAINS!); + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let chains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + chains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of chains) { + if (evmChains.includes(chain.name)) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const cloneFactoryQuotedFactory = new ethers.ContractFactory(CloneFactoryQuoted.abi, CloneFactoryQuoted.bytecode, wallet) + const cloneFactoryQuoted = await cloneFactoryQuotedFactory.deploy(chain.levelPriceFeedImplementation) + console.log(`${chain.name}, address: ${await cloneFactoryQuoted.getAddress()}`); + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployLevelPriceFeedImplementation.ts b/scripts/deployLevelPriceFeedImplementation.ts new file mode 100644 index 0000000..611d7c8 --- /dev/null +++ b/scripts/deployLevelPriceFeedImplementation.ts @@ -0,0 +1,38 @@ +import { Wallet, ethers } from "ethers"; +import levelPriceFeed from '../artifacts/contracts/levelpricefeed/levelPriceFeed.sol/levelPriceFeed.json'; +import testnet_chains from '../testnet_chains.json'; +import mainnet_chains from '../mainnet_chains.json'; + +async function main() { + const evmChains = JSON.parse(process.env.EVM_CHAINS!); + + const privateKey = process.env.PRIVATE_KEY; + + if (!privateKey) { + throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.'); + } + + const mainnet = process.env.MAINNET as string + let chains = testnet_chains.map((chain) => ({ ...chain })); + if (mainnet === "TRUE") { + chains = mainnet_chains.map((chain) => ({ ...chain })); + } + + for (const chain of chains) { + if (evmChains.includes(chain.name)) { + const provider = new ethers.JsonRpcProvider(chain.rpc) + const wallet = new Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address) + console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`); + + const levelPriceFeedFactory = new ethers.ContractFactory(levelPriceFeed.abi, levelPriceFeed.bytecode, wallet) + const levelPriceFeedImplementation = await levelPriceFeedFactory.deploy() + console.log(`${chain.name}, address: ${await levelPriceFeedImplementation.getAddress()}`); + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/testnet_chains.json b/testnet_chains.json index 1546d25..ba322fc 100644 --- a/testnet_chains.json +++ b/testnet_chains.json @@ -12,10 +12,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Ethereum Sepolia", @@ -30,10 +32,12 @@ "priceFeedQuotedImplementation": "0x1A069010D7F572c97925E83a1298Df8f96893c60", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "0x9eb66bDd9097E35eE3697d4a9DbF53a8D779b4f2", "cloneFactory": "", "cloneFactoryQuoted": "0x694723e8Fe9945CffDB671b02175DC55DeDf7F29", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "0x562B643fDbfFFe96c6447925627BC8B872D741ad" }, { "name": "Ethereum Holesky", @@ -48,10 +52,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "0x1bc0555c2137447160a2581837372f63835a8002" + "cloneFactoryYn": "0x1bc0555c2137447160a2581837372f63835a8002", + "cloneFactoryLevel": "" }, { "name": "BNB Chain", @@ -66,10 +72,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Polygon Mumbai", @@ -83,10 +91,13 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Polygon zkEVM", @@ -100,9 +111,13 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Avalanche Fuji C-Chain", @@ -117,10 +132,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Fantom", @@ -135,10 +152,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Moonbase", @@ -153,10 +172,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Arbitrum Goerli", @@ -170,9 +191,12 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Arbitrum Sepolia", @@ -187,10 +211,12 @@ "priceFeedQuotedImplementation": "0x2Babd8D4BCE072e78aA288c639Ef4516fCe26d89", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0xab2c7cc090A45836fae04501e0454413ECA96611", "cloneFactoryQuoted": "0x4f5E3B2d64670cd8EA2329c4B028a4c07832F846", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Optimism Goerli", @@ -205,10 +231,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Optimism Sepolia", @@ -223,10 +251,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0xe9c4145FCeDdc19bc9B788C5d16cF08AD70d3850", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Base Goerli", @@ -241,10 +271,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Base Sepolia", @@ -259,10 +291,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Mantle", @@ -277,10 +311,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Alfajores", @@ -295,10 +331,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Kava", @@ -312,10 +350,13 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Filecoin Calibration", @@ -330,10 +371,12 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" }, { "name": "Linea Goerli", @@ -348,9 +391,11 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "ynPriceFeedImplementation": "", + "levelPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", "cloneFactoryMellow": "", - "cloneFactoryYn": "" + "cloneFactoryYn": "", + "cloneFactoryLevel": "" } ]