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

fix sfrxETH Collateral, add new oracle #1026

Merged
merged 11 commits into from
Jan 20, 2024
7 changes: 7 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ interface INetworkConfig {
AAVE_V3_INCENTIVES_CONTROLLER?: string
AAVE_V3_POOL?: string
STARGATE_STAKING_CONTRACT?: string
CURVE_POOL_ETH_FRXETH?: string
}

export const networkConfig: { [key: string]: INetworkConfig } = {
Expand Down Expand Up @@ -219,6 +220,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD
rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH
cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH
frxETH: '0xc58f3385fbc1c8ad2c0c9a061d7c13b141d7a5df' // frxETH/ETH
},
AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9',
AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5',
Expand All @@ -238,6 +240,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
AAVE_V3_POOL: '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2',
AAVE_V3_INCENTIVES_CONTROLLER: '0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb',
STARGATE_STAKING_CONTRACT: '0xB0D502E938ed5f4df2E681fE6E419ff29631d62b',
CURVE_POOL_ETH_FRXETH: '0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577'
},
'1': {
name: 'mainnet',
Expand Down Expand Up @@ -326,6 +329,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD
rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH
cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH
frxETH: '0xc58f3385fbc1c8ad2c0c9a061d7c13b141d7a5df' // frxETH/ETH
},
AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9',
AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c',
Expand All @@ -342,6 +346,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
AAVE_V3_POOL: '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2',
AAVE_V3_INCENTIVES_CONTROLLER: '0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb',
STARGATE_STAKING_CONTRACT: '0xB0D502E938ed5f4df2E681fE6E419ff29631d62b',
CURVE_POOL_ETH_FRXETH: '0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577'
},
'3': {
name: 'tenderly',
Expand Down Expand Up @@ -425,6 +430,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD
rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH
cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH
frxETH: '0xc58f3385fbc1c8ad2c0c9a061d7c13b141d7a5df' // frxETH/ETH
},
AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9',
AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c',
Expand All @@ -441,6 +447,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
AAVE_V3_POOL: '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2',
AAVE_V3_INCENTIVES_CONTROLLER: '0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb',
STARGATE_STAKING_CONTRACT: '0xB0D502E938ed5f4df2E681fE6E419ff29631d62b',
CURVE_POOL_ETH_FRXETH: '0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577'
},
'5': {
name: 'goerli',
Expand Down
66 changes: 66 additions & 0 deletions contracts/plugins/assets/FraxOracleLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "../../libraries/Fixed.sol";
import "./OracleErrors.sol";

interface FraxAggregatorV3Interface is AggregatorV3Interface {
function priceSource() external view returns (address);

function addRoundData(
bool _isBadData,
uint104 _priceLow,
uint104 _priceHigh,
uint40 _timestamp
) external;
}

/// Used by asset plugins to price their collateral
library FraxOracleLib {
/// @dev Use for nested calls that should revert when there is a problem
/// @param timeout The number of seconds after which oracle values should be considered stale
/// @return {UoA/tok}
function price(FraxAggregatorV3Interface chainlinkFeed, uint48 timeout)
internal
view
returns (uint192)
{
try chainlinkFeed.latestRoundData() returns (
uint80 roundId,
int256 p,
uint256,
uint256 updateTime,
uint80 answeredInRound
) {
if (updateTime == 0 || answeredInRound < roundId) {
revert StalePrice();
}

// Downcast is safe: uint256(-) reverts on underflow; block.timestamp assumed < 2^48
uint48 secondsSince = uint48(block.timestamp - updateTime);
if (secondsSince > timeout) revert StalePrice();

if (p == 0) revert ZeroPrice();

// {UoA/tok}
return shiftl_toFix(uint256(p), -int8(chainlinkFeed.decimals()));
} catch (bytes memory errData) {
// Check if the priceSource was not set: if so, the chainlink feed has been deprecated
// and a _specific_ error needs to be raised in order to avoid looking like OOG
if (errData.length == 0) {
if (chainlinkFeed.priceSource() == address(0)) {
revert StalePrice();
}
// solhint-disable-next-line reason-string
revert();
}

// Otherwise, preserve the error bytes
// solhint-disable-next-line no-inline-assembly
assembly {
revert(add(32, errData), mload(errData))
}
}
}
}
7 changes: 7 additions & 0 deletions contracts/plugins/assets/OracleErrors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

// 0x19abf40e
error StalePrice();
// 0x4dfba023
error ZeroPrice();
4 changes: 1 addition & 3 deletions contracts/plugins/assets/OracleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ pragma solidity 0.8.19;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "../../libraries/Fixed.sol";

error StalePrice();
error ZeroPrice();
import "./OracleErrors.sol";

interface EACAggregatorProxy {
function aggregator() external view returns (address);
Expand Down
7 changes: 3 additions & 4 deletions contracts/plugins/assets/frax-eth/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Staked-Frax-ETH Collateral Plugin

**NOTE: The SFraxEthCollateral plugin SHOULD NOT be deployed and used until a `frxETH/ETH` chainlink oracle can be integrated with the plugin. As of 3/14/23, there is no chainlink oracle, but the FRAX team is working on getting one.**

## Summary

This plugin allows `sfrxETH` ((Staked-Frax-ETH)[https://docs.frax.finance/frax-ether/overview]) holders use their tokens as collateral in the Reserve Protocol.
Expand All @@ -16,8 +14,6 @@ You can get the `frxETH/sfrxETH` exchange rate from [`sfrxETH.pricePerShare()`](

`frxETH` contract: <https://etherscan.io/address/0x5E8422345238F34275888049021821E8E08CAa1f>

`wstETH` and `stETH` can be always swapped at any time to each other without any risk and limitation (Except smart contract risk), like `wETH` and `ETH`. Wrap & Unwrap app can be found here: <https://stake.lido.fi/wrap>

## Implementation

### Units
Expand All @@ -32,6 +28,9 @@ You can get the `frxETH/sfrxETH` exchange rate from [`sfrxETH.pricePerShare()`](

This function returns rate of `frxETH/sfrxETH`, getting from [pricePerShare()](https://github.com/FraxFinance/frxETH-public/blob/master/src/sfrxETH.sol#L82) function in sfrxETH contract.

#### target-per-ref price {tar/ref}

The targetPerRef price of `ETH/frxETH` is received from the frxETH/ETH FRAX-managed oracle ([details here](https://docs.frax.finance/frax-oracle/frax-oracle-overview)).
#### tryPrice

This function uses `refPerTok` and the chainlink price of `USD/ETH` to return the current price range of the collateral. Once an oracle becomes available for `frxETH/ETH`, this function should be modified to use it and return the appropiate `pegPrice`.
46 changes: 25 additions & 21 deletions contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import "@openzeppelin/contracts/utils/math/Math.sol";
import "../../../libraries/Fixed.sol";
import "../AppreciatingFiatCollateral.sol";
import "../OracleLib.sol";
import "../FraxOracleLib.sol";
import "./vendor/IsfrxEth.sol";

/**
* ************************************************************
* WARNING: this plugin is not ready to be used in Production
* ************************************************************
*/
import "./vendor/CurvePoolEmaPriceOracleWithMinMax.sol";

/**
* @title SFraxEthCollateral
Expand All @@ -21,20 +17,30 @@ import "./vendor/IsfrxEth.sol";
* tar = ETH
* UoA = USD
*/
contract SFraxEthCollateral is AppreciatingFiatCollateral {
contract SFraxEthCollateral is AppreciatingFiatCollateral, CurvePoolEmaPriceOracleWithMinMax {
using OracleLib for AggregatorV3Interface;
using FraxOracleLib for FraxAggregatorV3Interface;
pmckelvy1 marked this conversation as resolved.
Show resolved Hide resolved
using FixLib for uint192;

// solhint-disable no-empty-blocks
/// @param config.chainlinkFeed Feed units: {UoA/target}
constructor(CollateralConfig memory config, uint192 revenueHiding)
/// @param config.chainlinkFeed {UoA/target} price of ETH in USD terms
/// @param revenueHiding {1e18} percent amount of revenue to hide
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
address curvePoolEmaPriceOracleAddress,
uint256 _minimumCurvePoolEma,
uint256 _maximumCurvePoolEma
)
AppreciatingFiatCollateral(config, revenueHiding)
CurvePoolEmaPriceOracleWithMinMax(
curvePoolEmaPriceOracleAddress,
_minimumCurvePoolEma,
_maximumCurvePoolEma
)
{
require(config.defaultThreshold > 0, "defaultThreshold zero");
}

// solhint-enable no-empty-blocks

/// Can revert, used by other contract functions in order to catch errors
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
Expand All @@ -49,22 +55,20 @@ contract SFraxEthCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/target} * {ref/tok} * {target/ref} (1)
uint192 p = chainlinkFeed.price(oracleTimeout).mul(_underlyingRefPerTok());
// {target/ref} Get current market peg ({eth/frxeth})
pegPrice = _safeWrap(_getCurvePoolToken1EmaPrice());

// {UoA/tok} = {UoA/target} * {target/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(pegPrice).mul(_underlyingRefPerTok());
uint192 err = p.mul(oracleError, CEIL);

low = p - err;
high = p + err;
low = p - err;
// assert(low <= high); obviously true just by inspection

// TODO: Currently not checking for depegs between `frxETH` and `ETH`
// Should be modified to use a `frxETH/ETH` oracle when available
pegPrice = targetPerRef();
}

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function _underlyingRefPerTok() internal view override returns (uint192) {
uint256 rate = IsfrxEth(address(erc20)).pricePerShare();
return _safeWrap(rate);
return _safeWrap(IsfrxEth(address(erc20)).pricePerShare());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: ISC
pragma solidity ^0.8.19;

// Inspired by Frax Finance: https://github.com/FraxFinance

// Original Author
// Drake Evans: https://github.com/DrakeEvans

// Original Reviewers
// Dennis: https://github.com/denett

// ====================================================================

import { ICurvePoolEmaPriceOracleWithMinMax } from "./ICurvePoolEmaPriceOracleWithMinMax.sol";

interface IEmaPriceOracleStableSwap {
// solhint-disable-next-line func-name-mixedcase
function price_oracle() external view returns (uint256);
}

struct ConstructorParams {
address curvePoolEmaPriceOracleAddress;
uint256 minimumCurvePoolEma;
uint256 maximumCurvePoolEma;
}

/// @title CurvePoolEmaPriceOracleWithMinMax
/// @author Drake Evans (Frax Finance) https://github.com/drakeevans
/// @notice An oracle for getting EMA prices from Curve
contract CurvePoolEmaPriceOracleWithMinMax is ICurvePoolEmaPriceOracleWithMinMax {
/// @notice Curve pool, source of EMA
// solhint-disable-next-line var-name-mixedcase
address public immutable CURVE_POOL_EMA_PRICE_ORACLE;

/// @notice Precision of Curve pool price_oracle()
uint256 public constant CURVE_POOL_EMA_PRICE_ORACLE_DECIMALS = 18;

/// @notice Maximum price of token1 in token0 units of the EMA
/// @dev Must match precision of EMA
uint256 public minimumCurvePoolEma;

/// @notice Maximum price of token1 in token0 units of the EMA
/// @dev Must match precision of EMA
uint256 public maximumCurvePoolEma;

constructor(
address curvePoolEmaPriceOracleAddress,
uint256 _minimumCurvePoolEma,
uint256 _maximumCurvePoolEma
) {
CURVE_POOL_EMA_PRICE_ORACLE = curvePoolEmaPriceOracleAddress;
minimumCurvePoolEma = _minimumCurvePoolEma;
maximumCurvePoolEma = _maximumCurvePoolEma;
}

function _getCurvePoolToken1EmaPrice() internal view returns (uint256 _token1Price) {
uint256 _priceRaw = IEmaPriceOracleStableSwap(CURVE_POOL_EMA_PRICE_ORACLE).price_oracle();
uint256 _price = _priceRaw > maximumCurvePoolEma ? maximumCurvePoolEma : _priceRaw;

_token1Price = _price < minimumCurvePoolEma ? minimumCurvePoolEma : _price;
}

/// @notice The ```getCurvePoolToken1EmaPrice``` function gets the price of the second token
/// in the Curve pool (token1)
/// @dev Returned in units of the first token (token0)
/// @return _emaPrice The price of the second token in the Curve pool
function getCurvePoolToken1EmaPrice() external view returns (uint256 _emaPrice) {
return _getCurvePoolToken1EmaPrice();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;

interface ICurvePoolEmaPriceOracleWithMinMax {
// solhint-disable-next-line func-name-mixedcase
function CURVE_POOL_EMA_PRICE_ORACLE() external view returns (address);

// solhint-disable-next-line func-name-mixedcase
function CURVE_POOL_EMA_PRICE_ORACLE_DECIMALS() external view returns (uint256);

function getCurvePoolToken1EmaPrice() external view returns (uint256 _emaPrice);

function maximumCurvePoolEma() external view returns (uint256);

function minimumCurvePoolEma() external view returns (uint256);
}
13 changes: 13 additions & 0 deletions contracts/plugins/mocks/ChainlinkMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ contract MockV3Aggregator is AggregatorV3Interface {
// Additional variable to be able to test invalid behavior
uint256 public latestAnsweredRound;
address public aggregator;
address public priceSource;

mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
Expand All @@ -32,6 +33,7 @@ contract MockV3Aggregator is AggregatorV3Interface {
constructor(uint8 _decimals, int256 _initialAnswer) {
decimals = _decimals;
aggregator = address(this);
priceSource = address(this);
updateAnswer(_initialAnswer);
}

Expand All @@ -49,6 +51,17 @@ contract MockV3Aggregator is AggregatorV3Interface {
latestAnsweredRound = latestRound;
}

// used by Frax oracle
function addRoundData(bool isBadData, uint104 low, uint104 high, uint40 timestamp) public {
latestAnswer = int104(low + high) / 2;
latestTimestamp = block.timestamp;
latestRound++;
getAnswer[latestRound] = latestAnswer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
latestAnsweredRound = latestRound;
}

// Additional function to be able to test invalid Chainlink behavior
function setInvalidTimestamp() public {
getTimestamp[latestRound] = 0;
Expand Down
Loading
Loading