all files / plugins/assets/ FiatCollateral.sol

100% Statements 40/40
89.47% Branches 34/38
100% Functions 10/10
100% Lines 45/45
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219                                                                                                                                              3935× 3922× 3777×   3909×   3909× 3909×     3909×     3909× 3909× 3909×                                           6800×     6752×   6752× 6752×               5553× 5192×     5192×         5165× 5164× 5164× 5164×             5165× 359×   4806×       27× 23×     5188× 5188× 287×           30490× 28791×   1035×   664×                 10352×   10281× 9686×   523×     523×           72×         10842×       120×             5115×         8164×         20508×      
// 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 "./Asset.sol";
import "./OracleLib.sol";
 
uint48 constant MAX_DELAY_UNTIL_DEFAULT = 1209600; // {s} 2 weeks
 
struct CollateralConfig {
    uint48 priceTimeout; // {s} The number of seconds over which saved prices decay
    AggregatorV3Interface chainlinkFeed; // Feed units: {target/ref}
    uint192 oracleError; // {1} The % the oracle feed can be off by
    IERC20Metadata erc20; // The ERC20 of the collateral token
    uint192 maxTradeVolume; // {UoA} The max trade volume, in UoA
    uint48 oracleTimeout; // {s} The number of seconds until a oracle value becomes invalid
    bytes32 targetName; // The bytes32 representation of the target name
    uint192 defaultThreshold; // {1} A value like 0.05 that represents a deviation tolerance
    // set defaultThreshold to zero to create SelfReferentialCollateral
    uint48 delayUntilDefault; // {s} The number of seconds an oracle can mulfunction
}
 
/**
 * @title FiatCollateral
 * Parent class for all collateral. Can be extended to support appreciating collateral
 *
 * For: {tok} == {ref}, {ref} != {target}, {target} == {UoA}
 * Can be easily extended by (optionally) re-implementing:
 *   - tryPrice()
 *   - refPerTok()
 *   - targetPerRef()
 *   - claimRewards()
 * If you have appreciating collateral, then you should use AppreciatingFiatCollateral or
 * override refresh() yourself.
 *
 * Can intentionally disable default checks by setting config.defaultThreshold to 0
 */
contract FiatCollateral is ICollateral, Asset {
    using FixLib for uint192;
    using OracleLib for AggregatorV3Interface;
 
    // Default Status:
    // _whenDefault == NEVER: no risk of default (initial value)
    // _whenDefault > block.timestamp: delayed default may occur as soon as block.timestamp.
    //                In this case, the asset may recover, reachiving _whenDefault == NEVER.
    // _whenDefault <= block.timestamp: default has already happened (permanently)
    uint48 private constant NEVER = type(uint48).max;
    uint48 private _whenDefault = NEVER;
 
    uint48 public immutable delayUntilDefault; // {s} e.g 86400
 
    // targetName: The canonical name of this collateral's target unit.
    bytes32 public immutable targetName;
 
    uint192 public immutable pegBottom; // {target/ref} The bottom of the peg
 
    uint192 public immutable pegTop; // {target/ref} The top of the peg
 
    /// @param config.chainlinkFeed Feed units: {UoA/ref}
    constructor(CollateralConfig memory config)
        Asset(
            config.priceTimeout,
            config.chainlinkFeed,
            config.oracleError,
            config.erc20,
            config.maxTradeVolume,
            config.oracleTimeout
        )
    {
        require(config.targetName != bytes32(0), "targetName missing");
        if (config.defaultThreshold > 0) {
            require(config.delayUntilDefault > 0, "delayUntilDefault zero");
        }
        Erequire(config.delayUntilDefault <= 1209600, "delayUntilDefault too long");
 
        targetName = config.targetName;
        delayUntilDefault = config.delayUntilDefault;
 
        // Cache constants
        uint192 peg = targetPerRef(); // {target/ref}
 
        // {target/ref} = {target/ref} * {1}
        uint192 delta = peg.mul(config.defaultThreshold);
        pegBottom = peg - delta;
        pegTop = peg + delta;
    }
 
    /// 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);
 
        // {target/ref} = {target/ref} * {1}
        uint192 err = pegPrice.mul(oracleError, CEIL);
 
        low = pegPrice - err;
        high = pegPrice + err;
        // assert(low <= high); obviously true just by inspection
    }
 
    /// Should not revert
    /// Refresh exchange rates and update default status.
    /// @dev May need to override: limited to handling collateral with refPerTok() = 1
    function refresh() public virtual override(Asset, IAsset) {
        if (alreadyDefaulted()) return;
        CollateralStatus oldStatus = status();
 
        // Check for soft default + save lotPrice
        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
            if (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 The collateral's status
    function status() public view returns (CollateralStatus) {
        if (_whenDefault == NEVER) {
            return CollateralStatus.SOUND;
        } else if (_whenDefault > block.timestamp) {
            return CollateralStatus.IFFY;
        } else {
            return CollateralStatus.DISABLED;
        }
    }
 
    // === Helpers for child classes ===
 
    function markStatus(CollateralStatus status_) internal {
        // untestable:
        //      All calls to markStatus happen exclusively if the collateral is not defaulted
        if (_whenDefault <= block.timestamp) return; // prevent DISABLED -> SOUND/IFFY
 
        if (status_ == CollateralStatus.SOUND) {
            _whenDefault = NEVER;
        } else if (status_ == CollateralStatus.IFFY) {
            uint256 sum = block.timestamp + uint256(delayUntilDefault);
            // untestable:
            //      constructor enforces max length on delayUntilDefault
            Iif (sum >= NEVER) _whenDefault = NEVER;
            else if (sum < _whenDefault) _whenDefault = uint48(sum);
            // else: no change to _whenDefault
            // untested:
            //      explicit `if` to check DISABLED. else branch will never be hit
        } else Eif (status_ == CollateralStatus.DISABLED) {
            _whenDefault = uint48(block.timestamp);
        }
    }
 
    function alreadyDefaulted() internal view returns (bool) {
        return _whenDefault <= block.timestamp;
    }
 
    function whenDefault() external view returns (uint256) {
        return _whenDefault;
    }
 
    // === End child helpers ===
 
    /// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
    function refPerTok() public view virtual returns (uint192) {
        return FIX_ONE;
    }
 
    /// @return {target/ref} Quantity of whole target units per whole reference unit in the peg
    function targetPerRef() public view virtual returns (uint192) {
        return FIX_ONE;
    }
 
    /// @return If the asset is an instance of ICollateral or not
    function isCollateral() external pure virtual override(Asset, IAsset) returns (bool) {
        return true;
    }
}