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

C4: #35 #32, fixes rETH / cbETH / ankrETH ref unit and adds soft default checks #899

Merged
merged 12 commits into from
Aug 22, 2023
41 changes: 26 additions & 15 deletions contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,34 @@ import "./vendor/IAnkrETH.sol";

/**
* @title Ankr Staked Eth Collateral
* @notice Collateral plugin for Ankr ankrETH,
* @notice Collateral plugin for Ankr's ankrETH
* tok = ankrETH
* ref = ETH
* ref = ETH2
* tar = ETH
* UoA = USD
* @dev Not ready to deploy yet. Missing a {target/tok} feed from Chainlink.
*/
contract AnkrStakedEthCollateral is AppreciatingFiatCollateral {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;

// solhint-disable no-empty-blocks
/// @param config.chainlinkFeed Feed units: {UoA/ref}
constructor(CollateralConfig memory config, uint192 revenueHiding)
AppreciatingFiatCollateral(config, revenueHiding)
{}
AggregatorV3Interface public immutable targetPerTokChainlinkFeed; // {target/tok}
uint48 public immutable targetPerTokChainlinkTimeout;

// solhint-enable no-empty-blocks
/// @param config.chainlinkFeed {UoA/target} price of ETH in USD terms
/// @param _targetPerTokChainlinkFeed {target/tok} price of cbETH in ETH terms
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
AggregatorV3Interface _targetPerTokChainlinkFeed,
uint48 _targetPerTokChainlinkTimeout
) AppreciatingFiatCollateral(config, revenueHiding) {
require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed");
require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero");

targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed;
targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout;
}

/// Can revert, used by other contract functions in order to catch errors
/// @return low {UoA/tok} The low price estimate
Expand All @@ -41,22 +52,22 @@ contract AnkrStakedEthCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
uint192 pricePerRef = chainlinkFeed.price(oracleTimeout); // {UoA/ref}
uint192 targetPerTok = targetPerTokChainlinkFeed.price(targetPerTokChainlinkTimeout);

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

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

