Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

[230522 Audit][v1.4] Dev -> Master #168

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
branches:
- master
- dev
- dev2

env:
CI: true
Expand Down
46 changes: 37 additions & 9 deletions packages/perennial-oracle/contracts/ChainlinkFeedOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "./types/ChainlinkAggregator.sol";
* ChainlinkOracle instance if their payoff functions are based on the same underlying oracle.
*/
contract ChainlinkFeedOracle is IOracleProvider {
error InvalidPhaseInitialization();

struct Phase {
uint128 startingVersion;
Expand All @@ -33,22 +34,49 @@ contract ChainlinkFeedOracle is IOracleProvider {
/**
* @notice Initializes the contract state
* @param aggregator_ Chainlink price feed aggregator
* @param phases_ Array of phases to initialize the oracle with
* @dev If `phases_` is empty, the oracle will be initialized with the latest round from the aggregator as the
* starting round
*/
constructor(ChainlinkAggregator aggregator_) {
constructor(ChainlinkAggregator aggregator_, Phase[] memory phases_) {
aggregator = aggregator_;

_decimalOffset = SafeCast.toInt256(10 ** aggregator.decimals());

ChainlinkRound memory firstSeenRound = aggregator.getLatestRound();
if (phases_.length > 0) {
// Phases should be initialized with at least 2 values
if (phases_.length < 2) revert InvalidPhaseInitialization();

// Load the phases array with empty phase values. these phases will be invalid if requested
while (firstSeenRound.phaseId() > _phases.length) {
_phases.push(Phase(0, 0));
}
// Phases[0] should always be empty, since phases are 1-indexed
if (phases_[0].startingVersion != 0 || phases_[0].startingRoundId != 0) revert InvalidPhaseInitialization();

// Phases[1] should start at version 0
if (phases_[1].startingVersion != 0) revert InvalidPhaseInitialization();

// Set the lastSyncedRoundId to the starting round of the latest phase
ChainlinkRound memory latestRound = aggregator.getLatestRound();

// The phases array should be initialized up to the latest phase
if (phases_.length - 1 != latestRound.phaseId()) revert InvalidPhaseInitialization();

// Load phases array with the provided phases
for (uint i = 0; i < phases_.length; i++) {
_phases.push(phases_[i]);
}

_lastSyncedRoundId = latestRound.roundId;
} else {
ChainlinkRound memory firstSeenRound = aggregator.getLatestRound();

// first seen round starts as version 0 at current phase
_phases.push(Phase(0, uint128(firstSeenRound.roundId)));
_lastSyncedRoundId = firstSeenRound.roundId;
// Load the phases array with empty phase values. these phases will be invalid if requested
while (firstSeenRound.phaseId() > _phases.length) {
_phases.push(Phase(0, 0));
}

// first seen round starts as version 0 at current phase
_phases.push(Phase(0, uint128(firstSeenRound.roundId)));
_lastSyncedRoundId = firstSeenRound.roundId;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('ChainlinkFeedOracle', () => {
// This is necessary because smock's mocking is buggy for some reason, likely due to naming collisions
await aggregatorMock._setLatestRoundData(...latestData3)

oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorMock.address)
oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorMock.address, [])
})

describe('#constructor', () => {
Expand All @@ -53,6 +53,82 @@ describe('ChainlinkFeedOracle', () => {
expect(atVersion.timestamp).to.equal(expectedData.updatedAt)
expect(atVersion.version).to.equal(0)
})

context('with phases passed in', () => {
beforeEach(async () => {
const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: buildChainlinkRoundId(3, 123) },
{ startingVersion: 4, startingRoundId: buildChainlinkRoundId(4, 21) },
{ startingVersion: 12, startingRoundId: buildChainlinkRoundId(5, 20000) },
]
oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases)
})

it('sets initial params', async () => {
expect(await oracle.aggregator()).to.equal(aggregatorProxy.address)
})

it('returns version 0', async () => {
const version0Round = buildChainlinkRoundId(3, 123)
const expectedData = await aggregatorProxy.getRoundData(version0Round)

const atVersion = await oracle.atVersion(0)
expect(atVersion.price).to.equal(expectedData.answer.mul(10 ** 10))
expect(atVersion.timestamp).to.equal(expectedData.updatedAt)
expect(atVersion.version).to.equal(0)
})

it('returns a version from a passed in phase', async () => {
// Round from Phase 4
const version12Round = buildChainlinkRoundId(5, 20000)
const expectedData = await aggregatorProxy.getRoundData(version12Round)

const atVersion = await oracle.atVersion(12)
expect(atVersion.price).to.equal(expectedData.answer.mul(10 ** 10))
expect(atVersion.timestamp).to.equal(expectedData.updatedAt)
expect(atVersion.version).to.equal(12)
})

it('reverts if phases array has less than 2 items', async () => {
const initialPhases = [{ startingVersion: 0, startingRoundId: 0 }]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases[0] is non-empty', async () => {
const initialPhases = [
{ startingVersion: 1, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: 0 },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases[1] does not start at version 0', async () => {
const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 1, startingRoundId: 0 },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases array does not reach current phase', async () => {
const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: buildChainlinkRoundId(1, 5) },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})
})
})

