Skip to content

Commit

Permalink
C4: #35 #32, fixes RETH and CBETH ref unit exhange
Browse files Browse the repository at this point in the history
  • Loading branch information
jankjr committed Aug 16, 2023
1 parent 99d9db7 commit 2a592c5
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 147 deletions.
19 changes: 11 additions & 8 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 { CollateralConfig, AppreciatingFiatCollateral } from "../AppreciatingFiatCollateral.sol";
import { OracleLib, AggregatorV3Interface } from "../OracleLib.sol";

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

// TOK => CBEth
// REF => ETH (Calculated using spot price rather than cbETH exchange rate)
// TAR => ETH
// UoA => USD
contract CBEthCollateral is AppreciatingFiatCollateral {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;
Expand All @@ -23,7 +28,7 @@ contract CBEthCollateral is AppreciatingFiatCollateral {
AggregatorV3Interface public immutable refPerTokChainlinkFeed;
uint48 public immutable refPerTokChainlinkTimeout;

/// @param config.chainlinkFeed {UoA/ref} price of DAI in USD terms
/// @param config.chainlinkFeed {UoA/ref} price of cbETH in ETH terms
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
Expand All @@ -50,10 +55,8 @@ contract CBEthCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(
refPerTokChainlinkFeed.price(refPerTokChainlinkTimeout)
);
// {UoA/tok} = {ref/tok} * {UoA/ref}
uint192 p = _underlyingRefPerTok().mul(chainlinkFeed.price(oracleTimeout));
uint192 err = p.mul(oracleError, CEIL);

high = p + err;
Expand All @@ -65,6 +68,6 @@ contract CBEthCollateral is AppreciatingFiatCollateral {

/// @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 refPerTokChainlinkFeed.price(type(uint48).max);
}
}
9 changes: 5 additions & 4 deletions contracts/plugins/assets/cbeth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ 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 | ETH | ETH | USD |

### Functions

#### refPerTok {ref/tok}

`return _safeWrap(token.exchange_rate());`
Gets the exchange rate for `cbETH` to `ETH` using the [cbETH/ETH oracle](https://data.chain.link/ethereum/mainnet/crypto-eth/cbeth-eth). This is the rate is similar to the rate cbETH exposes in the contract directly, but the Oracle exchange rate better refects unstaking queue congestion.

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

### 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.
Gets the exchange rate for `rETH` to `ETH` using the [rETH/ETH oracle](https://data.chain.link/ethereum/mainnet/crypto-eth/reth-eth). This is the rate is similar to the rate rocket pool uses when converting between reth and eth and is closely followed by secondary markets. Oracle exchange rate better refects unstaking queue congestion.

While the value of ETH/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,
Expand Down
18 changes: 7 additions & 11 deletions contracts/plugins/assets/rocket-eth/RethCollateral.sol
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// 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 } from "../../../libraries/Fixed.sol";
import { CollateralConfig, AppreciatingFiatCollateral } from "../AppreciatingFiatCollateral.sol";
import { OracleLib, AggregatorV3Interface } from "../OracleLib.sol";

/**
* @title RethCollateral
* @notice Collateral plugin for Rocket-Pool ETH,
* tok = rETH
* ref = ETH
* ref = ETH (Calculated using spot price rather than RETH exchange rate)
* tar = ETH
* UoA = USD
*/
Expand Down Expand Up @@ -49,10 +47,8 @@ contract RethCollateral is AppreciatingFiatCollateral {
uint192 pegPrice
)
{
// {UoA/tok} = {UoA/ref} * {ref/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(
refPerTokChainlinkFeed.price(refPerTokChainlinkTimeout)
);
// {UoA/tok} = {ref/tok} * {UoA/ref}
uint192 p = _underlyingRefPerTok().mul(chainlinkFeed.price(oracleTimeout));
uint192 err = p.mul(oracleError, CEIL);

high = p + err;
Expand All @@ -64,6 +60,6 @@ contract RethCollateral is AppreciatingFiatCollateral {

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function _underlyingRefPerTok() internal view override returns (uint192) {
return _safeWrap(IReth(address(erc20)).getExchangeRate());
return refPerTokChainlinkFeed.price(type(uint48).max);
}
}
39 changes: 8 additions & 31 deletions test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '..
import {
CBETH_ETH_PRICE_FEED,
CB_ETH,
CB_ETH_ORACLE,
DEFAULT_THRESHOLD,
DELAY_UNTIL_DEFAULT,
ETH_USD_PRICE_FEED,
Expand All @@ -21,8 +20,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { MockV3Aggregator } from '@typechain/MockV3Aggregator'
import { CBEth, ERC20Mock, MockV3Aggregator__factory } from '@typechain/index'
import { mintCBETH, resetFork } from './helpers'
import { whileImpersonating } from '#/utils/impersonation'
import hre from 'hardhat'

interface CbEthCollateralFixtureContext extends CollateralFixtureContext {
cbETH: CBEth
Expand Down Expand Up @@ -126,46 +123,26 @@ const reduceTargetPerRef = async () => {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
const increaseTargetPerRef = async () => {}

const changeRefPerTok = async (ctx: CbEthCollateralFixtureContext, percentChange: BigNumber) => {
await whileImpersonating(hre, CB_ETH_ORACLE, async (oracleSigner) => {
const rate = await ctx.cbETH.exchangeRate()
await ctx.cbETH
.connect(oracleSigner)
.updateExchangeRate(rate.add(rate.mul(percentChange).div(bn('100'))))
{
const lastRound = await ctx.refPerTokChainlinkFeed.latestRoundData()
const nextAnswer = lastRound.answer.add(lastRound.answer.mul(percentChange).div(100))
await ctx.refPerTokChainlinkFeed.updateAnswer(nextAnswer)
}

{
const lastRound = await ctx.chainlinkFeed.latestRoundData()
const nextAnswer = lastRound.answer.add(lastRound.answer.mul(percentChange).div(100))
await ctx.chainlinkFeed.updateAnswer(nextAnswer)
}
})
}

// prettier-ignore
const reduceRefPerTok = async (
ctx: CbEthCollateralFixtureContext,
pctDecrease: BigNumberish
) => {
await changeRefPerTok(
ctx,
bn(pctDecrease).mul(-1)
)
const answer = await ctx.refPerTokChainlinkFeed.latestAnswer()
const nextAnswer = answer.sub(answer.div(100).mul(pctDecrease))
await ctx.refPerTokChainlinkFeed.updateAnswer(nextAnswer)
}

// prettier-ignore
const increaseRefPerTok = async (
ctx: CbEthCollateralFixtureContext,
pctIncrease: BigNumberish
) => {
await changeRefPerTok(
ctx,
bn(pctIncrease)
)
const answer = await ctx.refPerTokChainlinkFeed.latestAnswer()
const nextAnswer = answer.add(answer.div(100).mul(pctIncrease))
await ctx.refPerTokChainlinkFeed.updateAnswer(nextAnswer)
}

const getExpectedPrice = async (ctx: CbEthCollateralFixtureContext): Promise<BigNumber> => {
const clData = await ctx.chainlinkFeed.latestRoundData()
const clDecimals = await ctx.chainlinkFeed.decimals()
Expand Down
18 changes: 10 additions & 8 deletions test/plugins/individual-collateral/collateralTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,20 +302,22 @@ export default function fn<X extends CollateralFixtureContext>(
})

itHasRevenueHiding('does revenue hiding correctly', async () => {
ctx.collateral = await deployCollateral({
// Recreate the context to make it possible for the `reduceRefPerTok` method to
// modify oracles
const revenueHidingContext = await makeCollateralFixtureContext(alice, {
erc20: ctx.tok.address,
revenueHiding: fp('0.01'),
})
})()

// Should remain SOUND after a 1% decrease
await reduceRefPerTok(ctx, 1) // 1% decrease
await ctx.collateral.refresh()
expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND)
await reduceRefPerTok(revenueHidingContext, 1) // 1% decrease
await revenueHidingContext.collateral.refresh()
expect(await revenueHidingContext.collateral.status()).to.equal(CollateralStatus.SOUND)

// Should become DISABLED if drops more than that
await reduceRefPerTok(ctx, 1) // another 1% decrease
await ctx.collateral.refresh()
expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED)
await reduceRefPerTok(revenueHidingContext, 1) // another 1% decrease
await revenueHidingContext.collateral.refresh()
expect(await revenueHidingContext.collateral.status()).to.equal(CollateralStatus.DISABLED)
})

it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import {
RETH,
ETH_USD_PRICE_FEED,
RETH_ETH_PRICE_FEED,
RETH_NETWORK_BALANCES,
RETH_STORAGE,
} from './constants'
import { whileImpersonating } from '#/test/utils/impersonation'

/*
Define interfaces
Expand Down Expand Up @@ -142,56 +139,6 @@ const makeCollateralFixtureContext = (
return makeCollateralFixtureContext
}

// const deployCollateralCometMockContext = async (
// opts: CometCollateralOpts = {}
// ): Promise<RethCollateralFixtureContextMockComet> => {
// const collateralOpts = { ...defaultCometCollateralOpts, ...opts }

// const MockV3AggregatorFactory = <MockV3Aggregator__factory>(
// await ethers.getContractFactory('MockV3Aggregator')
// )
// const chainlinkFeed = <MockV3Aggregator>await MockV3AggregatorFactory.deploy(6, bn('1e6'))
// collateralOpts.chainlinkFeed = chainlinkFeed.address

// const CometFactory = <CometMock__factory>await ethers.getContractFactory('CometMock')
// const cusdcV3 = <CometMock>await CometFactory.deploy(bn('5e15'), bn('1e15'), CUSDC_V3)

// const CusdcV3WrapperFactory = <CusdcV3Wrapper__factory>(
// await ethers.getContractFactory('CusdcV3Wrapper')
// )
// const wcusdcV3 = <ICusdcV3Wrapper>(
// await CusdcV3WrapperFactory.deploy(cusdcV3.address, REWARDS, COMP)
// )
// const CusdcV3WrapperMockFactory = <CusdcV3WrapperMock__factory>(
// await ethers.getContractFactory('CusdcV3WrapperMock')
// )
// const wcusdcV3Mock = await (<ICusdcV3WrapperMock>(
// await CusdcV3WrapperMockFactory.deploy(wcusdcV3.address)
// ))

// const realMock = (await ethers.getContractAt(
// 'ICusdcV3WrapperMock',
// wcusdcV3Mock.address
// )) as ICusdcV3WrapperMock
// collateralOpts.erc20 = wcusdcV3.address
// collateralOpts.erc20 = realMock.address
// const usdc = <ERC20Mock>await ethers.getContractAt('ERC20Mock', USDC)
// const collateral = await deployCollateral(collateralOpts)

// const rewardToken = <ERC20Mock>await ethers.getContractAt('ERC20Mock', COMP)

// return {
// collateral,
// chainlinkFeed,
// cusdcV3,
// wcusdcV3: <ICusdcV3WrapperMock>wcusdcV3Mock,
// wcusdcV3Mock,
// usdc,
// tok: wcusdcV3,
// rewardToken,
// }
// }

/*
Define helper functions
*/
Expand All @@ -211,47 +158,23 @@ const reduceTargetPerRef = async () => {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
const increaseTargetPerRef = async () => {}

const rocketBalanceKey = ethers.utils.keccak256(ethers.utils.toUtf8Bytes('network.balance.total'))

// prettier-ignore
const reduceRefPerTok = async (
ctx: RethCollateralFixtureContext,
pctDecrease: BigNumberish
pctDecrease: BigNumberish
) => {
const rethNetworkBalances = await ethers.getContractAt(
'IRocketNetworkBalances',
RETH_NETWORK_BALANCES
)
const currentTotalEth = await rethNetworkBalances.getTotalETHBalance()
const lowerBal = currentTotalEth.sub(currentTotalEth.mul(pctDecrease).div(100))
const rocketStorage = await ethers.getContractAt('IRocketStorage', RETH_STORAGE)
await whileImpersonating(RETH_NETWORK_BALANCES, async (rethSigner) => {
await rocketStorage.connect(rethSigner).setUint(rocketBalanceKey, lowerBal)
})

const lastRound = await ctx.refPerTokChainlinkFeed.latestRoundData()
const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100))
const answer = await ctx.refPerTokChainlinkFeed.latestAnswer()
const nextAnswer = answer.sub(answer.div(100).mul(pctDecrease))
await ctx.refPerTokChainlinkFeed.updateAnswer(nextAnswer)
}

// prettier-ignore
const increaseRefPerTok = async (
ctx: RethCollateralFixtureContext,
pctIncrease: BigNumberish
pctIncrease: BigNumberish
) => {
const rethNetworkBalances = await ethers.getContractAt(
'IRocketNetworkBalances',
RETH_NETWORK_BALANCES
)
const currentTotalEth = await rethNetworkBalances.getTotalETHBalance()
const lowerBal = currentTotalEth.add(currentTotalEth.mul(pctIncrease).div(100))
const rocketStorage = await ethers.getContractAt('IRocketStorage', RETH_STORAGE)
await whileImpersonating(RETH_NETWORK_BALANCES, async (rethSigner) => {
await rocketStorage.connect(rethSigner).setUint(rocketBalanceKey, lowerBal)
})

const lastRound = await ctx.refPerTokChainlinkFeed.latestRoundData()
const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100))
const answer = await ctx.refPerTokChainlinkFeed.latestAnswer()
const nextAnswer = answer.add(answer.div(100).mul(pctIncrease))
await ctx.refPerTokChainlinkFeed.updateAnswer(nextAnswer)
}

Expand Down

0 comments on commit 2a592c5

Please sign in to comment.