-
Notifications
You must be signed in to change notification settings - Fork 168
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
15 changed files
with
1,060 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
import { ISubOracle } from "../../interfaces/oracle/ISubOracle.sol"; | ||
|
||
/// @title Beefy Oracle | ||
/// @author Beefy, @kexley | ||
/// @notice On-chain oracle using various sources | ||
contract BeefyOracle is OwnableUpgradeable { | ||
|
||
/// @dev Struct for the latest price of a token with the timestamp | ||
/// @param price Stored price | ||
/// @param timestamp Last update timestamp | ||
struct LatestPrice { | ||
uint256 price; | ||
uint256 timestamp; | ||
} | ||
|
||
/// @dev Struct for delegating the price calculation to a library using stored data | ||
/// @param oracle Address of the library for a particular oracle type | ||
/// @param data Stored data for calculating the price of a specific token using the library | ||
struct SubOracle { | ||
address oracle; | ||
bytes data; | ||
} | ||
|
||
/// @notice Latest price of a token with a timestamp | ||
mapping(address => LatestPrice) public latestPrice; | ||
|
||
/// @notice Oracle library address and payload for delegating the price calculation of a token | ||
mapping(address => SubOracle) public subOracle; | ||
|
||
/// @notice Length of time in seconds before a price goes stale | ||
uint256 public staleness; | ||
|
||
/// @notice Price of a token has been updated | ||
event PriceUpdated(address indexed token, uint256 price, uint256 timestamp); | ||
|
||
/// @notice New oracle has been set | ||
event SetOracle(address indexed token, address oracle, bytes data); | ||
|
||
/// @notice New staleness has been set | ||
event SetStaleness(uint256 staleness); | ||
|
||
/// @notice Initialize the contract | ||
/// @dev Ownership is transferred to msg.sender | ||
function initialize() external initializer { | ||
__Ownable_init(); | ||
} | ||
|
||
/// @notice Fetch the most recent stored price for a token | ||
/// @param _token Address of the token being fetched | ||
/// @return price Price of the token | ||
function getPrice(address _token) external view returns (uint256 price) { | ||
price = latestPrice[_token].price; | ||
} | ||
|
||
/// @notice Fetch the most recent stored price for an array of tokens | ||
/// @param _tokens Addresses of the tokens being fetched | ||
/// @return prices Prices of the tokens | ||
function getPrice(address[] calldata _tokens) external view returns (uint256[] memory prices) { | ||
for (uint i; i < _tokens.length; i++) { | ||
prices[i] = latestPrice[_tokens[i]].price; | ||
} | ||
} | ||
|
||
/// @notice Fetch an updated price for a token | ||
/// @param _token Address of the token being fetched | ||
/// @return price Updated price of the token | ||
/// @return success Price update was success or not | ||
function getFreshPrice(address _token) external returns (uint256 price, bool success) { | ||
(price, success) = _getFreshPrice(_token); | ||
} | ||
|
||
/// @notice Fetch updated prices for an array of tokens | ||
/// @param _tokens Addresses of the tokens being fetched | ||
/// @return prices Updated prices of the tokens | ||
/// @return successes Price updates were a success or not | ||
function getFreshPrice( | ||
address[] calldata _tokens | ||
) external returns (uint256[] memory prices, bool[] memory successes) { | ||
for (uint i; i < _tokens.length; i++) { | ||
(prices[i], successes[i]) = _getFreshPrice(_tokens[i]); | ||
} | ||
} | ||
|
||
/// @dev If the price is stale then calculate a new price by delegating to the sub oracle | ||
/// @param _token Address of the token being fetched | ||
/// @return price Updated price of the token | ||
/// @return success Price update was success or not | ||
function _getFreshPrice(address _token) private returns (uint256 price, bool success) { | ||
if (latestPrice[_token].timestamp + staleness > block.timestamp) { | ||
price = latestPrice[_token].price; | ||
success = true; | ||
} else { | ||
(price, success) = ISubOracle(subOracle[_token].oracle).getPrice(subOracle[_token].data); | ||
if (success) { | ||
latestPrice[_token] = LatestPrice({price: price, timestamp: block.timestamp}); | ||
emit PriceUpdated(_token, price, block.timestamp); | ||
} | ||
} | ||
} | ||
|
||
/// @notice Owner function to set a sub oracle and data for a token | ||
/// @dev The payload will be validated against the library | ||
/// @param _token Address of the token being fetched | ||
/// @param _oracle Address of the library used to calculate the price | ||
/// @param _data Payload specific to the token that will be used by the library | ||
function setOracle(address _token, address _oracle, bytes calldata _data) external onlyOwner { | ||
_setOracle(_token, _oracle, _data); | ||
} | ||
|
||
/// @notice Owner function to set a sub oracle and data for an array of tokens | ||
/// @dev The payloads will be validated against the libraries | ||
/// @param _tokens Addresses of the tokens being fetched | ||
/// @param _oracles Addresses of the libraries used to calculate the price | ||
/// @param _datas Payloads specific to the tokens that will be used by the libraries | ||
function setOracle( | ||
address[] calldata _tokens, | ||
address[] calldata _oracles, | ||
bytes[] calldata _datas | ||
) external onlyOwner { | ||
for (uint i; i < _tokens.length;) { | ||
_setOracle(_tokens[i], _oracles[i], _datas[i]); | ||
unchecked { i++; } | ||
} | ||
} | ||
|
||
/// @dev Set the sub oracle and data for a token, it also validates that the data is correct | ||
/// @param _token Address of the token being fetched | ||
/// @param _oracle Address of the library used to calculate the price | ||
/// @param _data Payload specific to the token that will be used by the library | ||
function _setOracle(address _token, address _oracle, bytes calldata _data) private { | ||
ISubOracle(_oracle).validateData(_data); | ||
subOracle[_token] = SubOracle({oracle: _oracle, data: _data}); | ||
emit SetOracle(_token, _oracle, _data); | ||
} | ||
|
||
/// @notice Owner function to set the staleness | ||
/// @param _staleness Length of time in seconds before a price becomes stale | ||
function setStaleness(uint256 _staleness) external onlyOwner { | ||
staleness = _staleness; | ||
emit SetStaleness(_staleness); | ||
} | ||
} |
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 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import { IChainlink } from "../../interfaces/oracle/IChainlink.sol"; | ||
import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; | ||
|
||
/// @title Beefy Oracle using Chainlink | ||
/// @author Beefy, @kexley | ||
/// @notice On-chain oracle using Chainlink | ||
library BeefyOracleChainlink { | ||
|
||
/// @dev No response from the Chainlink feed | ||
error NoAnswer(); | ||
|
||
/// @notice Fetch price from the Chainlink feed and scale to 18 decimals | ||
/// @param _data Payload from the central oracle with the address of the Chainlink feed | ||
/// @return price Retrieved price from the Chainlink feed | ||
/// @return success Successful price fetch or not | ||
function getPrice(bytes calldata _data) external view returns (uint256 price, bool success) { | ||
address chainlink = abi.decode(_data, (address)); | ||
try IChainlink(chainlink).decimals() returns (uint8 decimals) { | ||
try IChainlink(chainlink).latestAnswer() returns (int256 latestAnswer) { | ||
price = BeefyOracleHelper.scaleAmount(uint256(latestAnswer), decimals); | ||
success = true; | ||
} catch {} | ||
} catch {} | ||
} | ||
|
||
/// @notice Data validation for new oracle data being added to central oracle | ||
/// @param _data Encoded Chainlink feed address | ||
function validateData(bytes calldata _data) external view { | ||
address chainlink = abi.decode(_data, (address)); | ||
try IChainlink(chainlink).decimals() returns (uint8) { | ||
try IChainlink(chainlink).latestAnswer() returns (int256) { | ||
} catch { revert NoAnswer(); } | ||
} catch { revert NoAnswer(); } | ||
} | ||
} |
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: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import { IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; | ||
import { IBeefyOracle } from "../../interfaces/oracle/IBeefyOracle.sol"; | ||
|
||
/// @title Beefy Oracle Helper | ||
/// @author Beefy, @kexley | ||
/// @notice Helper functions for Beefy oracles | ||
library BeefyOracleHelper { | ||
|
||
/// @dev Calculate the price of the output token in 18 decimals given the base token price | ||
/// and the amount out received from swapping 1 unit of the base token | ||
/// @param _oracle Central Beefy oracle which stores the base token price | ||
/// @param _token Address of token to calculate the price of | ||
/// @param _baseToken Address of the base token used in the price chain | ||
/// @param _amountOut Amount received from swapping 1 unit of base token into the target token | ||
/// @return price Price of the target token in 18 decimals | ||
function priceFromBaseToken( | ||
address _oracle, | ||
address _token, | ||
address _baseToken, | ||
uint256 _amountOut | ||
) internal returns (uint256 price) { | ||
(uint256 basePrice,) = IBeefyOracle(_oracle).getFreshPrice(_baseToken); | ||
uint8 decimals = IERC20MetadataUpgradeable(_token).decimals(); | ||
_amountOut = scaleAmount(_amountOut, decimals); | ||
price = _amountOut * 1 ether / basePrice; | ||
} | ||
|
||
/// @dev Scale an input amount to 18 decimals | ||
/// @param _amount Amount to be scaled up or down | ||
/// @param _decimals Decimals that the amount is already in | ||
/// @return scaledAmount New scaled amount in 18 decimals | ||
function scaleAmount( | ||
uint256 _amount, | ||
uint8 _decimals | ||
) internal pure returns (uint256 scaledAmount) { | ||
scaledAmount = _decimals == 18 ? _amount : _amount * 10 ** 18 / 10 ** _decimals; | ||
} | ||
} |
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,67 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; | ||
import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; | ||
import { ISolidlyPair} from "../../interfaces/common/ISolidlyPair.sol"; | ||
|
||
/// @title Beefy Oracle for Solidly | ||
/// @author Beefy, @kexley | ||
/// @notice On-chain oracle using Solidly | ||
library BeefyOracleSolidly { | ||
|
||
/// @dev Array length is not correct | ||
error ArrayLength(); | ||
|
||
/// @dev No price for base token | ||
/// @param token Base token | ||
error NoBasePrice(address token); | ||
|
||
/// @dev Token is not present in the pair | ||
/// @param token Input token | ||
/// @param pair Solidly pair | ||
error TokenNotInPair(address token, address pair); | ||
|
||
/// @notice Fetch price from the Solidly pairs using the TWAP observations | ||
/// @param _data Payload from the central oracle with the addresses of the token route, pool | ||
/// route and TWAP periods counted in 30 minute increments | ||
/// @return price Retrieved price from the chained quotes | ||
/// @return success Successful price fetch or not | ||
function getPrice(bytes calldata _data) external returns (uint256 price, bool success) { | ||
(address[] memory tokens, address[] memory pools, uint256[] memory twapPeriods) = | ||
abi.decode(_data, (address[], address[], uint256[])); | ||
|
||
uint256 amount = 10 ** IERC20MetadataUpgradeable(tokens[0]).decimals(); | ||
for (uint i; i < pools.length; i++) { | ||
amount = ISolidlyPair(pools[i]).quote(tokens[i], amount, twapPeriods[i]); | ||
} | ||
|
||
price = BeefyOracleHelper.priceFromBaseToken( | ||
msg.sender, tokens[tokens.length - 1], tokens[0], amount | ||
); | ||
if (price != 0) success = true; | ||
} | ||
|
||
/// @notice Data validation for new oracle data being added to central oracle | ||
/// @param _data Encoded addresses of the token route, pool route and TWAP periods | ||
function validateData(bytes calldata _data) external view { | ||
(address[] memory tokens, address[] memory pools, uint256[] memory twapPeriods) = | ||
abi.decode(_data, (address[], address[], uint256[])); | ||
|
||
if (tokens.length != pools.length + 1 || tokens.length != twapPeriods.length + 1) { | ||
revert ArrayLength(); | ||
} | ||
|
||
uint256 basePrice = IBeefyOracle(msg.sender).getPrice(tokens[0]); | ||
if (basePrice == 0) revert NoBasePrice(tokens[0]); | ||
|
||
for (uint i; i < pools.length; i++) { | ||
address token = tokens[i]; | ||
address pool = pools[i]; | ||
if (token != ISolidlyPair(pool).token0() || token != ISolidlyPair(pool).token1()) { | ||
revert TokenNotInPair(token, pool); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.