Skip to content

Commit

Permalink
C4: handle Chainlink oracle deprecation (#886)
Browse files Browse the repository at this point in the history
  • Loading branch information
jankjr authored Aug 10, 2023
1 parent 30d5bfc commit 4ce086f
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 11 deletions.
9 changes: 9 additions & 0 deletions contracts/plugins/assets/OracleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();

Expand Down
18 changes: 18 additions & 0 deletions contracts/plugins/mocks/ChainlinkMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ 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;
mapping(uint256 => uint256) private getStartedAt;

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;
Expand Down Expand Up @@ -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],
Expand All @@ -102,6 +114,12 @@ contract MockV3Aggregator is AggregatorV3Interface {
uint80 answeredInRound
)
{
if (aggregator == address(0)) {
// solhint-disable-next-line no-inline-assembly
assembly {
revert(0, 0)
}
}
return (
uint80(latestRound),
getAnswer[latestRound],
Expand Down
32 changes: 21 additions & 11 deletions test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -149,19 +149,12 @@ async function gnosisFixture(): Promise<GnosisFixture> {
}
}

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<CollateralFixture> {
) {
const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock')
const USDC: ContractFactory = await ethers.getContractFactory('USDCMock')
const ATokenMockFactory: ContractFactory = await ethers.getContractFactory('StaticATokenMock')
Expand Down Expand Up @@ -349,7 +342,7 @@ async function collateralFixture(
ausdt[0],
abusd[0],
zcoin[0],
]
] as ERC20Mock[]
const collateral = [
dai[1],
usdc[1],
Expand All @@ -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<ReturnType<typeof collateralFixture>>

type RSRAndCompAaveAndCollateralAndModuleFixture = RSRFixture &
COMPAAVEFixture &
CollateralFixture &
Expand Down Expand Up @@ -663,7 +672,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise<DefaultFixture> =
const stRSR: TestIStRSR = <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,
Expand Down Expand Up @@ -742,5 +751,6 @@ const makeDefaultFixture = async (setBasket: boolean): Promise<DefaultFixture> =
facadeTest,
rsrTrader,
rTokenTrader,
bySymbol,
}
}
64 changes: 64 additions & 0 deletions test/plugins/OracleDeprecation.test.ts
Original file line number Diff line number Diff line change
@@ -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'
)
})
})
})

0 comments on commit 4ce086f

Please sign in to comment.