// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "../../interfaces/IAsset.sol";
import "../../libraries/Fixed.sol";
import "./FiatCollateral.sol";
import "./Asset.sol";
import "./OracleLib.sol";
/**
* @title AppreciatingFiatCollateral
* Collateral that may need revenue hiding to become truly "up only"
*
* For: {tok} != {ref}, {ref} != {target}, {target} == {UoA}
* Inheritors _must_ implement _underlyingRefPerTok()
* Can be easily extended by (optionally) re-implementing:
* - tryPrice()
* - refPerTok()
* - targetPerRef()
* - claimRewards()
* Should not have to re-implement any other methods.
*
* Can intentionally disable default checks by setting config.defaultThreshold to 0
* Can intentionally do no revenue hiding by setting revenueHiding to 0
*/
abstract contract AppreciatingFiatCollateral is FiatCollateral {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;
// revenueShowing = FIX_ONE.minus(revenueHiding)
uint192 public immutable revenueShowing; // {1} The maximum fraction of refPerTok to show
// does not become nonzero until after first refresh()
uint192 public exposedReferencePrice; // {ref/tok} max ref price observed, sub revenue hiding
/// @param config.chainlinkFeed Feed units: {UoA/ref}
/// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide
constructor(CollateralConfig memory config, uint192 revenueHiding) FiatCollateral(config) {
Erequire(revenueHiding < FIX_ONE, "revenueHiding out of range");
revenueShowing = FIX_ONE.minus(revenueHiding);
}
/// Can revert, used by other contract functions in order to catch errors
/// Should not return FIX_MAX for low
/// Should only return FIX_MAX for high if low is 0
/// @dev Override this when pricing is more complicated than just a single oracle
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
/// @return pegPrice {target/ref} The actual price observed in the peg
function tryPrice()
external
view
virtual
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
// {target/ref} = {UoA/ref} / {UoA/target} (1)
pegPrice = chainlinkFeed.price(oracleTimeout);
// {UoA/tok} = {target/ref} * {ref/tok} * {UoA/target} (1)
uint192 p = pegPrice.mul(_underlyingRefPerTok());
uint192 err = p.mul(oracleError, CEIL);
low = p - err;
high = p + err;
// assert(low <= high); obviously true just by inspection
}
/// Should not revert
/// Refresh exchange rates and update default status.
/// @dev Should not need to override: can handle collateral with variable refPerTok()
function refresh() public virtual override {
if (alreadyDefaulted()) {
// continue to update rates
exposedReferencePrice = _underlyingRefPerTok().mul(revenueShowing);
return;
}
CollateralStatus oldStatus = status();
// Check for hard default
// must happen before tryPrice() call since `refPerTok()` returns a stored value
// revenue hiding: do not DISABLE if drawdown is small
uint192 underlyingRefPerTok = _underlyingRefPerTok();
// {ref/tok} = {ref/tok} * {1}
uint192 hiddenReferencePrice = underlyingRefPerTok.mul(revenueShowing);
// uint192(<) is equivalent to Fix.lt
if (underlyingRefPerTok < exposedReferencePrice) {
exposedReferencePrice = hiddenReferencePrice;
markStatus(CollateralStatus.DISABLED);
} else if (hiddenReferencePrice > exposedReferencePrice) {
exposedReferencePrice = hiddenReferencePrice;
}
// Check for soft default + save prices
try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) {
// {UoA/tok}, {UoA/tok}, {target/ref}
// (0, 0) is a valid price; (0, FIX_MAX) is unpriced
// Save prices if priced
Eif (high < FIX_MAX) {
savedLowPrice = low;
savedHighPrice = high;
lastSave = uint48(block.timestamp);
} else {
// must be unpriced
assert(low == 0);
}
// If the price is below the default-threshold price, default eventually
// uint192(+/-) is the same as Fix.plus/minus
if (pegPrice < pegBottom || pegPrice > pegTop || low == 0) {
markStatus(CollateralStatus.IFFY);
} else {
markStatus(CollateralStatus.SOUND);
}
} catch (bytes memory errData) {
// see: docs/solidity-style.md#Catching-Empty-Data
if (errData.length == 0) revert(); // solhint-disable-line reason-string
markStatus(CollateralStatus.IFFY);
}
CollateralStatus newStatus = status();
if (oldStatus != newStatus) {
emit CollateralStatusChanged(oldStatus, newStatus);
}
}
/// @return {ref/tok} Exposed quantity of whole reference units per whole collateral tokens
function refPerTok() public view virtual override returns (uint192) {
return exposedReferencePrice;
}
/// Should update in inheritors
/// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens
function _underlyingRefPerTok() internal view virtual returns (uint192);
}
|