diff --git a/contracts/oracles/ChainlinkOracle.sol b/contracts/oracles/ChainlinkOracle.sol index 2d7505e3..13108e73 100755 --- a/contracts/oracles/ChainlinkOracle.sol +++ b/contracts/oracles/ChainlinkOracle.sol @@ -115,7 +115,7 @@ contract ChainlinkOracle is AccessControlledV8, OracleInterface { * @param asset Address of the asset * @return Price in USD from Chainlink or a manually set price for the asset */ - function getPrice(address asset) public view returns (uint256) { + function getPrice(address asset) public view virtual returns (uint256) { uint256 decimals; if (asset == NATIVE_TOKEN_ADDR) { diff --git a/contracts/oracles/SequencerChainlinkOracle.sol b/contracts/oracles/SequencerChainlinkOracle.sol new file mode 100644 index 00000000..4ee730b7 --- /dev/null +++ b/contracts/oracles/SequencerChainlinkOracle.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { ChainlinkOracle } from "./ChainlinkOracle.sol"; +import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +/** + @title Sequencer Chain Link Oracle + @notice Oracle to fetch price using chainlink oracles on L2s with sequencer +*/ +contract SequencerChainlinkOracle is ChainlinkOracle { + /// @notice L2 Sequencer feed + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + AggregatorV3Interface public immutable sequencer; + + /// @notice L2 Sequencer grace period + uint256 public constant GRACE_PERIOD_TIME = 3600; + + /** + @notice Contract constructor + @param _sequencer L2 sequencer + @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(AggregatorV3Interface _sequencer) ChainlinkOracle() { + require(address(_sequencer) != address(0), "zero address"); + + sequencer = _sequencer; + } + + /// @inheritdoc ChainlinkOracle + function getPrice(address asset) public view override returns (uint) { + if (!isSequencerActive()) revert("L2 sequencer unavailable"); + return super.getPrice(asset); + } + + function isSequencerActive() internal view returns (bool) { + // answer from oracle is a variable with a value of either 1 or 0 + // 0: The sequencer is up + // 1: The sequencer is down + // startedAt: This timestamp indicates when the sequencer changed status + (, int256 answer, uint256 startedAt, , ) = sequencer.latestRoundData(); + if (block.timestamp - startedAt <= GRACE_PERIOD_TIME || answer == 1) return false; + return true; + } +} diff --git a/deploy/1-deploy-oracles.ts b/deploy/1-deploy-oracles.ts index 6259c598..ab176c20 100644 --- a/deploy/1-deploy-oracles.ts +++ b/deploy/1-deploy-oracles.ts @@ -4,6 +4,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { ADDRESSES } from "../helpers/deploymentConfig"; +const ARBITRUM_SEQUENCER = "0xFdB631F5EE196F0ed6FAa767959853A9F217697D"; + const func: DeployFunction = async function ({ getNamedAccounts, deployments, network }: HardhatRuntimeEnvironment) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); @@ -62,21 +64,39 @@ const func: DeployFunction = async function ({ getNamedAccounts, deployments, ne }, }); - await deploy("ChainlinkOracle", { - contract: network.live ? "ChainlinkOracle" : "MockChainlinkOracle", - from: deployer, - log: true, - deterministicDeployment: false, - args: [], - proxy: { - owner: proxyOwnerAddress, - proxyContract: "OptimizedTransparentProxy", - execute: { - methodName: "initialize", - args: network.live ? [accessControlManagerAddress] : [], + if (network.name === "arbitrum") { + await deploy("SequencerChainlinkOracle", { + contract: "SequencerChainlinkOracle", + from: deployer, + log: true, + deterministicDeployment: false, + args: [ARBITRUM_SEQUENCER], + proxy: { + owner: proxyOwnerAddress, + proxyContract: "OptimizedTransparentProxy", + execute: { + methodName: "initialize", + args: [accessControlManagerAddress], + }, }, - }, - }); + }); + } else { + await deploy("ChainlinkOracle", { + contract: network.live ? "ChainlinkOracle" : "MockChainlinkOracle", + from: deployer, + log: true, + deterministicDeployment: false, + args: [], + proxy: { + owner: proxyOwnerAddress, + proxyContract: "OptimizedTransparentProxy", + execute: { + methodName: "initialize", + args: network.live ? [accessControlManagerAddress] : [], + }, + }, + }); + } // Skip deployment if chain is not BNB chain if (networkName === "bsctetnet" || networkName === "bscmainnet") { diff --git a/hardhat.config.ts b/hardhat.config.ts index fb27ff39..ab7f3949 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -130,6 +130,15 @@ const config: HardhatUserConfig = { live: true, accounts: process.env.DEPLOYER_PRIVATE_KEY ? [`0x${process.env.DEPLOYER_PRIVATE_KEY}`] : [], }, + arbitrum: { + url: "https://arbitrum.llamarpc.com/", + chainId: 42161, + live: true, + timeout: 1200000, + accounts: { + mnemonic: process.env.MNEMONIC || "", + }, + }, }, gasReporter: { enabled: process.env.REPORT_GAS !== undefined, diff --git a/test/SequencerChainlinkOracle.ts b/test/SequencerChainlinkOracle.ts new file mode 100644 index 00000000..a0ad0045 --- /dev/null +++ b/test/SequencerChainlinkOracle.ts @@ -0,0 +1,73 @@ +import { FakeContract, MockContract, smock } from "@defi-wonderland/smock"; +import chai from "chai"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers } from "hardhat"; + +import { AggregatorV3Interface, SequencerChainlinkOracle, SequencerChainlinkOracle__factory } from "../typechain-types"; +import { getTime } from "./utils/time"; + +const { expect } = chai; +chai.use(smock.matchers); + +describe("SequencerChainlinkOracle", () => { + let sequencerChainlinkOracle: MockContract; + let sequencerFeed: FakeContract; + const BNB_ADDR = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"; + const GRACE_PERIOD = 3600; + const expectedPrice = parseUnits("1", 18); + before(async () => { + await ethers.provider.send("evm_mine", []); // Mine a block to ensure provider is initialized + sequencerFeed = await smock.fake("AggregatorV3Interface"); + const sequencerChainlinkOracleFactory = await smock.mock( + "SequencerChainlinkOracle", + ); + sequencerChainlinkOracle = await sequencerChainlinkOracleFactory.deploy(sequencerFeed.address); + // configure a hardcoded price just for the sake of returning any value + await sequencerChainlinkOracle.setVariable("prices", { + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB": expectedPrice, // native-token address hardcoded price + }); + }); + it("Should revert if sequencer is down", async () => { + sequencerFeed.latestRoundData.returns({ + roundId: 0, // arbitraty value (not used in logic) + answer: 1, // (1 - for 'sequencer down') + startedAt: 1, // block.timestamp - startedAt should be > 3600 (GRACE_PERIOD) + updatedAt: 1, // arbitraty value (not used in logic) + answeredInRound: 1, // arbitraty value (not used in logic) + }); + + await expect(sequencerChainlinkOracle.getPrice(BNB_ADDR)).to.be.revertedWith("L2 sequencer unavailable"); + }); + it("Should revert if sequencer is up, but GRACE_PERIOD has not passed", async () => { + sequencerFeed.latestRoundData.returns({ + roundId: 0, // arbitraty value (not used in logic) + answer: 1, // (1 - for 'sequencer down') + startedAt: (await getTime()) - GRACE_PERIOD, // block.timestamp - startedAt should be = 3600 (GRACE_PERIOD) + updatedAt: 1, // arbitraty value (not used in logic) + answeredInRound: 1, // arbitraty value (not used in logic) + }); + + await expect(sequencerChainlinkOracle.getPrice(BNB_ADDR)).to.be.revertedWith("L2 sequencer unavailable"); + + sequencerFeed.latestRoundData.returns({ + roundId: 0, // arbitraty value (not used in logic) + answer: 1, // (1 - for 'sequencer down') + startedAt: await getTime(), // block.timestamp - startedAt should be < 3600 (GRACE_PERIOD) + updatedAt: 1, // arbitraty value (not used in logic) + answeredInRound: 1, // arbitraty value (not used in logic) + }); + + await expect(sequencerChainlinkOracle.getPrice(BNB_ADDR)).to.be.revertedWith("L2 sequencer unavailable"); + }); + it("Should return price", async () => { + sequencerFeed.latestRoundData.returns({ + roundId: 0, // arbitraty value (not used in logic) + answer: 0, // (0 - for 'sequencer up') + startedAt: 1, // block.timestamp - startedAt should be > 3600 (GRACE_PERIOD) + updatedAt: 1, // arbitraty value (not used in logic) + answeredInRound: 1, // arbitraty value (not used in logic) + }); + + expect(await sequencerChainlinkOracle.getPrice(BNB_ADDR)).to.equal(expectedPrice); + }); +});