Skip to content

Commit

Permalink
feat: Level price feed contract
Browse files Browse the repository at this point in the history
  • Loading branch information
rbajollari committed Nov 14, 2024
1 parent 7464a84 commit a08de28
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 24 deletions.
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

0 comments on commit a08de28

Please sign in to comment.