pegPrice = targetPerRef(); // ETH/ETH
// {target/ref} = {target/tok} / {ref/tok}
pegPrice = targetPerTok.div(_underlyingRefPerTok());
}

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function _underlyingRefPerTok() internal view override returns (uint192) {
uint256 rate = IAnkrETH(address(erc20)).ratio();
return FIX_ONE.div(_safeWrap(rate), FLOOR);
return FIX_ONE.div(_safeWrap(IAnkrETH(address(erc20)).ratio()), FLOOR);
}
}
12 changes: 6 additions & 6 deletions contracts/plugins/assets/ankr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ This plugin allows the usage of [ankrETH](https://www.ankr.com/about-staking/) a

The `ankrETH` token represents the users staked ETH plus accumulated staking rewards. It is immediately liquid, which enables users to trade them instantly, or unstake them to redeem the original underlying asset.

User's balances in `ankrETH` remain constant, but the value of each ankrETH token grows over time. It is a reward-bearing token, meaning that the fair value of 1 ankrETH token vs. ETH increases over time as staking rewards accumulate. When possible, users will have the option to redeem ankrETH and unstake ETH with accumulated [staking rewards](https://www.ankr.com/docs/staking/liquid-staking/eth/overview/).
User's balances in `ankrETH` remain constant, but the value of each ankrETH token grows over time. It is a reward-bearing token, meaning that the fair value of 1 ankrETH token vs. ETH2 increases over time as staking rewards accumulate. When possible, users will have the option to redeem ankrETH and unstake ETH2 for ETH with accumulated [staking rewards](https://www.ankr.com/docs/staking/liquid-staking/eth/overview/).

## Implementation

### Units

| tok | ref | target | UoA |
| ------- | --- | ------ | --- |
| ankrETH | ETH | ETH | USD |
| tok | ref | target | UoA |
| ------- | ---- | ------ | --- |
| ankrETH | ETH2 | ETH | USD |

### Functions

#### refPerTok {ref/tok}

The exchange rate between ETH and ankrETH can be fetched using the ankrETH contract function `ratio()`. From this, we can obtain the inverse rate from ankrETH to ETH, and use that as `refPerTok`.
The exchange rate between ETH2 and ankrETH can be fetched using the ankrETH contract function `ratio()`. From this, we can obtain the inverse rate from ankrETH to ETH2, and use that as `refPerTok`.

This new ratio, increases over time, which means that the amount of ETH redeemable for each ankrETH token always increases.
This new ratio, increases over time, which means that the amount of ETH redeemable for each ankrETH token always increases, though redemptions sit behind a withdrawal queue.

`ratio()` returns the exchange rate in 18 decimals.
45 changes: 28 additions & 17 deletions contracts/plugins/assets/cbeth/CBETHCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
pragma solidity 0.8.19;

import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { _safeWrap } from "../../../libraries/Fixed.sol";
import "../AppreciatingFiatCollateral.sol";
import { CEIL, FixLib, _safeWrap } from "../../../libraries/Fixed.sol";
import { AggregatorV3Interface, OracleLib } from "../OracleLib.sol";
import { CollateralConfig, AppreciatingFiatCollateral } from "../AppreciatingFiatCollateral.sol";

interface CBEth is IERC20Metadata {
function mint(address account, uint256 amount) external returns (bool);
Expand All @@ -15,25 +16,34 @@ interface CBEth is IERC20Metadata {
function exchangeRate() external view returns (uint256 _exchangeRate);
}

/**
* @title CBEthCollateral
* @notice Collateral plugin for Coinbase's staked ETH
* tok = cbETH
* ref = ETH2
* tar = ETH
* UoA = USD
*/
contract CBEthCollateral is AppreciatingFiatCollateral {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;

CBEth public immutable token;
AggregatorV3Interface public immutable refPerTokChainlinkFeed;
uint48 public immutable refPerTokChainlinkTimeout;
AggregatorV3Interface public immutable targetPerTokChainlinkFeed; // {target/tok}
uint48 public immutable targetPerTokChainlinkTimeout;

/// @param config.chainlinkFeed {UoA/ref} price of DAI in USD terms
/// @param config.chainlinkFeed {UoA/target} price of ETH in USD terms
/// @param _targetPerTokChainlinkFeed {target/tok} price of cbETH in ETH terms
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
AggregatorV3Interface _refPerTokChainlinkFeed,
uint48 _refPerTokChainlinkTimeout
AggregatorV3Interface _targetPerTokChainlinkFeed,
uint48 _targetPerTokChainlinkTimeout
) AppreciatingFiatCollateral(config, revenueHiding) {
token = CBEth(address(config.erc20));
require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed");
require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero");

refPerTokChainlinkFeed = _refPerTokChainlinkFeed;
refPerTokChainlinkTimeout = _refPerTokChainlinkTimeout;
targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed;
targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout;
}

/// Can revert, used by other contract functions in order to catch errors
Expand All @@ -50,21 +60,22 @@ contract CBEthCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(
refPerTokChainlinkFeed.price(refPerTokChainlinkTimeout)
);
uint192 targetPerTok = targetPerTokChainlinkFeed.price(targetPerTokChainlinkTimeout);

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

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

pegPrice = targetPerRef(); // {target/ref} ETH/ETH is always 1
// {target/ref} = {target/tok} / {ref/tok}
pegPrice = targetPerTok.div(_underlyingRefPerTok());
}

/// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens
function _underlyingRefPerTok() internal view override returns (uint192) {
return _safeWrap(token.exchangeRate());
return _safeWrap(CBEth(address(erc20)).exchangeRate());
}
}
6 changes: 3 additions & 3 deletions contracts/plugins/assets/cbeth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ This plugin allows `CBETH` holders to use their tokens as collateral in the Rese

### Units

| tok | ref | target | UoA |
| ----- | --- | ------ | --- |
| cbeth | ETH | ETH | ETH |
| tok | ref | target | UoA |
| ----- | ---- | ------ | --- |
| cbeth | ETH2 | ETH | USD |

### Functions

Expand Down
12 changes: 6 additions & 6 deletions contracts/plugins/assets/rocket-eth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ stake in the POS ETH2.0 consenus layer.

### Units

