From 1c617cf58676aaaac5e32e2bab6a5c27d4264c03 Mon Sep 17 00:00:00 2001 From: ryanbajollari <54822716+rbajollari@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:26:31 +0100 Subject: [PATCH] feat: YieldNest price feed contract (#56) --- .env.example | 2 + contracts/yn/interfaces/IynViewer.sol | 6 + contracts/ynpricefeed/CloneFactory.sol | 44 +++++++ contracts/ynpricefeed/ynPriceFeed.sol | 126 +++++++++++++++++++++ mainnet_chains.json | 16 ++- scripts/createYnPriceFeeds.ts | 61 ++++++++++ scripts/deployYnCloneFactory.ts | 38 +++++++ scripts/deployYnPriceFeedImplementation.ts | 38 +++++++ testnet_chains.json | 88 ++++++++++---- 9 files changed, 396 insertions(+), 23 deletions(-) create mode 100644 contracts/yn/interfaces/IynViewer.sol create mode 100644 contracts/ynpricefeed/CloneFactory.sol create mode 100644 contracts/ynpricefeed/ynPriceFeed.sol create mode 100644 scripts/createYnPriceFeeds.ts create mode 100644 scripts/deployYnCloneFactory.ts create mode 100644 scripts/deployYnPriceFeedImplementation.ts diff --git a/.env.example b/.env.example index 344f01c..2286b26 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,5 @@ QUOTED_PRICE_FEEDS=["WETH/ETH", "WETH/USDC"] MELLOW_PRICE_FEEDS=["amphrETH/wstETH"] MELLOW_VAULTS=["0x5fD13359Ba15A84B76f7F87568309040176167cd"] MELLOW_QUOTE_ASSETS=["0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"] +YN_PRICE_FEEDS=["YNETH/ETH"] +YN_VIEWERS=["0x0365a6eF790e05EEe386B57326e5Ceaf5B10899e"] diff --git a/contracts/yn/interfaces/IynViewer.sol b/contracts/yn/interfaces/IynViewer.sol new file mode 100644 index 0000000..556c43c --- /dev/null +++ b/contracts/yn/interfaces/IynViewer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IynViewer { + function getRate() external view returns (uint256); +} diff --git a/contracts/ynpricefeed/CloneFactory.sol b/contracts/ynpricefeed/CloneFactory.sol new file mode 100644 index 0000000..b8fa431 --- /dev/null +++ b/contracts/ynpricefeed/CloneFactory.sol @@ -0,0 +1,44 @@ +pragma solidity ^0.8.20; + +import "./ynPriceFeed.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +/// @title Factory for creating ynPriceFeed contract clones. +/// @notice This contract will create a ynPriceFeed clone and map its address to the clone creator. +/// @dev Cloning is done with OpenZeppelin's Clones contract. +contract CloneFactory { + event ynPriceFeedCloneCreated( + address _ynPriceFeedCloneAddress + ); + + mapping (address => address) public ynPriceFeedCloneAddresses; + address public implementationAddress; + + /// @param _implementationAddress Address of implementation contract to be cloned. + constructor(address _implementationAddress) { + implementationAddress = _implementationAddress; + } + + /// @notice Create clone of ynPriceFeed contract and initialize it. + /// @dev Clone method returns address of created clone. + /// @param _ynViewer Address of ynViewer contract. + /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. + /// @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 createYnPriceFeed( + address _ynViewer, + uint8 _priceFeedDecimals, + string calldata _priceFeedBase, + string calldata _priceFeedQuote + ) external { + address ynPriceFeedCloneAddress = Clones.clone(implementationAddress); + ynPriceFeed(ynPriceFeedCloneAddress).initialize( + _ynViewer, + _priceFeedDecimals, + _priceFeedBase, + _priceFeedQuote + ); + ynPriceFeedCloneAddresses[msg.sender] = ynPriceFeedCloneAddress; + emit ynPriceFeedCloneCreated(ynPriceFeedCloneAddress); + } +} diff --git a/contracts/ynpricefeed/ynPriceFeed.sol b/contracts/ynpricefeed/ynPriceFeed.sol new file mode 100644 index 0000000..8056f41 --- /dev/null +++ b/contracts/ynpricefeed/ynPriceFeed.sol @@ -0,0 +1,126 @@ +// 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"; +import "../yn/interfaces/IynViewer.sol"; + +/// @title Contract for retreiving a YieldNest LRT's exchange rate value with chainlink's AggregatorV3Interface +/// implemented. +/// @author Ojo Network (https://docs.ojo.network/) +contract ynPriceFeed is Initializable, AggregatorV3Interface { + uint8 private priceFeedDecimals; + + string private priceFeedBase; + + string private priceFeedQuote; + + address public ynViewer; + + 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 _ynViewer Address of ynViewer contract. + /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. + /// @param _priceFeedBase Base asset of PriceFeed. + /// @param _priceFeedQuote Quote asset of PriceFeed. + function initialize( + address _ynViewer, + uint8 _priceFeedDecimals, + string calldata _priceFeedBase, + string calldata _priceFeedQuote + ) external initializer { + ynViewer = _ynViewer; + priceFeedDecimals = _priceFeedDecimals; + priceFeedBase = _priceFeedBase; + priceFeedQuote = _priceFeedQuote; + } + + /// @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 ynViewer 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 ynViewer 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(); + + IynViewer ynViewer_ = IynViewer(ynViewer); + answer = int256(ynViewer_.getRate()); + + // These values are equal after chainlink’s OCR update + startedAt = block.timestamp; + 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 4dbdc5a..039755c 100644 --- a/mainnet_chains.json +++ b/mainnet_chains.json @@ -11,9 +11,11 @@ "priceFeedImplementation": "0xa1aB70C0F3725AcA1D1e85Bd4402Dd2d5F6AFf19", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0xd285A4F0Ad1BB6b1Db8cD3dD839E9f423938ef9E", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Optimism", @@ -27,9 +29,11 @@ "priceFeedImplementation": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Base", @@ -43,9 +47,11 @@ "priceFeedImplementation": "0x09d43904C8ABd470df1B793df68904A9714558CF", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Ethereum", @@ -59,8 +65,10 @@ "priceFeedImplementation": "0xde471274F1B684476d341eB131224F389AD4A270", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "0xc2E105535132E588b5D1764A0b9472e5537FA9cD", + "ynPriceFeedImplementation": "", "cloneFactory": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32" + "cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32", + "cloneFactoryYn": "" } ] diff --git a/scripts/createYnPriceFeeds.ts b/scripts/createYnPriceFeeds.ts new file mode 100644 index 0000000..3d90577 --- /dev/null +++ b/scripts/createYnPriceFeeds.ts @@ -0,0 +1,61 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactory from '../artifacts/contracts/ynpricefeed/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 ynPriceFeedDecimals = process.env.PRICE_FEED_DECIMALS as any; + const ynPriceFeeds = JSON.parse(process.env.YN_PRICE_FEEDS!); + const ynViewers = JSON.parse(process.env.YN_VIEWERS!); + + if (ynPriceFeeds.length !== ynViewers.length) { + throw new Error('unequal amount of ynViewers associated with ynPriceFeeds'); + } + + 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 cloneFactoryYnContract = new ethers.Contract(chain.cloneFactoryYn, CloneFactory.abi, wallet) + let i = 0 + for (const ynPriceFeed of ynPriceFeeds) { + console.log(`Deploying ${ynPriceFeed} price feed on ${chain.name}`); + try { + const [baseAsset, quoteAsset] = ynPriceFeed.split('/'); + + console.log("baseAsset", baseAsset) + console.log("quoteAsset", quoteAsset) + const tx = await cloneFactoryYnContract.createYnPriceFeed(ynViewers[i], ynPriceFeedDecimals, 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 ${ynPriceFeed} on ${chain.name}:`, error); + } + i += 1 + } + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployYnCloneFactory.ts b/scripts/deployYnCloneFactory.ts new file mode 100644 index 0000000..da223b9 --- /dev/null +++ b/scripts/deployYnCloneFactory.ts @@ -0,0 +1,38 @@ +import { Wallet, ethers } from "ethers"; +import CloneFactoryQuoted from '../artifacts/contracts/ynpricefeed/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.ynPriceFeedImplementation) + console.log(`${chain.name}, address: ${await cloneFactoryQuoted.getAddress()}`); + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deployYnPriceFeedImplementation.ts b/scripts/deployYnPriceFeedImplementation.ts new file mode 100644 index 0000000..33ab3a7 --- /dev/null +++ b/scripts/deployYnPriceFeedImplementation.ts @@ -0,0 +1,38 @@ +import { Wallet, ethers } from "ethers"; +import ynPriceFeed from '../artifacts/contracts/ynpricefeed/ynPriceFeed.sol/ynPriceFeed.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 ynPriceFeedFactory = new ethers.ContractFactory(ynPriceFeed.abi, ynPriceFeed.bytecode, wallet) + const ynPriceFeedImplementation = await ynPriceFeedFactory.deploy() + console.log(`${chain.name}, address: ${await ynPriceFeedImplementation.getAddress()}`); + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/testnet_chains.json b/testnet_chains.json index 94ea302..1546d25 100644 --- a/testnet_chains.json +++ b/testnet_chains.json @@ -11,9 +11,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Ethereum Sepolia", @@ -27,9 +29,29 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "0x1A069010D7F572c97925E83a1298Df8f96893c60", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "0x694723e8Fe9945CffDB671b02175DC55DeDf7F29", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" + }, + { + "name": "Ethereum Holesky", + "chainId": 17000, + "rpc": "https://ethereum-holesky-rpc.publicnode.com", + "gateway": "", + "tokenSymbol": "ETH", + "gasReceiver": "", + "ojoContract": "", + "create2Deployer": "", + "priceFeedImplementation": "", + "priceFeedQuotedImplementation": "", + "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c", + "cloneFactory": "", + "cloneFactoryQuoted": "", + "cloneFactoryMellow": "", + "cloneFactoryYn": "0x1bc0555c2137447160a2581837372f63835a8002" }, { "name": "BNB Chain", @@ -43,9 +65,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Polygon Mumbai", @@ -61,7 +85,8 @@ "mellowPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Polygon zkEVM", @@ -91,9 +116,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Fantom", @@ -107,9 +134,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Moonbase", @@ -123,9 +152,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Arbitrum Goerli", @@ -140,7 +171,8 @@ "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", "cloneFactory": "", - "cloneFactoryQuoted": "" + "cloneFactoryQuoted": "", + "cloneFactoryYn": "" }, { "name": "Arbitrum Sepolia", @@ -151,13 +183,14 @@ "gasReceiver": "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6", "ojoContract": "0x5BB3E85f91D08fe92a3D123EE35050b763D6E6A7", "create2Deployer": "0x98b2920d53612483f91f12ed7754e51b4a77919e", - "priceFeedImplementation": "0x3DB6DF9EDfDcfE97D574Aa6f106C767051561Be2", "priceFeedQuotedImplementation": "0x2Babd8D4BCE072e78aA288c639Ef4516fCe26d89", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0xab2c7cc090A45836fae04501e0454413ECA96611", "cloneFactoryQuoted": "0x4f5E3B2d64670cd8EA2329c4B028a4c07832F846", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Optimism Goerli", @@ -171,9 +204,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Optimism Sepolia", @@ -187,9 +222,11 @@ "priceFeedImplementation": "0x48B10B538B7E5af4CbFd93B1C4d36668e8F6F644", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0xe9c4145FCeDdc19bc9B788C5d16cF08AD70d3850", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Base Goerli", @@ -203,9 +240,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Base Sepolia", @@ -219,9 +258,11 @@ "priceFeedImplementation": "0x09d43904C8ABd470df1B793df68904A9714558CF", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Mantle", @@ -235,9 +276,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Alfajores", @@ -251,9 +294,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Kava", @@ -269,7 +314,8 @@ "mellowPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Filecoin Calibration", @@ -283,9 +329,11 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" }, { "name": "Linea Goerli", @@ -299,8 +347,10 @@ "priceFeedImplementation": "", "priceFeedQuotedImplementation": "", "mellowPriceFeedImplementation": "", + "ynPriceFeedImplementation": "", "cloneFactory": "", "cloneFactoryQuoted": "", - "cloneFactoryMellow": "" + "cloneFactoryMellow": "", + "cloneFactoryYn": "" } ]