describe('#sync', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('ChainlinkFeedOracle', () => {
aggregatorProxy.latestRoundData.returns([initialRound, ethers.BigNumber.from(432100000000), 0, HOUR, initialRound])

aggregatorProxy.decimals.whenCalledWith().returns(8)
oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address)
oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, [])
})

describe('#constructor', () => {
Expand All @@ -53,6 +53,92 @@ describe('ChainlinkFeedOracle', () => {
expect(atVersion.timestamp).to.equal(HOUR)
expect(atVersion.version).to.equal(0)
})

context('with phases passed in', () => {
beforeEach(async () => {
const latestRound = buildChainlinkRoundId(2, 400)
aggregatorProxy.latestRoundData.returns([
latestRound,
ethers.BigNumber.from(123400000000),
0,
HOUR,
latestRound,
])

const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: buildChainlinkRoundId(1, 5) },
{ startingVersion: 12, startingRoundId: buildChainlinkRoundId(2, 123) },
]
oracle = await new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases)
})

it('sets initial params', async () => {
expect(await oracle.aggregator()).to.equal(aggregatorProxy.address)
})

it('returns version 0', async () => {
const version0Round = buildChainlinkRoundId(1, 5)
aggregatorProxy.getRoundData
.whenCalledWith(version0Round)
.returns([version0Round, 432100000000, 0, HOUR, version0Round])

const atVersion = await oracle.atVersion(0)
expect(atVersion.price).to.equal(utils.parseEther('4321'))
expect(atVersion.timestamp).to.equal(HOUR)
expect(atVersion.version).to.equal(0)
})

it('returns a version from a passed in phase', async () => {
// Round from Phase 2
const version12Round = buildChainlinkRoundId(2, 123)
aggregatorProxy.getRoundData
.whenCalledWith(version12Round)
.returns([version12Round, 123400000000, 0, HOUR * 2, version12Round])

const atVersion = await oracle.atVersion(12)
expect(atVersion.price).to.equal(utils.parseEther('1234'))
expect(atVersion.timestamp).to.equal(HOUR * 2)
expect(atVersion.version).to.equal(12)
})

it('reverts if phases array has less than 2 items', async () => {
const initialPhases = [{ startingVersion: 0, startingRoundId: 0 }]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases[0] is non-empty', async () => {
const initialPhases = [
{ startingVersion: 1, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: 0 },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases[1] does not start at version 0', async () => {
const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 1, startingRoundId: 0 },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})

it('reverts if phases array does not reach current phase', async () => {
const initialPhases = [
{ startingVersion: 0, startingRoundId: 0 },
{ startingVersion: 0, startingRoundId: buildChainlinkRoundId(1, 5) },
]
await expect(
new ChainlinkFeedOracle__factory(owner).deploy(aggregatorProxy.address, initialPhases),
).to.be.revertedWithCustomError(oracle, 'InvalidPhaseInitialization')
})
})
})

describe('#sync', () => {
Expand Down
Loading
Loading