Skip to content

Commit

Permalink
ETH+/ETH collateral feedback (#1141)
Browse files Browse the repository at this point in the history
Co-authored-by: Julian M. Rodriguez <[email protected]>
  • Loading branch information
tbrent and julianmrodri authored May 29, 2024
1 parent 5aecbfc commit 932f8e2
Show file tree
Hide file tree
Showing 24 changed files with 345 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,16 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral {
assert(low == 0);
}

// Check RToken status
// Check pool status: inner RToken must be both isReady() and
// fullyCollateralized() to prevent injection of bad debt.
try pairedBasketHandler.isReady() returns (bool isReady) {
if (!isReady || low == 0 || _anyDepeggedInPool() || _anyDepeggedOutsidePool()) {
if (
!isReady ||
low == 0 ||
_anyDepeggedInPool() ||
_anyDepeggedOutsidePool() ||
!pairedBasketHandler.fullyCollateralized()
) {
// If the price is below the default-threshold price, default eventually
// uint192(+/-) is the same as Fix.plus/minus
markStatus(CollateralStatus.IFFY);
Expand Down Expand Up @@ -119,6 +126,7 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral {
}

/// @dev Not up-only! The RToken can devalue its exchange rate peg
/// @dev Assumption: The RToken BU is intended to equal the reference token in value
/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function underlyingRefPerTok() public view virtual override returns (uint192) {
// {ref/tok} = quantity of the reference unit token in the pool per LP token
Expand Down Expand Up @@ -154,34 +162,13 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral {
function _anyDepeggedInPool() internal view virtual override returns (bool) {
// Assumption: token0 is the RToken; token1 is the reference token

// Check RToken price against reference token, accounting for appreciation
try this.tokenPrice(0) returns (uint192 low0, uint192 high0) {
// {UoA/tok} = {UoA/tok} + {UoA/tok}
uint192 mid0 = (low0 + high0) / 2;
// Check reference token price
try this.tokenPrice(1) returns (uint192 low1, uint192 high1) {
// {target/ref} = {UoA/ref} = {UoA/ref} + {UoA/ref}
uint192 mid1 = (low1 + high1) / 2;

// Remove the appreciation portion of the RToken price
// {UoA/ref} = {UoA/tok} * {tok} / {ref}
mid0 = mid0.muluDivu(rToken.totalSupply(), rToken.basketsNeeded());

try this.tokenPrice(1) returns (uint192 low1, uint192 high1) {
// {target/ref} = {UoA/ref} = {UoA/ref} + {UoA/ref}
uint192 mid1 = (low1 + high1) / 2;

// Check price of reference token
if (mid1 < pegBottom || mid1 > pegTop) return true;

// {target/ref} = {UoA/ref} / {UoA/ref} * {target/ref}
uint192 ratio = mid0.div(mid1); // * targetPerRef(), but we know it's 1

// Check price of RToken relative to reference token
if (ratio < pegBottom || ratio > pegTop) return true;
} catch (bytes memory errData) {
// see: docs/solidity-style.md#Catching-Empty-Data
// untested:
// pattern validated in other plugins, cost to test is high
if (errData.length == 0) revert(); // solhint-disable-line reason-string
return true;
}
// Check price of reference token
if (mid1 < pegBottom || mid1 > pegTop) return true;
} catch (bytes memory errData) {
// see: docs/solidity-style.md#Catching-Empty-Data
// untested:
Expand All @@ -190,6 +177,8 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral {
return true;
}

// The RToken does not need to be monitored given more restrictive hard-default checks

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import "./CurveAppreciatingRTokenFiatCollateral.sol";
* tar = ETH
* UoA = USD
*
* @notice Curve pools with native ETH or ERC777 should be avoided,
* see docs/collateral.md for information
* @notice This Curve Pool contains WETH, which can be used to intercept execution by providing
* `use_eth=true` to remove_liquidity()/remove_liquidity_one_coin(). It is guarded against
* by the recommended method of calling `claim_admin_fees()`.
*/
contract CurveAppreciatingRTokenSelfReferentialCollateral is CurveAppreciatingRTokenFiatCollateral {
using OracleLib for AggregatorV3Interface;
Expand All @@ -36,46 +37,18 @@ contract CurveAppreciatingRTokenSelfReferentialCollateral is CurveAppreciatingRT
PTConfiguration memory ptConfig
) CurveAppreciatingRTokenFiatCollateral(config, revenueHiding, ptConfig) {}

// solhint-enable no-empty-blocks
/// Should not revert (unless CurvePool is re-entrant!)
/// Refresh exchange rates and update default status.
function refresh() public virtual override {
curvePool.claim_admin_fees(); // revert if curve pool is re-entrant
super.refresh();
}

// === Internal ===

function _anyDepeggedInPool() internal view virtual override returns (bool) {
// Assumption: token0 is the RToken; token1 is the reference token

// Check RToken price against reference token, accounting for appreciation
try this.tokenPrice(0) returns (uint192 low0, uint192 high0) {
// {UoA/tok} = {UoA/tok} + {UoA/tok}
uint192 mid0 = (low0 + high0) / 2;

// Remove the appreciation portion of the RToken price
// {UoA/ref} = {UoA/tok} * {tok} / {ref}
mid0 = mid0.muluDivu(rToken.totalSupply(), rToken.basketsNeeded());

try this.tokenPrice(1) returns (uint192 low1, uint192 high1) {
// {UoA/ref} = {UoA/ref} + {UoA/ref}
uint192 mid1 = (low1 + high1) / 2;

// {target/ref} = {UoA/ref} / {UoA/ref} * {target/ref}
uint192 ratio = mid0.div(mid1); // * targetPerRef(), but we know it's 1

// Check price of RToken relative to reference token
if (ratio < pegBottom || ratio > pegTop) return true;
} catch (bytes memory errData) {
// see: docs/solidity-style.md#Catching-Empty-Data
// untested:
// pattern validated in other plugins, cost to test is high
if (errData.length == 0) revert(); // solhint-disable-line reason-string
return true;
}
} catch (bytes memory errData) {
// see: docs/solidity-style.md#Catching-Empty-Data
// untested:
// pattern validated in other plugins, cost to test is high
if (errData.length == 0) revert(); // solhint-disable-line reason-string
return true;
}

// WETH cannot de-peg against ETH (the price feed we have is ETH/USD)
// The RToken does not need to be monitored given more restrictive hard-default checks
return false;
}
}
39 changes: 29 additions & 10 deletions contracts/plugins/assets/curve/CurveRecursiveCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import "../OracleLib.sol";
* - The RToken _must_ be the same RToken using this plugin as collateral!
* - The RToken SHOULD have an RSR overcollateralization layer. DO NOT USE WITHOUT RSR!
* - The LP token should be worth ~2x the reference token. Do not use with 1x lpTokens.
* - Lastly: Do NOT deploy an RToken with this collateral! It can only be swapped
* in at a later date once the RToken has nonzero issuance.
*
* tok = ConvexStakingWrapper or CurveGaugeWrapper
* ref = coins(0) in the pool
Expand All @@ -30,6 +32,9 @@ contract CurveRecursiveCollateral is CurveStableCollateral {

IRToken internal immutable rToken; // token1

// does not become nonzero until after first refresh()
uint192 internal poolVirtualPrice; // {lpToken@t=0/lpToken} max virtual price sub revenue hiding

/// @param config.erc20 must be of type ConvexStakingWrapper or CurveGaugeWrapper
/// @param config.chainlinkFeed Feed units: {UoA/ref}
constructor(
Expand All @@ -38,9 +43,6 @@ contract CurveRecursiveCollateral is CurveStableCollateral {
PTConfiguration memory ptConfig
) CurveStableCollateral(config, revenueHiding, ptConfig) {
rToken = IRToken(address(token1));

// {ref/tok} LP token's virtual price
exposedReferencePrice = _safeWrap(curvePool.get_virtual_price()).mul(revenueShowing);
}

/// Can revert, used by other contract functions in order to catch errors
Expand Down Expand Up @@ -83,25 +85,42 @@ contract CurveRecursiveCollateral is CurveStableCollateral {
function refresh() public virtual override {
CollateralStatus oldStatus = status();

try this.underlyingRefPerTok() returns (uint192) {
try this.underlyingRefPerTok() returns (uint192 underlyingRefPerTok_) {
// Instead of ensuring the underlyingRefPerTok is up-only, solely check
// that the pool's virtual price is up-only. Otherwise this collateral
// would create default cascades.
// would create default cascades when basketsNeeded()/totalSupply() falls.

// === Check for virtualPrice hard default ===

// {ref/tok}
// {lpToken@t=0/lpToken}
uint192 virtualPrice = _safeWrap(curvePool.get_virtual_price());

// {ref/tok} = {ref/tok} * {1}
uint192 hiddenReferencePrice = virtualPrice.mul(revenueShowing);
// {lpToken@t=0/lpToken}
uint192 hiddenVirtualPrice = virtualPrice.mul(revenueShowing);

// uint192(<) is equivalent to Fix.lt
if (virtualPrice < exposedReferencePrice) {
exposedReferencePrice = virtualPrice;
if (virtualPrice < poolVirtualPrice) {
poolVirtualPrice = virtualPrice;
markStatus(CollateralStatus.DISABLED);
} else if (hiddenVirtualPrice > poolVirtualPrice) {
poolVirtualPrice = hiddenVirtualPrice;
}

// === Update exposedReferencePrice, ignoring default ===

// {ref/tok} = {ref/tok} * {1}
uint192 hiddenReferencePrice = underlyingRefPerTok_.mul(revenueShowing);

// uint192(<) is equivalent to Fix.lt
if (underlyingRefPerTok_ < exposedReferencePrice) {
exposedReferencePrice = underlyingRefPerTok_;
// markStatus(CollateralStatus.DISABLED); // don't DISABLE
} else if (hiddenReferencePrice > exposedReferencePrice) {
exposedReferencePrice = hiddenReferencePrice;
}

// === Check for soft default ===

// Check for soft default + save prices
try this.tryPrice() returns (uint192 low, uint192 high, uint192) {
// {UoA/tok}, {UoA/tok}, {UoA/tok}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,16 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral {
assert(low == 0);
}

// Check RToken status
// Check pool status: inner RToken must be both isReady() and
// fullyCollateralized() to prevent injection of bad debt.
try pairedBasketHandler.isReady() returns (bool isReady) {
if (!isReady) {
markStatus(CollateralStatus.IFFY);
} else if (low == 0 || _anyDepeggedInPool() || _anyDepeggedOutsidePool()) {
if (
!isReady ||
low == 0 ||
_anyDepeggedInPool() ||
_anyDepeggedOutsidePool() ||
!pairedBasketHandler.fullyCollateralized()
) {
// If the price is below the default-threshold price, default eventually
// uint192(+/-) is the same as Fix.plus/minus
markStatus(CollateralStatus.IFFY);
Expand Down
12 changes: 11 additions & 1 deletion contracts/plugins/assets/curve/PoolTokens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ import "@openzeppelin/contracts/utils/math/Math.sol";
import "contracts/plugins/assets/OracleLib.sol";
import "contracts/libraries/Fixed.sol";

// solhint-disable func-name-mixedcase
// solhint-disable func-param-name-mixedcase, func-name-mixedcase
interface ICurvePool {
// reentrancy check -- use with ETH / WETH pools
function claim_admin_fees() external;

function remove_liquidity(
uint256 _amount,
uint256[2] calldata min_amounts,

Check warning on line 17 in contracts/plugins/assets/curve/PoolTokens.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Variable name must be in mixedCase
bool use_eth,

Check warning on line 18 in contracts/plugins/assets/curve/PoolTokens.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Variable name must be in mixedCase
address receiver
) external;

// For Curve Plain Pools and V2 Metapools
function coins(uint256) external view returns (address);

Expand Down
9 changes: 9 additions & 0 deletions contracts/plugins/mocks/CurvePoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ contract CurvePoolMock is ICurvePool {
coins = _coins;
}

function claim_admin_fees() external {}

function remove_liquidity(
uint256 _amount,
uint256[2] calldata min_amounts,
bool use_eth,
address receiver
) external {}

function setBalances(uint256[] memory newBalances) external {
_balances = newBalances;
}
Expand Down
20 changes: 20 additions & 0 deletions contracts/plugins/mocks/CurveReentrantReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "../../interfaces/IAsset.sol";

contract CurveReentrantReceiver {
ICollateral curvePlugin;

constructor(ICollateral curvePlugin_) {
curvePlugin = curvePlugin_;
curvePlugin.refresh(); // should not revert yet
}

fallback() external payable {
// should revert if re-entrant
try curvePlugin.refresh() {} catch {
revert("refresh() reverted");
}
}
}
8 changes: 4 additions & 4 deletions scripts/addresses/1-tmp-assets-collateral.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"cUSDCv3": "0x33Ba1BC07b0fafb4BBC1520B330081b91ca6bdf0",
"cvx3Pool": "0x8E5ADdC553962DAcdF48106B6218AC93DA9617b2",
"cvxPayPool": "0x5315Fbe0CEB299F53aE375f65fd9376767C8224c",
"cvxeUSDFRAXBP": "0xE529B59C1764d6E5a274099Eb660DD9e130A5481",
"cvxeUSDFRAXBP": "0x994455cE66Fd984e2A0A0aca453e637810a8f032",
"cvxMIM3Pool": "0x3d21f841C0Fb125176C1DBDF0DE196b071323A75",
"cvxETHPlusETH": "0xc4a5Fb266E8081D605D87f0b1290F54B0a5Dc221",
"crveUSDFRAXBP": "0x945b0ad788dD6dB3864AB23876C68C1bf000d237",
"cvxETHPlusETH": "0x05F164E71C46a8f8FB2ba71550a00eeC9FCd85cd",
"crveUSDFRAXBP": "0xCDC5f5E041b49Cad373E94930E2b3bE30be70535",
"crvMIM3Pool": "0x692cf8CE08d03eF1f8C3dCa82F67935fa9417B62",
"crv3Pool": "0xf59a7987EDd5380cbAb30c37D1c808686f9b67B9",
"sDAI": "0x62a9DDC6FF6077E823690118eCc935d16A8de47e",
Expand Down Expand Up @@ -119,4 +119,4 @@
"CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52",
"CVX": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B"
}
}
}
6 changes: 3 additions & 3 deletions scripts/addresses/mainnet-3.4.0/1-tmp-assets-collateral.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"cUSDCv3": "0x33Ba1BC07b0fafb4BBC1520B330081b91ca6bdf0",
"cvx3Pool": "0x8E5ADdC553962DAcdF48106B6218AC93DA9617b2",
"cvxPayPool": "0x5315Fbe0CEB299F53aE375f65fd9376767C8224c",
"cvxeUSDFRAXBP": "0xE529B59C1764d6E5a274099Eb660DD9e130A5481",
"cvxeUSDFRAXBP": "0x994455cE66Fd984e2A0A0aca453e637810a8f032",
"cvxMIM3Pool": "0x3d21f841C0Fb125176C1DBDF0DE196b071323A75",
"cvxETHPlusETH": "0xc4a5Fb266E8081D605D87f0b1290F54B0a5Dc221",
"crveUSDFRAXBP": "0x945b0ad788dD6dB3864AB23876C68C1bf000d237",
"cvxETHPlusETH": "0x05F164E71C46a8f8FB2ba71550a00eeC9FCd85cd",
"crveUSDFRAXBP": "0xCDC5f5E041b49Cad373E94930E2b3bE30be70535",
"crvMIM3Pool": "0x692cf8CE08d03eF1f8C3dCa82F67935fa9417B62",
"crv3Pool": "0xf59a7987EDd5380cbAb30c37D1c808686f9b67B9",
"sDAI": "0x62a9DDC6FF6077E823690118eCc935d16A8de47e",
Expand Down
Loading

0 comments on commit 932f8e2

Please sign in to comment.