-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Minimal proxy PriceFeed contract implementing Chainlink interface * Price Feed contract init * implement latestRound and getRoundData * add deployment scripts * fix deploying * fix other deployment scripts
- Loading branch information
1 parent
aeb2c90
commit 030c37c
Showing
11 changed files
with
347 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import "./PriceFeed.sol"; | ||
import "@openzeppelin/contracts/proxy/Clones.sol"; | ||
|
||
/// @title Factory for creating PriceFeed contract clones. | ||
/// @notice This contract will create a PriceFeed clone and map its address to the clone creator. | ||
/// @dev Cloning is done with OpenZeppelin's Clones contract. | ||
contract CloneFactory { | ||
event PriceFeedCloneCreated( | ||
address _priceFeedCloneAddress | ||
); | ||
|
||
mapping (address => address) public PriceFeedCloneAddresses; | ||
address public implementationAddress; | ||
|
||
/// @param _implementationAddress Address of implementation contract to be cloned. | ||
constructor(address _implementationAddress) { | ||
implementationAddress = _implementationAddress; | ||
} | ||
|
||
/// @notice Create clone of PriceFeed contract and initialize it. | ||
/// @dev Clone method returns address of created clone. | ||
/// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. | ||
/// @param _priceFeedDescription Description of PriceFeed, should be set to asset symbol ticker. | ||
function createPriceFeed(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) external { | ||
address priceFeedCloneAddress = Clones.clone(implementationAddress); | ||
PriceFeed(priceFeedCloneAddress).initialize(_priceFeedDecimals, _priceFeedDescription); | ||
PriceFeedCloneAddresses[msg.sender] = priceFeedCloneAddress; | ||
emit PriceFeedCloneCreated(priceFeedCloneAddress); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// 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 "../IOjo.sol"; | ||
import "../OjoTypes.sol"; | ||
|
||
/// @title Contract for calling Ojo's oracle contract with chainlink's AggregatorV3Interface implemented. | ||
/// @author Ojo Network (https://docs.ojo.network/) | ||
contract PriceFeed is Initializable, AggregatorV3Interface { | ||
uint8 private priceFeedDecimals; | ||
|
||
string private priceFeedDescription; | ||
|
||
IOjo public immutable ojo; | ||
|
||
uint80 constant DEFAULT_ROUND = 1; | ||
|
||
uint256 constant DEFAULT_VERSION = 1; | ||
|
||
uint256 internal constant INT256_MAX = uint256(type(int256).max); | ||
|
||
error GetRoundDataCanBeOnlyCalledWithLatestRound(uint80 requestedRoundId); | ||
|
||
error UnsafeUintToIntConversion(uint256 value); | ||
|
||
constructor(address ojo_) { | ||
ojo = IOjo(ojo_); | ||
} | ||
|
||
/// @notice Initialize clone of this contract. | ||
/// @dev This function is used in place of a constructor in proxy contracts. | ||
/// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. | ||
/// @param _priceFeedDescription Description of PriceFeed. | ||
function initialize(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) | ||
external | ||
initializer { | ||
priceFeedDecimals = _priceFeedDecimals; | ||
priceFeedDescription = _priceFeedDescription; | ||
} | ||
|
||
/// @notice Amount of decimals price is denominated in. | ||
function decimals() external view returns (uint8) { | ||
return priceFeedDecimals; | ||
} | ||
|
||
/// @notice Asset that this proxy is tracking. | ||
/// @dev This should be set as the asset symbol ticker as it used to query the Ojo contract. | ||
function description() external view returns (string memory) { | ||
return priceFeedDescription; | ||
} | ||
|
||
/// @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 Fetches price data from Ojo 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 Fetches latest price data from Ojo contract. | ||
/// @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 latestRoundData() | ||
public | ||
view | ||
returns ( | ||
uint80 roundId, | ||
int256 answer, | ||
uint256 startedAt, | ||
uint256 updatedAt, | ||
uint80 answeredInRound | ||
) { | ||
roundId = latestRound(); | ||
bytes32 assetName = bytes32(bytes(priceFeedDescription)); | ||
|
||
OjoTypes.PriceData memory priceData = ojo.getPriceData(assetName); | ||
|
||
if (priceData.price > INT256_MAX) { | ||
revert UnsafeUintToIntConversion(priceData.price); | ||
} | ||
|
||
// These values are equal after chainlink’s OCR update | ||
startedAt = priceData.resolveTime; | ||
updatedAt = priceData.resolveTime; | ||
|
||
// roundId is always equal to answeredInRound | ||
answeredInRound = roundId; | ||
|
||
return ( | ||
roundId, | ||
int256(priceData.price), | ||
startedAt, | ||
updatedAt, | ||
answeredInRound | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Wallet, ethers } from "ethers"; | ||
import CloneFactory from '../artifacts/contracts/pricefeed/CloneFactory.sol/CloneFactory.json'; | ||
import testnet_chains from '../testnet_chains.json'; | ||
import mainnet_chains from '../mainnet_chains.json'; | ||
|
||
async function main() { | ||
const cloneFactoryAddress = process.env.CLONE_FACTORY_CONTRACT_ADDRESS as string; | ||
const priceFeedDecimals = 18; | ||
const priceFeedDescription = "ETH"; | ||
|
||
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 evmChains = testnet_chains.map((chain) => ({ ...chain })); | ||
if (mainnet === "TRUE") { | ||
evmChains = mainnet_chains.map((chain) => ({ ...chain })); | ||
} | ||
|
||
for (const chain of evmChains) { | ||
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 cloneFactoryContract = new ethers.Contract(cloneFactoryAddress, CloneFactory.abi, wallet) | ||
await cloneFactoryContract.createPriceFeed(priceFeedDecimals, priceFeedDescription) | ||
} | ||
} | ||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { Wallet, ethers } from "ethers"; | ||
import CloneFactory from '../artifacts/contracts/pricefeed/CloneFactory.sol/CloneFactory.json'; | ||
import testnet_chains from '../testnet_chains.json'; | ||
import mainnet_chains from '../mainnet_chains.json'; | ||
|
||
async function main () { | ||
const priceFeedImplementation = process.env.PRICE_FEED_IMPLEMENTATION_CONTRACT_ADDRESS; | ||
|
||
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 evmChains = testnet_chains.map((chain) => ({ ...chain })); | ||
if (mainnet === "TRUE") { | ||
evmChains = mainnet_chains.map((chain) => ({ ...chain })); | ||
} | ||
|
||
for (const chain of evmChains) { | ||
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 priceFeedFactory = new ethers.ContractFactory(CloneFactory.abi, CloneFactory.bytecode, wallet) | ||
const priceFeed = await priceFeedFactory.deploy(priceFeedImplementation) | ||
console.log(`${chain.name}, address: ${await priceFeed.getAddress()}`); | ||
} | ||
} | ||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { Wallet, ethers } from "ethers"; | ||
import PriceFeed from '../artifacts/contracts/pricefeed/PriceFeed.sol/PriceFeed.json'; | ||
import testnet_chains from '../testnet_chains.json'; | ||
import mainnet_chains from '../mainnet_chains.json'; | ||
|
||
async function main() { | ||
const ojoAddress = process.env.OJO_CONTRACT_ADDRESS; | ||
|
||
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 evmChains = testnet_chains.map((chain) => ({ ...chain })); | ||
if (mainnet === "TRUE") { | ||
evmChains = mainnet_chains.map((chain) => ({ ...chain })); | ||
} | ||
|
||
for (const chain of evmChains) { | ||
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 priceFeedFactory = new ethers.ContractFactory(PriceFeed.abi, PriceFeed.bytecode, wallet) | ||
const priceFeed = await priceFeedFactory.deploy(ojoAddress) | ||
console.log(`${chain.name}, address: ${await priceFeed.getAddress()}`); | ||
} | ||
} | ||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.