-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from perpetual-protocol/feature/price-feed-upd…
…ater PriceFeedUpdater
- Loading branch information
Showing
6 changed files
with
164 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.7.6; | ||
|
||
import { Address } from "@openzeppelin/contracts/utils/Address.sol"; | ||
import { IPriceFeedUpdate } from "./interface/IPriceFeedUpdate.sol"; | ||
|
||
contract PriceFeedUpdater { | ||
using Address for address; | ||
|
||
address[] internal _priceFeeds; | ||
|
||
constructor(address[] memory priceFeedsArg) { | ||
// PFU_PFANC: price feed address is not contract | ||
for (uint256 i = 0; i < priceFeedsArg.length; i++) { | ||
require(priceFeedsArg[i].isContract(), "PFU_PFANC"); | ||
} | ||
|
||
_priceFeeds = priceFeedsArg; | ||
} | ||
|
||
// | ||
// EXTERNAL NON-VIEW | ||
// | ||
|
||
/* solhint-disable payable-fallback */ | ||
fallback() external { | ||
for (uint256 i = 0; i < _priceFeeds.length; i++) { | ||
// Updating PriceFeed might be failed because of price not changed, | ||
// Add try-catch here to update all markets anyway | ||
/* solhint-disable no-empty-blocks */ | ||
try IPriceFeedUpdate(_priceFeeds[i]).update() {} catch {} | ||
} | ||
} | ||
|
||
// | ||
// EXTERNAL VIEW | ||
// | ||
|
||
function getPriceFeeds() external view returns (address[] memory) { | ||
return _priceFeeds; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.7.6; | ||
|
||
interface IPriceFeedUpdate { | ||
/// @dev Update latest price. | ||
function update() external; | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { MockContract, smock } from "@defi-wonderland/smock" | ||
import chai, { expect } from "chai" | ||
import { Wallet } from "ethers" | ||
import { parseEther } from "ethers/lib/utils" | ||
import { ethers, waffle } from "hardhat" | ||
import { | ||
ChainlinkPriceFeedV2, | ||
ChainlinkPriceFeedV2__factory, | ||
PriceFeedUpdater, | ||
TestAggregatorV3__factory, | ||
} from "../typechain" | ||
|
||
chai.use(smock.matchers) | ||
|
||
interface PriceFeedUpdaterFixture { | ||
ethPriceFeed: MockContract<ChainlinkPriceFeedV2> | ||
btcPriceFeed: MockContract<ChainlinkPriceFeedV2> | ||
priceFeedUpdater: PriceFeedUpdater | ||
admin: Wallet | ||
alice: Wallet | ||
} | ||
|
||
describe("PriceFeedUpdater Spec", () => { | ||
const loadFixture: ReturnType<typeof waffle.createFixtureLoader> = waffle.createFixtureLoader() | ||
let fixture: PriceFeedUpdaterFixture | ||
|
||
beforeEach(async () => { | ||
fixture = await loadFixture(createFixture) | ||
}) | ||
afterEach(async () => { | ||
fixture.btcPriceFeed.update.reset() | ||
fixture.ethPriceFeed.update.reset() | ||
}) | ||
|
||
async function executeFallback(priceFeedUpdater: PriceFeedUpdater) { | ||
const { alice } = fixture | ||
await alice.sendTransaction({ | ||
to: priceFeedUpdater.address, | ||
value: 0, | ||
gasLimit: 150000, // Give gas limit to force run transaction without dry run | ||
}) | ||
} | ||
|
||
async function createFixture(): Promise<PriceFeedUpdaterFixture> { | ||
const [admin, alice] = waffle.provider.getWallets() | ||
|
||
const aggregatorFactory = await smock.mock<TestAggregatorV3__factory>("TestAggregatorV3") | ||
const aggregator = await aggregatorFactory.deploy() | ||
|
||
const chainlinkPriceFeedV2Factory = await smock.mock<ChainlinkPriceFeedV2__factory>("ChainlinkPriceFeedV2") | ||
const ethPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 900) | ||
const btcPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 900) | ||
|
||
await ethPriceFeed.deployed() | ||
await btcPriceFeed.deployed() | ||
|
||
const priceFeedUpdaterFactory = await ethers.getContractFactory("PriceFeedUpdater") | ||
const priceFeedUpdater = (await priceFeedUpdaterFactory.deploy([ | ||
ethPriceFeed.address, | ||
btcPriceFeed.address, | ||
])) as PriceFeedUpdater | ||
|
||
return { ethPriceFeed, btcPriceFeed, priceFeedUpdater, admin, alice } | ||
} | ||
it("the result of getPriceFeeds should be same as priceFeeds given when deployment", async () => { | ||
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture | ||
const priceFeeds = await priceFeedUpdater.getPriceFeeds() | ||
expect(priceFeeds).deep.equals([ethPriceFeed.address, btcPriceFeed.address]) | ||
}) | ||
|
||
it("force error, when someone sent eth to contract", async () => { | ||
const { alice, priceFeedUpdater } = fixture | ||
const tx = alice.sendTransaction({ | ||
to: priceFeedUpdater.address, | ||
value: parseEther("0.1"), | ||
gasLimit: 150000, // Give gas limit to force run transaction without dry run | ||
}) | ||
await expect(tx).to.be.reverted | ||
}) | ||
|
||
describe("When priceFeedUpdater fallback execute", () => { | ||
it("should success if all priceFeed are updated successfully", async () => { | ||
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture | ||
|
||
await executeFallback(priceFeedUpdater) | ||
|
||
expect(ethPriceFeed.update).to.have.been.calledOnce | ||
expect(btcPriceFeed.update).to.have.been.calledOnce | ||
}) | ||
it("should still success if any one of priceFeed is updated fail", async () => { | ||
const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture | ||
|
||
ethPriceFeed.update.reverts() | ||
await executeFallback(priceFeedUpdater) | ||
|
||
expect(ethPriceFeed.update).to.have.been.calledOnce | ||
expect(btcPriceFeed.update).to.have.been.calledOnce | ||
}) | ||
}) | ||
}) |