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

Allow the sorted oracle price feed to trigger fallback #24

Open
wants to merge 7 commits into
base: main
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions contracts/interfaces/IPriceFeed.sol
@@ -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);

}
6 changes: 6 additions & 0 deletions contracts/interfaces/IRegistry.sol
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.6.12;

interface IRegistry {
function getAddressForOrDie(bytes32) external view returns (address);
}
10 changes: 10 additions & 0 deletions contracts/interfaces/ISortedOracles.sol
@@ -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);
}
71 changes: 71 additions & 0 deletions contracts/misc/CeloProxyPriceProvider.sol
@@ -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
Copy link
Contributor

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?

/// 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]);
}
}
39 changes: 39 additions & 0 deletions contracts/misc/PriceFeed.sol
@@ -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));
}
}
17 changes: 17 additions & 0 deletions contracts/mocks/oracle/MockPriceFeed.sol
@@ -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;
}
}
14 changes: 14 additions & 0 deletions contracts/mocks/oracle/MockRegistry.sol
@@ -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;
}
}
23 changes: 23 additions & 0 deletions contracts/mocks/oracle/MockSortedOracles.sol
@@ -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;
}
}
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions test/CeloProxyPriceProvider.js
@@ -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");
});
});