Skip to content

Commit 5042fb8

Browse files
authored
Merge pull request #38 from perpetual-protocol/feature/price-feed-updater
PriceFeedUpdater
2 parents 3945f1f + fc78ee5 commit 5042fb8

File tree

6 files changed

+164
-13
lines changed

6 files changed

+164
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [unreleased]
8+
## [0.5.0] - 2022-08-23
9+
10+
- Add `PriceFeedUpdater`
911

1012
## [0.4.2] - 2022-06-08
1113

contracts/PriceFeedUpdater.sol

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity 0.7.6;
3+
4+
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
5+
import { IPriceFeedUpdate } from "./interface/IPriceFeedUpdate.sol";
6+
7+
contract PriceFeedUpdater {
8+
using Address for address;
9+
10+
address[] internal _priceFeeds;
11+
12+
constructor(address[] memory priceFeedsArg) {
13+
// PFU_PFANC: price feed address is not contract
14+
for (uint256 i = 0; i < priceFeedsArg.length; i++) {
15+
require(priceFeedsArg[i].isContract(), "PFU_PFANC");
16+
}
17+
18+
_priceFeeds = priceFeedsArg;
19+
}
20+
21+
//
22+
// EXTERNAL NON-VIEW
23+
//
24+
25+
/* solhint-disable payable-fallback */
26+
fallback() external {
27+
for (uint256 i = 0; i < _priceFeeds.length; i++) {
28+
// Updating PriceFeed might be failed because of price not changed,
29+
// Add try-catch here to update all markets anyway
30+
/* solhint-disable no-empty-blocks */
31+
try IPriceFeedUpdate(_priceFeeds[i]).update() {} catch {}
32+
}
33+
}
34+
35+
//
36+
// EXTERNAL VIEW
37+
//
38+
39+
function getPriceFeeds() external view returns (address[] memory) {
40+
return _priceFeeds;
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity 0.7.6;
3+
4+
interface IPriceFeedUpdate {
5+
/// @dev Update latest price.
6+
function update() external;
7+
}

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@perp/perp-oracle-contract",
3-
"version": "0.4.3",
4-
"description": "Perpetual Protocol Curie (v2) oracle contracts - v0.3.4 is not an audited version",
3+
"version": "0.5.0",
4+
"description": "Perpetual Protocol Curie (v2) oracle contracts - v0.5.0 is not an audited version",
55
"license": "GPL-3.0-or-later",
66
"author": {
77
"name": "Perpetual Protocol",
@@ -42,7 +42,7 @@
4242
},
4343
"devDependencies": {
4444
"@chainlink/contracts": "0.1.7",
45-
"@defi-wonderland/smock": "2.0.7",
45+
"@defi-wonderland/smock": "2.2.0",
4646
"@nomiclabs/hardhat-ethers": "2.0.5",
4747
"@nomiclabs/hardhat-waffle": "2.0.3",
4848
"@openzeppelin/contracts": "3.4.0",

test/PriceFeedUpdater.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { MockContract, smock } from "@defi-wonderland/smock"
2+
import chai, { expect } from "chai"
3+
import { Wallet } from "ethers"
4+
import { parseEther } from "ethers/lib/utils"
5+
import { ethers, waffle } from "hardhat"
6+
import {
7+
ChainlinkPriceFeedV2,
8+
ChainlinkPriceFeedV2__factory,
9+
PriceFeedUpdater,
10+
TestAggregatorV3__factory,
11+
} from "../typechain"
12+
13+
chai.use(smock.matchers)
14+
15+
interface PriceFeedUpdaterFixture {
16+
ethPriceFeed: MockContract<ChainlinkPriceFeedV2>
17+
btcPriceFeed: MockContract<ChainlinkPriceFeedV2>
18+
priceFeedUpdater: PriceFeedUpdater
19+
admin: Wallet
20+
alice: Wallet
21+
}
22+
23+
describe("PriceFeedUpdater Spec", () => {
24+
const loadFixture: ReturnType<typeof waffle.createFixtureLoader> = waffle.createFixtureLoader()
25+
let fixture: PriceFeedUpdaterFixture
26+
27+
beforeEach(async () => {
28+
fixture = await loadFixture(createFixture)
29+
})
30+
afterEach(async () => {
31+
fixture.btcPriceFeed.update.reset()
32+
fixture.ethPriceFeed.update.reset()
33+
})
34+
35+
async function executeFallback(priceFeedUpdater: PriceFeedUpdater) {
36+
const { alice } = fixture
37+
await alice.sendTransaction({
38+
to: priceFeedUpdater.address,
39+
value: 0,
40+
gasLimit: 150000, // Give gas limit to force run transaction without dry run
41+
})
42+
}
43+
44+
async function createFixture(): Promise<PriceFeedUpdaterFixture> {
45+
const [admin, alice] = waffle.provider.getWallets()
46+
47+
const aggregatorFactory = await smock.mock<TestAggregatorV3__factory>("TestAggregatorV3")
48+
const aggregator = await aggregatorFactory.deploy()
49+
50+
const chainlinkPriceFeedV2Factory = await smock.mock<ChainlinkPriceFeedV2__factory>("ChainlinkPriceFeedV2")
51+
const ethPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 900)
52+
const btcPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 900)
53+
54+
await ethPriceFeed.deployed()
55+
await btcPriceFeed.deployed()
56+
57+
const priceFeedUpdaterFactory = await ethers.getContractFactory("PriceFeedUpdater")
58+
const priceFeedUpdater = (await priceFeedUpdaterFactory.deploy([
59+
ethPriceFeed.address,
60+
btcPriceFeed.address,
61+
])) as PriceFeedUpdater
62+
63+
return { ethPriceFeed, btcPriceFeed, priceFeedUpdater, admin, alice }
64+
}
65+
it("the result of getPriceFeeds should be same as priceFeeds given when deployment", async () => {
66+
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture
67+
const priceFeeds = await priceFeedUpdater.getPriceFeeds()
68+
expect(priceFeeds).deep.equals([ethPriceFeed.address, btcPriceFeed.address])
69+
})
70+
71+
it("force error, when someone sent eth to contract", async () => {
72+
const { alice, priceFeedUpdater } = fixture
73+
const tx = alice.sendTransaction({
74+
to: priceFeedUpdater.address,
75+
value: parseEther("0.1"),
76+
gasLimit: 150000, // Give gas limit to force run transaction without dry run
77+
})
78+
await expect(tx).to.be.reverted
79+
})
80+
81+
describe("When priceFeedUpdater fallback execute", () => {
82+
it("should success if all priceFeed are updated successfully", async () => {
83+
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture
84+
85+
await executeFallback(priceFeedUpdater)
86+
87+
expect(ethPriceFeed.update).to.have.been.calledOnce
88+
expect(btcPriceFeed.update).to.have.been.calledOnce
89+
})
90+
it("should still success if any one of priceFeed is updated fail", async () => {
91+
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture
92+
93+
ethPriceFeed.update.reverts()
94+
await executeFallback(priceFeedUpdater)
95+
96+
expect(ethPriceFeed.update).to.have.been.calledOnce
97+
expect(btcPriceFeed.update).to.have.been.calledOnce
98+
})
99+
})
100+
})

0 commit comments

Comments
 (0)