From b400c96a5035d8a8b1ff6f148bc8188d3473f04e Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 9 Aug 2023 11:59:14 +0100 Subject: [PATCH 1/2] fix, update OracleLib to correctly report a StalePrice if oracle has been deprecated --- contracts/plugins/assets/OracleLib.sol | 9 ++++ contracts/plugins/mocks/ChainlinkMock.sol | 20 ++++--- test/fixtures.ts | 32 ++++++++---- test/plugins/OracleDeprecation.test.ts | 64 +++++++++++++++++++++++ 4 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 test/plugins/OracleDeprecation.test.ts diff --git a/contracts/plugins/assets/OracleLib.sol b/contracts/plugins/assets/OracleLib.sol index b0e253876..495186e36 100644 --- a/contracts/plugins/assets/OracleLib.sol +++ b/contracts/plugins/assets/OracleLib.sol @@ -6,6 +6,10 @@ import "../../libraries/Fixed.sol"; error StalePrice(); +interface EACAggregatorProxy { + function aggregator() external view returns (address); +} + /// Used by asset plugins to price their collateral library OracleLib { /// @dev Use for on-the-fly calculations that should revert @@ -16,6 +20,11 @@ library OracleLib { view returns (uint192) { + // If the aggregator is not set, the chainlink feed has been deprecated + if (EACAggregatorProxy(address(chainlinkFeed)).aggregator() == address(0)) { + revert StalePrice(); + } + (uint80 roundId, int256 p, , uint256 updateTime, uint80 answeredInRound) = chainlinkFeed .latestRoundData(); diff --git a/contracts/plugins/mocks/ChainlinkMock.sol b/contracts/plugins/mocks/ChainlinkMock.sol index db794d7ec..c140df319 100644 --- a/contracts/plugins/mocks/ChainlinkMock.sol +++ b/contracts/plugins/mocks/ChainlinkMock.sol @@ -23,6 +23,7 @@ contract MockV3Aggregator is AggregatorV3Interface { // Additional variable to be able to test invalid behavior uint256 public latestAnsweredRound; + address public aggregator; mapping(uint256 => int256) public getAnswer; mapping(uint256 => uint256) public getTimestamp; @@ -30,9 +31,14 @@ contract MockV3Aggregator is AggregatorV3Interface { constructor(uint8 _decimals, int256 _initialAnswer) { decimals = _decimals; + aggregator = address(this); updateAnswer(_initialAnswer); } + function deprecate() external { + aggregator = address(0); + } + function updateAnswer(int256 _answer) public { latestAnswer = _answer; latestTimestamp = block.timestamp; @@ -80,6 +86,12 @@ contract MockV3Aggregator is AggregatorV3Interface { uint80 answeredInRound ) { + if (aggregator == address(0)) { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(0, 0) + } + } return ( _roundId, getAnswer[_roundId], @@ -102,13 +114,7 @@ contract MockV3Aggregator is AggregatorV3Interface { uint80 answeredInRound ) { - return ( - uint80(latestRound), - getAnswer[latestRound], - getStartedAt[latestRound], - getTimestamp[latestRound], - uint80(latestAnsweredRound) - ); + return this.getRoundData(uint80(latestAnsweredRound)); } function description() external pure override returns (string memory) { diff --git a/test/fixtures.ts b/test/fixtures.ts index 2f20d2a66..15944a43b 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1,4 +1,4 @@ -import { BigNumber, ContractFactory } from 'ethers' +import { ContractFactory } from 'ethers' import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { getChainId } from '../common/blockchain-utils' @@ -149,19 +149,12 @@ async function gnosisFixture(): Promise { } } -interface CollateralFixture { - erc20s: ERC20Mock[] // all erc20 addresses - collateral: Collateral[] // all collateral - basket: Collateral[] // only the collateral actively backing the RToken - basketsNeededAmts: BigNumber[] // reference amounts -} - async function collateralFixture( compToken: ERC20Mock, comptroller: ComptrollerMock, aaveToken: ERC20Mock, config: IConfig -): Promise { +) { const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock') const USDC: ContractFactory = await ethers.getContractFactory('USDCMock') const ATokenMockFactory: ContractFactory = await ethers.getContractFactory('StaticATokenMock') @@ -349,7 +342,7 @@ async function collateralFixture( ausdt[0], abusd[0], zcoin[0], - ] + ] as ERC20Mock[] const collateral = [ dai[1], usdc[1], @@ -374,9 +367,25 @@ async function collateralFixture( collateral, basket, basketsNeededAmts, + bySymbol: { + dai, + usdc, + usdt, + busd, + cdai, + cusdc, + cusdt, + adai, + ausdc, + ausdt, + abusd, + zcoin, + }, } } +type CollateralFixture = Awaited> + type RSRAndCompAaveAndCollateralAndModuleFixture = RSRFixture & COMPAAVEFixture & CollateralFixture & @@ -663,7 +672,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = const stRSR: TestIStRSR = await ethers.getContractAt('TestIStRSR', await main.stRSR()) // Deploy collateral for Main - const { erc20s, collateral, basket, basketsNeededAmts } = await collateralFixture( + const { erc20s, collateral, basket, basketsNeededAmts, bySymbol } = await collateralFixture( compToken, compoundMock, aaveToken, @@ -742,5 +751,6 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = facadeTest, rsrTrader, rTokenTrader, + bySymbol, } } diff --git a/test/plugins/OracleDeprecation.test.ts b/test/plugins/OracleDeprecation.test.ts new file mode 100644 index 000000000..d76d16df0 --- /dev/null +++ b/test/plugins/OracleDeprecation.test.ts @@ -0,0 +1,64 @@ +import { Wallet } from 'ethers' +import { ethers } from 'hardhat' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { fp } from '../../common/numbers' +import { ERC20Mock, TestIRToken } from '../../typechain' +import { Collateral, DefaultFixture, defaultFixture } from '../fixtures' +import { expect } from 'chai' + +describe('Chainlink Oracle', () => { + // Tokens + let rsr: ERC20Mock + let compToken: ERC20Mock + let aaveToken: ERC20Mock + let rToken: TestIRToken + + // Assets + let basket: Collateral[] + + let wallet: Wallet + + const amt = fp('1e4') + let fixture: DefaultFixture + + before('create fixture loader', async () => { + ;[wallet] = (await ethers.getSigners()) as unknown as Wallet[] + }) + + beforeEach(async () => { + // Deploy fixture + fixture = await loadFixture(defaultFixture) + ;({ rsr, compToken, aaveToken, basket, rToken } = fixture) + + // Get collateral tokens + await rsr.connect(wallet).mint(wallet.address, amt) + await compToken.connect(wallet).mint(wallet.address, amt) + await aaveToken.connect(wallet).mint(wallet.address, amt) + + // Issue RToken to enable RToken.price + for (let i = 0; i < basket.length; i++) { + const tok = await ethers.getContractAt('ERC20Mock', await basket[i].erc20()) + await tok.connect(wallet).mint(wallet.address, amt) + await tok.connect(wallet).approve(rToken.address, amt) + } + await rToken.connect(wallet).issue(amt) + }) + + describe('Chainlink deprecates an asset', () => { + it('Refresh should mark the asset as IFFY', async () => { + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const [, aUSDCCollateral] = fixture.bySymbol.ausdc + const chainLinkOracle = MockV3AggregatorFactory.attach(await aUSDCCollateral.chainlinkFeed()) + await aUSDCCollateral.refresh() + await aUSDCCollateral.tryPrice() + expect(await aUSDCCollateral.status()).to.equal(0) + await chainLinkOracle.deprecate() + await aUSDCCollateral.refresh() + expect(await aUSDCCollateral.status()).to.equal(1) + await expect(aUSDCCollateral.tryPrice()).to.be.revertedWithCustomError( + aUSDCCollateral, + 'StalePrice' + ) + }) + }) +}) From 31394fdd52e2f16595dff36949076804b85e3f81 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 10 Aug 2023 20:38:59 +0100 Subject: [PATCH 2/2] revert controlflow change --- contracts/plugins/mocks/ChainlinkMock.sol | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contracts/plugins/mocks/ChainlinkMock.sol b/contracts/plugins/mocks/ChainlinkMock.sol index c140df319..6e0fee678 100644 --- a/contracts/plugins/mocks/ChainlinkMock.sol +++ b/contracts/plugins/mocks/ChainlinkMock.sol @@ -114,7 +114,19 @@ contract MockV3Aggregator is AggregatorV3Interface { uint80 answeredInRound ) { - return this.getRoundData(uint80(latestAnsweredRound)); + if (aggregator == address(0)) { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(0, 0) + } + } + return ( + uint80(latestRound), + getAnswer[latestRound], + getStartedAt[latestRound], + getTimestamp[latestRound], + uint80(latestAnsweredRound) + ); } function description() external pure override returns (string memory) {