Skip to content

Commit

Permalink
feat: YieldNest price feed contract (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbajollari authored Nov 7, 2024
1 parent 41f6c74 commit 1c617cf
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
6 changes: 6 additions & 0 deletions contracts/yn/interfaces/IynViewer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IynViewer {
function getRate() external view returns (uint256);
}
44 changes: 44 additions & 0 deletions contracts/ynpricefeed/CloneFactory.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
126 changes: 126 additions & 0 deletions contracts/ynpricefeed/ynPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}
16 changes: 12 additions & 4 deletions mainnet_chains.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
"priceFeedImplementation": "0xa1aB70C0F3725AcA1D1e85Bd4402Dd2d5F6AFf19",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0xd285A4F0Ad1BB6b1Db8cD3dD839E9f423938ef9E",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": ""
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
},
{
"name": "Optimism",
Expand All @@ -27,9 +29,11 @@
"priceFeedImplementation": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": ""
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
},
{
"name": "Base",
Expand All @@ -43,9 +47,11 @@
"priceFeedImplementation": "0x09d43904C8ABd470df1B793df68904A9714558CF",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": ""
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
},
{
"name": "Ethereum",
Expand All @@ -59,8 +65,10 @@
"priceFeedImplementation": "0xde471274F1B684476d341eB131224F389AD4A270",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "0xc2E105535132E588b5D1764A0b9472e5537FA9cD",
"ynPriceFeedImplementation": "",
"cloneFactory": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32"
"cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32",
"cloneFactoryYn": ""
}
]
61 changes: 61 additions & 0 deletions scripts/createYnPriceFeeds.ts
Original file line number Diff line number Diff line change
@@ -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;
});
38 changes: 38 additions & 0 deletions scripts/deployYnCloneFactory.ts
Original file line number Diff line number Diff line change
@@ -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;
});
38 changes: 38 additions & 0 deletions scripts/deployYnPriceFeedImplementation.ts
Original file line number Diff line number Diff line change
@@ -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;
});
Loading

0 comments on commit 1c617cf

Please sign in to comment.