Skip to content

Commit

Permalink
feat: Proxy CL interface (#40)
Browse files Browse the repository at this point in the history
* 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
rbajollari authored May 14, 2024
1 parent aeb2c90 commit 030c37c
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ OJO_CHAIN=ojo
OJO_ADDRESS=ojo1es9mhmnunh208ucwq8rlrl97hqulxrz8k37dcu
RESOLVE_WINDOW=7200
ASSET_LIMIT=5
PRICE_FEED_IMPLEMENTATION_CONTRACT_ADDRESS=0xD1077c12ba7C0ED41d288F5505af2Cb23bBD680a
CLONE_FACTORY_CONTRACT_ADDRESS=0x9AaE2ac2637B9f441d1537bBdCEB712854dd426B
33 changes: 33 additions & 0 deletions contracts/pricefeed/CloneFactory.sol
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);
}
}
128 changes: 128 additions & 0 deletions contracts/pricefeed/PriceFeed.sol
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
);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"author": "rbajollari",
"dependencies": {
"@axelar-network/axelar-gmp-sdk-solidity": "^5.6.4",
"@chainlink/contracts": "^0.6.1",
"@openzeppelin/contracts": "^5.0.0",
"@openzeppelin/contracts-upgradeable": "^5.0.1",
"dotenv": "^16.3.1",
Expand Down
37 changes: 37 additions & 0 deletions scripts/createPriceFeed.ts
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;
});
36 changes: 36 additions & 0 deletions scripts/deployCloneFactory.ts
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;
});
7 changes: 6 additions & 1 deletion scripts/deployMockOjo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Wallet, ethers } from "ethers";
import MockOjo from '../artifacts/contracts/MockOjo.sol/MockOjo.json';
import Create2Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create2Deployer.sol/Create2Deployer.json';
import testnet_chains from '../testnet_chains.json';
import mainnet_chains from '../mainnet_chains.json';

async function main() {
const ojoContractddress = process.env.OJO_CONTRACT_ADDRESS;
Expand All @@ -13,7 +14,11 @@ async function main() {
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
}

const evmChains = testnet_chains.map((chain) => ({ ...chain }));
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)
Expand Down
4 changes: 2 additions & 2 deletions scripts/deployOjo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ async function main() {
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
}

const mainnet = Boolean(process.env.MAINNET)
const mainnet = process.env.MAINNET as string
let evmChains = testnet_chains.map((chain) => ({ ...chain }));
if (mainnet === true) {
if (mainnet === "TRUE") {
evmChains = mainnet_chains.map((chain) => ({ ...chain }));
}

Expand Down
36 changes: 36 additions & 0 deletions scripts/deployPriceFeedImplementation.ts
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;
});
4 changes: 2 additions & 2 deletions scripts/upgradeOjo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ async function main() {
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
}

const mainnet = Boolean(process.env.MAINNET)
const mainnet = process.env.MAINNET as string
let evmChains = testnet_chains.map((chain) => ({ ...chain }));
if (mainnet === true) {
if (mainnet === "TRUE") {
evmChains = mainnet_chains.map((chain) => ({ ...chain }));
}

Expand Down
Loading

0 comments on commit 030c37c

Please sign in to comment.