Skip to content

Commit

Permalink
Swapper oracle (#242)
Browse files Browse the repository at this point in the history
* Add Beefy Oracle

* Add Beefy Swapper
  • Loading branch information
kexleyBeefy authored Oct 5, 2023
1 parent b161b90 commit 1fabb4f
Show file tree
Hide file tree
Showing 15 changed files with 1,060 additions and 0 deletions.
147 changes: 147 additions & 0 deletions contracts/BIFI/infra/BeefyOracle/BeefyOracle.sol
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);
}
}
39 changes: 39 additions & 0 deletions contracts/BIFI/infra/BeefyOracle/BeefyOracleChainlink.sol
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(); }
}
}
42 changes: 42 additions & 0 deletions contracts/BIFI/infra/BeefyOracle/BeefyOracleHelper.sol
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;
}
}
67 changes: 67 additions & 0 deletions contracts/BIFI/infra/BeefyOracle/BeefyOracleSolidly.sol
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);
}
}
}
}
Loading

0 comments on commit 1fabb4f

Please sign in to comment.