all files / p1/ AssetRegistry.sol

100% Statements 46/46
100% Branches 36/36
100% Functions 13/13
100% Lines 54/54
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                                                                          53× 53× 53× 53× 53× 106×                 1347× 1347× 14195×                       1282×                       95×   94× 92×       94×               64× 63×   62× 59×       61× 61× 61×             5213× 5211×             17335× 17285× 16893×           2162×           288× 288× 288× 3059×             310× 310× 310× 310× 2913× 2913×                     1388×         1387×             1481× 1481× 139×     1342×     1435× 1435×     1435×   1435× 1433×     1435×       156× 156× 155×                    
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../interfaces/IAssetRegistry.sol";
import "../interfaces/IMain.sol";
import "./mixins/Component.sol";
 
/// The AssetRegistry provides the mapping from ERC20 to Asset, allowing the rest of Main
/// to think in terms of ERC20 tokens and target/ref units.
contract AssetRegistryP1 is ComponentP1, IAssetRegistry {
    using EnumerableSet for EnumerableSet.AddressSet;
 
    uint256 public constant GAS_TO_RESERVE = 900000; // just enough to disable basket on n=128
 
    // Peer-component addresses
    IBasketHandler private basketHandler;
    IBackingManager private backingManager;
 
    // Registered ERC20s
    EnumerableSet.AddressSet private _erc20s;
 
    // Registered Assets
    mapping(IERC20 => IAsset) private assets;
 
    /* ==== Contract Invariants ====
       The contract state is just the mapping assets; _erc20s is ignored in properties.
 
       invariant: _erc20s == keys(assets)
       invariant: addr == assets[addr].erc20()
           where: addr in assets
     */
 
    /// Initialize the AssetRegistry with assets
    // effects: assets' = {a.erc20(): a for a in assets_}
    function init(IMain main_, IAsset[] calldata assets_) external initializer {
        __Component_init(main_);
        basketHandler = main_.basketHandler();
        backingManager = main_.backingManager();
        uint256 length = assets_.length;
        for (uint256 i = 0; i < length; ++i) {
            _register(assets_[i]);
        }
    }
 
    /// Update the state of all assets
    /// @custom:refresher
    // actions: calls refresh(c) for c in keys(assets) when c.isCollateral()
    function refresh() public {
        // It's a waste of gas to require notPausedOrFrozen because assets can be updated directly
        uint256 length = _erc20s.length();
        for (uint256 i = 0; i < length; ++i) {
            assets[IERC20(_erc20s.at(i))].refresh();
        }
    }
 
    /// Register `asset`
    /// If either the erc20 address or the asset was already registered, fail
    /// @return true if the erc20 address was not already registered.
    /// @custom:governance
    // checks: asset.erc20() not in keys(assets) or assets[asset.erc20] == asset
    // effects: assets' = assets.set(asset.erc20(), asset)
    // returns: (asset.erc20 not in keys(assets))
    function register(IAsset asset) external governance returns (bool) {
        return _register(asset);
    }
 
    /// Register `asset` if and only if its erc20 address is already registered.
    /// If the erc20 address was not registered, revert.
    /// @return swapped If the asset was swapped for a previously-registered asset
    /// @custom:governance
    // contract
    // checks: asset.erc20() in assets
    // effects: assets' = assets + {asset.erc20(): asset}
    // actions: if asset.erc20() is in basketHandler's basket then basketHandler.disableBasket()
    function swapRegistered(IAsset asset) external governance returns (bool swapped) {
        require(_erc20s.contains(address(asset.erc20())), "no ERC20 collision");
 
        try basketHandler.quantity{ gas: _reserveGas() }(asset.erc20()) returns (uint192 quantity) {
            if (quantity > 0) basketHandler.disableBasket(); // not an interaction
        } catch {
            basketHandler.disableBasket();
        }
 
        swapped = _registerIgnoringCollisions(asset);
    }
 
    /// Unregister an asset, requiring that it is already registered
    /// @custom:governance
    // checks: assets[asset.erc20()] == asset
    // effects: assets' = assets - {asset.erc20():_} + {asset.erc20(), asset}
    function unregister(IAsset asset) external governance {
        require(_erc20s.contains(address(asset.erc20())), "no asset to unregister");
        require(assets[asset.erc20()] == asset, "asset not found");
 
        try basketHandler.quantity{ gas: _reserveGas() }(asset.erc20()) returns (uint192 quantity) {
            if (quantity > 0) basketHandler.disableBasket(); // not an interaction
        } catch {
            basketHandler.disableBasket();
        }
 
        _erc20s.remove(address(asset.erc20()));
        assets[asset.erc20()] = IAsset(address(0));
        emit AssetUnregistered(asset.erc20(), asset);
    }
 
    /// Return the Asset registered for erc20; revert if erc20 is not registered.
    // checks: erc20 in assets
    // returns: assets[erc20]
    function toAsset(IERC20 erc20) external view returns (IAsset) {
        require(_erc20s.contains(address(erc20)), "erc20 unregistered");
        return assets[erc20];
    }
 
    /// Return the Collateral registered for erc20; revert if erc20 is not registered as Collateral
    // checks: erc20 in assets, assets[erc20].isCollateral()
    // returns: assets[erc20]
    function toColl(IERC20 erc20) external view returns (ICollateral) {
        require(_erc20s.contains(address(erc20)), "erc20 unregistered");
        require(assets[erc20].isCollateral(), "erc20 is not collateral");
        return ICollateral(address(assets[erc20]));
    }
 
    /// Returns true if erc20 is registered.
    // returns: (erc20 in assets)
    function isRegistered(IERC20 erc20) external view returns (bool) {
        return _erc20s.contains(address(erc20));
    }
 
    /// Returns keys(assets) as a (duplicate-free) list.
    // returns: [keys(assets)] without duplicates.
    function erc20s() external view returns (IERC20[] memory erc20s_) {
        uint256 length = _erc20s.length();
        erc20s_ = new IERC20[](length);
        for (uint256 i = 0; i < length; ++i) {
            erc20s_[i] = IERC20(_erc20s.at(i));
        }
    }
 
    /// Returns keys(assets), values(assets) as (duplicate-free) lists.
    // returns: [keys(assets)], [values(assets)] without duplicates.
    function getRegistry() external view returns (Registry memory reg) {
        uint256 length = _erc20s.length();
        reg.erc20s = new IERC20[](length);
        reg.assets = new IAsset[](length);
        for (uint256 i = 0; i < length; ++i) {
            reg.erc20s[i] = IERC20(_erc20s.at(i));
            reg.assets[i] = assets[IERC20(_erc20s.at(i))];
        }
    }
 
    /// Register an asset
    /// Forbids registering a different asset for an ERC20 that is already registered
    /// @return registered If the asset was moved from unregistered to registered
    // checks: (asset.erc20() not in assets) or (assets[asset.erc20()] == asset)
    // effects: assets' = assets.set(asset.erc20(), asset)
    // returns: assets.erc20() not in assets
    function _register(IAsset asset) internal returns (bool registered) {
        require(
            !_erc20s.contains(address(asset.erc20())) || assets[asset.erc20()] == asset,
            "duplicate ERC20 detected"
        );
 
        registered = _registerIgnoringCollisions(asset);
    }
 
    /// Register an asset, unregistering any previous asset with the same ERC20.
    // effects: assets' = assets.set(asset.erc20(), asset)
    // returns: assets[asset.erc20()] != asset
    function _registerIgnoringCollisions(IAsset asset) private returns (bool swapped) {
        IERC20Metadata erc20 = asset.erc20();
        if (_erc20s.contains(address(erc20))) {
            if (assets[erc20] == asset) return false;
            else emit AssetUnregistered(erc20, assets[erc20]);
        } else {
            _erc20s.add(address(erc20));
        }
 
        assets[erc20] = asset;
        emit AssetRegistered(erc20, asset);
 
        // Refresh to ensure it does not revert, and to save a recent lastPrice
        asset.refresh();
 
        if (!main.frozen()) {
            backingManager.grantRTokenAllowance(erc20);
        }
 
        return true;
    }
 
    function _reserveGas() private view returns (uint256) {
        uint256 gas = gasleft();
        require(gas > GAS_TO_RESERVE, "not enough gas to unregister safely");
        return gas - GAS_TO_RESERVE;
    }
 
    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[47] private __gap;
}