| tok | ref | target | UoA |
| ---- | --- | ------ | --- |
| rETH | ETH | ETH | USD |
| tok | ref | target | UoA |
| ---- | ---- | ------ | --- |
| rETH | ETH2 | ETH | USD |

### refPerTok()

Gets the exchange rate for `rETH` to `ETH` from the rETH token contract using the [getExchangeRate()](https://github.com/rocket-pool/rocketpool/blob/master/contracts/contract/token/RocketTokenRETH.sol#L66)
function. This is the rate used by rocket pool when converting between reth and eth and is closely followed by secondary markets.
While the value of ETH/rETH **should** be only-increasing, it is possible that slashing or inactivity events could occur for the rETH
Gets the exchange rate for `rETH` to `ETH2` from the rETH token contract using the [getExchangeRate()](https://github.com/rocket-pool/rocketpool/blob/master/contracts/contract/token/RocketTokenRETH.sol#L66)
function. This is the rate used by rocket pool when converting between reth and eth2 and is closely followed by secondary markets.
While the value of ETH2/rETH **should** be only-increasing, it is possible that slashing or inactivity events could occur for the rETH
validators. As such, `rETH` inherits `AppreciatingFiatCollateral` to allow for some amount of revenue-hiding. The amount of
revenue-hiding should be determined by the deployer, but can likely be quite high, as it is more likely that any dips, however large,
would be temporary, and, in particularly bad instances, be covered by the Rocket Pool protocol.
Expand Down
44 changes: 23 additions & 21 deletions contracts/plugins/assets/rocket-eth/RethCollateral.sol
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "../../../libraries/Fixed.sol";
import "../AppreciatingFiatCollateral.sol";
import "../OracleLib.sol";
import "./vendor/IReth.sol";
import { CEIL, FixLib, _safeWrap } from "../../../libraries/Fixed.sol";
import { AggregatorV3Interface, OracleLib } from "../OracleLib.sol";
import { CollateralConfig, AppreciatingFiatCollateral } from "../AppreciatingFiatCollateral.sol";
import { IReth } from "./vendor/IReth.sol";

/**
* @title RethCollateral
tbrent marked this conversation as resolved.
Show resolved Hide resolved
* @notice Collateral plugin for Rocket-Pool ETH,
* @notice Collateral plugin for Rocket-Pool ETH
* tok = rETH
* ref = ETH
* ref = ETH2
* tar = ETH
* UoA = USD
*/
contract RethCollateral is AppreciatingFiatCollateral {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;

AggregatorV3Interface public immutable refPerTokChainlinkFeed;
uint48 public immutable refPerTokChainlinkTimeout;
AggregatorV3Interface public immutable targetPerTokChainlinkFeed;
uint48 public immutable targetPerTokChainlinkTimeout;

/// @param config.chainlinkFeed Feed units: {UoA/ref}
/// @param config.chainlinkFeed {UoA/target} price of ETH in USD terms
/// @param _targetPerTokChainlinkFeed {target/tok} price of rETH in ETH terms
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
AggregatorV3Interface _refPerTokChainlinkFeed,
uint48 _refPerTokChainlinkTimeout
AggregatorV3Interface _targetPerTokChainlinkFeed,
uint48 _targetPerTokChainlinkTimeout
) AppreciatingFiatCollateral(config, revenueHiding) {
require(address(_refPerTokChainlinkFeed) != address(0), "missing refPerTok feed");
require(_refPerTokChainlinkTimeout != 0, "refPerTokChainlinkTimeout zero");
refPerTokChainlinkFeed = _refPerTokChainlinkFeed;
refPerTokChainlinkTimeout = _refPerTokChainlinkTimeout;
require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed");
require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero");

targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed;
targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout;
}

/// Can revert, used by other contract functions in order to catch errors
Expand All @@ -49,17 +50,18 @@ contract RethCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(
refPerTokChainlinkFeed.price(refPerTokChainlinkTimeout)
);
uint192 targetPerTok = targetPerTokChainlinkFeed.price(targetPerTokChainlinkTimeout);

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

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

pegPrice = targetPerRef(); // {target/ref} ETH/ETH is always 1
// {target/ref} = {target/tok} / {ref/tok}
pegPrice = targetPerTok.div(_underlyingRefPerTok());
}

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
Expand Down