Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Level price feed contract #57

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
41 changes: 41 additions & 0 deletions contracts/levelpricefeed/CloneFactory.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
129 changes: 129 additions & 0 deletions contracts/levelpricefeed/levelPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}
16 changes: 12 additions & 4 deletions mainnet_chains.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"levelPriceFeedImplementation": "",
"cloneFactory": "0xd285A4F0Ad1BB6b1Db8cD3dD839E9f423938ef9E",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
"cloneFactoryYn": "",
"cloneFactoryLevel": ""
},
{
"name": "Optimism",
Expand All @@ -30,10 +32,12 @@
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"levelPriceFeedImplementation": "",
"cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
"cloneFactoryYn": "",
"cloneFactoryLevel": ""
},
{
"name": "Base",
Expand All @@ -48,10 +52,12 @@
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"levelPriceFeedImplementation": "",
"cloneFactory": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryYn": ""
"cloneFactoryYn": "",
"cloneFactoryLevel": ""
},
{
"name": "Ethereum",
Expand All @@ -66,9 +72,11 @@
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "0xc2E105535132E588b5D1764A0b9472e5537FA9cD",
"ynPriceFeedImplementation": "",
"levelPriceFeedImplementation": "",
"cloneFactory": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32",
"cloneFactoryYn": ""
"cloneFactoryYn": "",
"cloneFactoryLevel": ""
}
]
60 changes: 60 additions & 0 deletions scripts/createLevelPriceFeeds.ts
Original file line number Diff line number Diff line change
@@ -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;
});
38 changes: 38 additions & 0 deletions scripts/deployLevelCloneFactory.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/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;
});
38 changes: 38 additions & 0 deletions scripts/deployLevelPriceFeedImplementation.ts
Original file line number Diff line number Diff line change
@@ -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;
});
Loading
Loading