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
Allow the sorted oracle price feed to trigger fallback #24
Open
diwu1989
wants to merge
7
commits into
moolamarket:main
Choose a base branch
from
diwu1989:sortedOracleFallback
base: main
Could not load branches
Branch not found: {{ refName }}
Could not load tags
Nothing to show
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
41cc731
CeloProxyPriceProvider added
2944eff
CeloProxyPriceProvider added
27c2b14
PriceFeed added
09cd6e6
added PriceFeed and tests
a251430
added PriceFeed and tests + fix
4ad1654
small mix
9699c56
update sorted oracle price feed to return fallback
diwu1989 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
pragma solidity 0.6.12; | ||
|
||
/************ | ||
@title IPriceFeed interface | ||
@notice Interface for the Aave price oracle.*/ | ||
interface IPriceFeed { | ||
|
||
// note this will always return 0 before update has been called successfully for the first time. | ||
function consult() external view returns (uint); | ||
|
||
} |
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,6 @@ | ||
// SPDX-License-Identifier: agpl-3.0 | ||
pragma solidity 0.6.12; | ||
|
||
interface IRegistry { | ||
function getAddressForOrDie(bytes32) external view returns (address); | ||
} |
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,10 @@ | ||
// SPDX-License-Identifier: agpl-3.0 | ||
pragma solidity 0.6.12; | ||
|
||
interface ISortedOracles { | ||
function medianRate(address) external view returns (uint256, uint256); | ||
|
||
function medianTimestamp(address) external view returns (uint256); | ||
|
||
function isOldestReportExpired(address token) external view returns (bool, address); | ||
} |
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,71 @@ | ||
pragma solidity 0.6.12; | ||
|
||
import "@openzeppelin/contracts/math/SafeMath.sol"; | ||
import "../dependencies/openzeppelin/contracts/Ownable.sol"; | ||
|
||
import "../interfaces/IPriceOracleGetter.sol"; | ||
import "../interfaces/IPriceFeed.sol"; | ||
import "../interfaces/IRegistry.sol"; | ||
|
||
/// @title CeloProxyPriceProvider | ||
/// @author Moola | ||
/// @notice Proxy smart contract to get the price of an asset from a price source, with Celo SortedOracles | ||
/// smart contracts as primary option | ||
/// - If the returned price by a SortedOracles is <= 0, the call is forwarded to a fallbackOracle | ||
/// - Owned by the Aave governance system, allowed to add sources for assets, replace them | ||
/// and change the fallbackOracle | ||
|
||
contract CeloProxyPriceProvider is IPriceOracleGetter, Ownable { | ||
using SafeMath for uint256; | ||
|
||
mapping(address => address) internal priceFeeds; | ||
IRegistry public immutable registry; | ||
|
||
constructor(address[] memory _assets, address[] memory _priceFeeds, address _registry) public { | ||
updateAssets(_assets, _priceFeeds); | ||
registry = IRegistry(_registry); | ||
} | ||
|
||
function updateAssets(address[] memory _assets, address[] memory _priceFeeds) public onlyOwner { | ||
|
||
require( | ||
_assets.length == _priceFeeds.length, | ||
"the quantity does not match" | ||
); | ||
|
||
for (uint256 i = 0; i < _assets.length; i++) { | ||
priceFeeds[_assets[i]] = _priceFeeds[i]; | ||
} | ||
} | ||
|
||
/// @notice Gets an asset price by address | ||
/// @param _asset The asset address | ||
function getAssetPrice(address _asset) public view override returns (uint256) { | ||
if (_asset == registry.getAddressForOrDie(keccak256(abi.encodePacked("GoldToken")))) { | ||
return 1 ether; | ||
} | ||
|
||
return (IPriceFeed(priceFeeds[_asset]).consult()); | ||
} | ||
|
||
/// @notice Gets a list of prices from a list of assets addresses | ||
/// @param _assets The list of assets addresses | ||
function getAssetsPrices(address[] memory _assets) | ||
public | ||
view | ||
returns (uint256[] memory) | ||
{ | ||
uint256[] memory prices = new uint256[](_assets.length); | ||
for (uint256 i = 0; i < _assets.length; i++) { | ||
prices[i] = getAssetPrice(_assets[i]); | ||
} | ||
|
||
return prices; | ||
} | ||
|
||
/// @notice Gets the address of the fallback oracle | ||
/// @return address The addres of the fallback oracle | ||
function getPriceFeed(address _asset) public view returns (address) { | ||
return (priceFeeds[_asset]); | ||
} | ||
} |
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,39 @@ | ||
pragma solidity 0.6.12; | ||
|
||
import '@openzeppelin/contracts/math/SafeMath.sol'; | ||
import '../interfaces/IPriceOracleGetter.sol'; | ||
import '../interfaces/IRegistry.sol'; | ||
import '../interfaces/ISortedOracles.sol'; | ||
|
||
contract PriceFeed { | ||
using SafeMath for uint256; | ||
|
||
address private immutable asset; | ||
IRegistry public immutable registry; | ||
bytes32 constant SORTED_ORACLES_REGISTRY_ID = keccak256(abi.encodePacked('SortedOracles')); | ||
|
||
constructor(address _asset, address _registry) public { | ||
asset = _asset; | ||
registry = IRegistry(_registry); | ||
} | ||
|
||
function consult() external view returns (uint256) { | ||
uint256 _price; | ||
uint256 _divisor; | ||
bool _expired; | ||
ISortedOracles _oracles = getSortedOracles(); | ||
(_price, _divisor) = _oracles.medianRate(asset); | ||
require(_price > 0, 'Reported price is 0'); | ||
|
||
(_expired, ) = _oracles.isOldestReportExpired(asset); | ||
if (_expired) { | ||
// return 0 to trigger fallback | ||
return 0; | ||
} | ||
return _divisor.mul(1 ether).div(_price); | ||
} | ||
|
||
function getSortedOracles() internal view returns (ISortedOracles) { | ||
return ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); | ||
} | ||
} |
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,17 @@ | ||
pragma solidity 0.6.12; | ||
import "../../interfaces/IPriceFeed.sol"; | ||
|
||
contract MockPriceFeed is IPriceFeed{ | ||
uint price; | ||
|
||
constructor(address _pair, address _tokenA, address _tokenB) public {} | ||
|
||
function consult() external view override returns (uint) { | ||
|
||
return price; | ||
} | ||
|
||
function setPrice(uint _price) public { | ||
price = _price; | ||
} | ||
} |
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,14 @@ | ||
pragma solidity 0.6.12; | ||
import "../../interfaces/IRegistry.sol"; | ||
|
||
contract MockRegistry is IRegistry{ | ||
address _address; | ||
|
||
constructor(address __address) public { | ||
_address = __address; | ||
} | ||
|
||
function getAddressForOrDie(bytes32) external view override returns (address) { | ||
return _address; | ||
} | ||
} |
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,23 @@ | ||
pragma solidity 0.6.12; | ||
|
||
import '../../interfaces/ISortedOracles.sol'; | ||
|
||
contract MockSortedOracles is ISortedOracles { | ||
bool public expired; | ||
|
||
function medianRate(address) external view override returns (uint256, uint256) { | ||
return (1, 1); | ||
} | ||
|
||
function medianTimestamp(address) external view override returns (uint256) { | ||
return block.timestamp; | ||
} | ||
|
||
function isOldestReportExpired(address) public view override returns (bool, address) { | ||
return (expired, address(0)); | ||
} | ||
|
||
function setExpired(bool _expired) public { | ||
expired = _expired; | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
const Ganache = require("./helpers/ganache"); | ||
const { expect, assert, util } = require("chai"); | ||
const { BigNumber, utils, providers } = require("ethers"); | ||
const { ethers } = require("hardhat"); | ||
|
||
describe("CeloProxyPriceProvider", function () { | ||
const ganache = new Ganache(); | ||
|
||
let owner; | ||
let user; | ||
let liquidityDistributor; | ||
|
||
let token1; | ||
let token2; | ||
let token3; | ||
|
||
let priceFeed1; | ||
let priceFeed2; | ||
|
||
let celoProxyPriceProvider; | ||
|
||
before("setup", async () => { | ||
let accounts = await ethers.getSigners(); | ||
|
||
owner = accounts[0]; | ||
user = accounts[1]; | ||
liquidityDistributor = accounts[3]; | ||
|
||
const Token = await ethers.getContractFactory("MintableERC20"); | ||
|
||
token1 = await Token.deploy("", "", 18); | ||
await token1.deployed(); | ||
|
||
token2 = await Token.deploy("", "", 0); | ||
await token2.deployed(); | ||
|
||
token3 = await Token.deploy("", "", 0); | ||
await token3.deployed(); | ||
|
||
const PriceFeed = await ethers.getContractFactory("MockPriceFeed"); | ||
const emptyAddress = ethers.utils.getAddress("0x0000000000000000000000000000000000000000"); | ||
priceFeed1 = await PriceFeed.deploy(emptyAddress, token1.address, token2.address); | ||
await priceFeed1.deployed(); | ||
await priceFeed1.setPrice(ethers.constants.WeiPerEther); | ||
|
||
priceFeed2 = await PriceFeed.deploy(emptyAddress, token1.address, token2.address); | ||
await priceFeed2.deployed(); | ||
await priceFeed2.setPrice(ethers.constants.WeiPerEther.div(2)); | ||
|
||
const Registry = await ethers.getContractFactory("MockRegistry"); | ||
const goldTockenAddress = ethers.utils.getAddress("0x34d6a0f5c2f5d0082141fe73d93b9dd00ca7ce11"); | ||
const registry = await Registry.deploy(goldTockenAddress); | ||
await registry.deployed(); | ||
|
||
const CeloProxyPriceProvider = await ethers.getContractFactory("CeloProxyPriceProvider"); | ||
celoProxyPriceProvider = await CeloProxyPriceProvider.deploy([token1.address, token2.address], [priceFeed1.address, priceFeed2.address], registry.address); | ||
await celoProxyPriceProvider.deployed(); | ||
|
||
await ganache.snapshot(); | ||
}); | ||
|
||
afterEach("revert", function () { | ||
return ganache.revert(); | ||
}); | ||
|
||
//positive tests | ||
|
||
it("should check updateAssets", async () => { | ||
await celoProxyPriceProvider.connect(owner).updateAssets([token1.address, token2.address], [priceFeed2.address, priceFeed1.address]); | ||
|
||
const asset1Price = await celoProxyPriceProvider.getAssetPrice(token1.address); | ||
const asset2Price = await celoProxyPriceProvider.getAssetPrice(token2.address); | ||
|
||
expect(asset1Price).to.equal(ethers.constants.WeiPerEther.div(2)); | ||
expect(asset2Price).to.equal(ethers.constants.WeiPerEther); | ||
}); | ||
|
||
it("should check getAssetPrice", async () => { | ||
const asset1Price = await celoProxyPriceProvider.getAssetPrice(token1.address); | ||
const asset2Price = await celoProxyPriceProvider.getAssetPrice(token2.address); | ||
|
||
expect(asset1Price).to.equal(ethers.constants.WeiPerEther); | ||
expect(asset2Price).to.equal(ethers.constants.WeiPerEther.div(2)); | ||
}); | ||
|
||
it("should check getAssetsPrices", async () => { | ||
const assetsPrice = await celoProxyPriceProvider.getAssetsPrices([token1.address, token2.address]); | ||
|
||
expect(assetsPrice[0]).to.equal(ethers.constants.WeiPerEther); | ||
expect(assetsPrice[1]).to.equal(ethers.constants.WeiPerEther.div(2)); | ||
}); | ||
|
||
it("should check getPriceFeed", async () => { | ||
const priceFeed1Address = await celoProxyPriceProvider.getPriceFeed(token1.address); | ||
const priceFeed2Address = await celoProxyPriceProvider.getPriceFeed(token2.address); | ||
|
||
expect(priceFeed1Address).to.equal(priceFeed1.address); | ||
expect(priceFeed2Address).to.equal(priceFeed2.address); | ||
}); | ||
|
||
//negative tests | ||
|
||
it("should not allow return unknown assets price", async () => { | ||
await expect( | ||
celoProxyPriceProvider.getAssetPrice(token3.address) | ||
).to.be.revertedWith("Transaction reverted: function call to a non-contract account"); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably should change to "Moola governance" here?