Skip to content

Commit

Permalink
feat: Inception LRT Price Feed (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbajollari authored Dec 27, 2024
1 parent 7464a84 commit 7dbb63c
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ MELLOW_VAULTS=["0x5fD13359Ba15A84B76f7F87568309040176167cd"]
MELLOW_QUOTE_ASSETS=["0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"]
YN_PRICE_FEEDS=["YNETH/ETH"]
YN_VIEWERS=["0x0365a6eF790e05EEe386B57326e5Ceaf5B10899e"]
INCEPTION_PRICE_FEEDS=["inwstETHs/wstETH"]
INCEPTION_VAULTS=["0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97"]
INCEPTION_LRT_ADDRESS=["0x8E0789d39db454DBE9f4a77aCEF6dc7c69f6D552"]
7 changes: 7 additions & 0 deletions contracts/inception/interfaces/IVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IVault {
/// @dev returns the total deposited into asset strategy
function getTotalDeposited() external view returns (uint256);
}
48 changes: 48 additions & 0 deletions contracts/inceptionpricefeed/CloneFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./InceptionPriceFeed.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";

/// @title Factory for creating InceptionPriceFeed contract clones.
/// @notice This contract will create a InceptionPriceFeed clone and map its address to the clone creator.
/// @dev Cloning is done with OpenZeppelin's Clones contract.
contract CloneFactory {
event InceptionPriceFeedCloneCreated(
address _inceptionPriceFeedCloneAddress
);

mapping (address => address) public InceptionPriceFeedCloneAddresses;
address public implementationAddress;

/// @param _implementationAddress Address of implementation contract to be cloned.
constructor(address _implementationAddress) {
implementationAddress = _implementationAddress;
}

/// @notice Create clone of InceptionPriceFeed contract and initialize it.
/// @dev Clone method returns address of created clone.
/// @param _vault Address of Inception vault contract.
/// @param _inceptionLRT Address of Inception LRT token.
/// @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 createInceptionPriceFeed(
address _vault,
address _inceptionLRT,
uint8 _priceFeedDecimals,
string calldata _priceFeedBase,
string calldata _priceFeedQuote
) external {
address inceptionPriceFeedCloneAddress = Clones.clone(implementationAddress);
InceptionPriceFeed(inceptionPriceFeedCloneAddress).initialize(
_vault,
_inceptionLRT,
_priceFeedDecimals,
_priceFeedBase,
_priceFeedQuote
);
InceptionPriceFeedCloneAddresses[msg.sender] = inceptionPriceFeedCloneAddress;
emit InceptionPriceFeedCloneCreated(inceptionPriceFeedCloneAddress);
}
}
138 changes: 138 additions & 0 deletions contracts/inceptionpricefeed/InceptionPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "../inception/interfaces/IVault.sol";

