Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C4: handle Chainlink oracle deprecation #886

Merged
merged 2 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
)
})
})
})