/// @title Contract for retreiving a Inception LRT's exchange rate value with chainlink's AggregatorV3Interface
/// implemented.
/// @author Ojo Network (https://docs.ojo.network/)
contract InceptionPriceFeed is Initializable, AggregatorV3Interface {
uint8 private priceFeedDecimals;

string private priceFeedBase;

string private priceFeedQuote;

address public vault;

address public inceptionLRT;

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 _vault Address of Inception vault contract.
/// @param _inceptionLRT Address of Inception LRT token.
/// @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 _vault,
address _inceptionLRT,
uint8 _priceFeedDecimals,
string calldata _priceFeedBase,
string calldata _priceFeedQuote
) external initializer {
vault = _vault;
inceptionLRT = _inceptionLRT;
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 Inception LRT Vault 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 Inception LRT Vault.
/// @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();

IVault vault_ = IVault(vault);
ERC20 inceptionLRT_ = ERC20(inceptionLRT);

answer = 0;

if (inceptionLRT_.totalSupply() != 0) {
answer = int256(vault_.getTotalDeposited()) * 1e18 / int256(inceptionLRT_.totalSupply());
}

// 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
);
}
}
8 changes: 8 additions & 0 deletions mainnet_chains.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"priceFeedImplementation": "0xa1aB70C0F3725AcA1D1e85Bd4402Dd2d5F6AFf19",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"inceptionPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0xd285A4F0Ad1BB6b1Db8cD3dD839E9f423938ef9E",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryInception": "",
"cloneFactoryYn": ""
},
{
Expand All @@ -29,10 +31,12 @@
"priceFeedImplementation": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"inceptionPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0x02Ed15B70D4dE1209c3Dd5a75195CB3f3dDB8B07",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryInception": "",
"cloneFactoryYn": ""
},
{
Expand All @@ -47,10 +51,12 @@
"priceFeedImplementation": "0x09d43904C8ABd470df1B793df68904A9714558CF",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "",
"inceptionPriceFeedImplementation": "",
"ynPriceFeedImplementation": "",
"cloneFactory": "0xfaC9d315b9b558e10eBdb0462aA42577aADe6601",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "",
"cloneFactoryInception": "",
"cloneFactoryYn": ""
},
{
Expand All @@ -65,10 +71,12 @@
"priceFeedImplementation": "0xde471274F1B684476d341eB131224F389AD4A270",
"priceFeedQuotedImplementation": "",
"mellowPriceFeedImplementation": "0xc2E105535132E588b5D1764A0b9472e5537FA9cD",
"inceptionPriceFeedImplementation": "0x418630F34A6E82c93a97a1b6E79B6a20C76EaD05",
"ynPriceFeedImplementation": "",
"cloneFactory": "0x710C8a3c8CB393cA24748849de3585b5C48D4D0c",
"cloneFactoryQuoted": "",
"cloneFactoryMellow": "0x721c05f08308Bcce5C62e342070564Fd4441ec32",
"cloneFactoryInception": "0x2Babd8D4BCE072e78aA288c639Ef4516fCe26d89",
"cloneFactoryYn": ""
}
]
62 changes: 62 additions & 0 deletions scripts/createInceptionPriceFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Wallet, ethers } from "ethers";
import CloneFactory from '../artifacts/contracts/inceptionpricefeed/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 inceptionPriceFeedDecimals = process.env.PRICE_FEED_DECIMALS as any;
const inceptionPriceFeeds = JSON.parse(process.env.INCEPTION_PRICE_FEEDS!);
const inceptionVaults = JSON.parse(process.env.INCEPTION_VAULTS!);
const inceptionLRTAddresses = JSON.parse(process.env.INCEPTION_LRT_ADDRESS!);

if (inceptionPriceFeeds.length !== inceptionVaults.length || inceptionPriceFeeds.length !== inceptionLRTAddresses.length) {
throw new Error('unequal amount of inceptionVaults associated with inceptionPriceFeeds');
}

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 cloneFactoryInceptionContract = new ethers.Contract(chain.cloneFactoryInception, CloneFactory.abi, wallet)
let i = 0
for (const inceptionPriceFeed of inceptionPriceFeeds) {
console.log(`Deploying ${inceptionPriceFeed} price feed on ${chain.name}`);
try {
const [baseAsset, quoteAsset] = inceptionPriceFeed.split('/');

console.log("baseAsset", baseAsset)
console.log("quoteAsset", quoteAsset)
const tx = await cloneFactoryInceptionContract.createInceptionPriceFeed(inceptionVaults[i], inceptionLRTAddresses[i], inceptionPriceFeedDecimals, 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 ${inceptionPriceFeed} on ${chain.name}:`, error);
}
i += 1
}
}
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
38 changes: 38 additions & 0 deletions scripts/deployInceptionCloneFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Wallet, ethers } from "ethers";
import InceptionCloneFactory from '../artifacts/contracts/inceptionpricefeed/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 inceptionCloneFactoryFactory = new ethers.ContractFactory(InceptionCloneFactory.abi, InceptionCloneFactory.bytecode, wallet)
const inceptionCloneFactory = await inceptionCloneFactoryFactory.deploy(chain.inceptionPriceFeedImplementation)
console.log(`${chain.name}, address: ${await inceptionCloneFactory.getAddress()}`);
}
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
38 changes: 38 additions & 0 deletions scripts/deployInceptionPriceFeedImplementation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Wallet, ethers } from "ethers";
import InceptionPriceFeed from '../artifacts/contracts/inceptionpricefeed/InceptionPriceFeed.sol/InceptionPriceFeed.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 inceptionPriceFeedFactory = new ethers.ContractFactory(InceptionPriceFeed.abi, InceptionPriceFeed.bytecode, wallet)
const inceptionPriceFeed = await inceptionPriceFeedFactory.deploy()
console.log(`${chain.name}, address: ${await inceptionPriceFeed.getAddress()}`);
}
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Loading

0 comments on commit 7dbb63c

Please sign in to comment.