all files / plugins/assets/convex/ PoolTokens.sol

82.05% Statements 64/78
63.16% Branches 96/152
100% Functions 8/8
83.7% Lines 113/135
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 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347                                                                                                                                                                                                                  302× 299× 295×       289×   285× 285× 285×                 285× 285× 780× 780×               285× 285× 285× 285×                 285× 285× 285× 285× 285× 285× 281× 277×     273× 273× 273× 273× 273×           269× 269× 269× 269× 269× 269× 265× 261×     257× 257× 257× 257× 257× 61× 57× 57×       253× 253× 253× 253× 253× 186× 183× 180×     244× 244× 244× 244× 244×           241× 241× 241× 241× 241×           241× 241× 241× 241× 241×                     688×       688× 688× 688× 688×     688× 274× 251× 251×         251× 243× 243× 36× 36×     163× 163× 163×                         657×               167× 405× 405× 405×   374× 374×           14×   14× 42× 42× 42×     14×           447× 447× 266× 108×         292× 292× 800×   292×       299× 299× 819×   299×                         657× 657×      
// SPDX-License-Identifier: ISC
pragma solidity 0.8.17;
 
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "contracts/plugins/assets/OracleLib.sol";
import "contracts/libraries/Fixed.sol";
 
// solhint-disable func-name-mixedcase
interface ICurvePool {
    // For Curve Plain Pools and V2 Metapools
    function coins(uint256) external view returns (address);
 
    // Only exists in Curve Lending Pools
    function underlying_coins(uint256) external view returns (address);
 
    // Only exists in V1 Curve Metapools; not used currently
    function base_coins(uint256) external view returns (address);
 
    function balances(uint256) external view returns (uint256);
 
    function get_virtual_price() external view returns (uint256);
 
    function token() external view returns (address);
 
    function exchange(
        int128,
        int128,
        uint256,
        uint256
    ) external;
}
 
// solhint-enable func-name-mixedcase
 
/// Supports CvxCurve non-meta pools for up to 4 tokens
contract PoolTokens {
    using OracleLib for AggregatorV3Interface;
    using FixLib for uint192;
 
    error WrongIndex(uint8 maxLength);
    error NoToken(uint8 tokenNumber);
 
    enum CurvePoolType {
        Plain,
        Lending,
        Metapool // not supported via this class. parent class handles metapool math
    }
 
    // === State (Immutable) ===
 
    ICurvePool public immutable curvePool;
    IERC20Metadata public immutable lpToken;
    uint8 internal immutable nTokens;
 
    IERC20Metadata internal immutable token0;
    IERC20Metadata internal immutable token1;
    IERC20Metadata internal immutable token2;
    IERC20Metadata internal immutable token3;
 
    // For each token, we maintain up to two feeds/timeouts/errors
    // The data below would normally be a struct, but we want bytecode substitution
 
    AggregatorV3Interface internal immutable _t0feed0;
    AggregatorV3Interface internal immutable _t0feed1;
    uint48 internal immutable _t0timeout0; // {s}
    uint48 internal immutable _t0timeout1; // {s}
    uint192 internal immutable _t0error0; // {1}
    uint192 internal immutable _t0error1; // {1}
 
    AggregatorV3Interface internal immutable _t1feed0;
    AggregatorV3Interface internal immutable _t1feed1;
    uint48 internal immutable _t1timeout0; // {s}
    uint48 internal immutable _t1timeout1; // {s}
    uint192 internal immutable _t1error0; // {1}
    uint192 internal immutable _t1error1; // {1}
 
    AggregatorV3Interface internal immutable _t2feed0;
    AggregatorV3Interface internal immutable _t2feed1;
    uint48 internal immutable _t2timeout0; // {s}
    uint48 internal immutable _t2timeout1; // {s}
    uint192 internal immutable _t2error0; // {1}
    uint192 internal immutable _t2error1; // {1}
 
    AggregatorV3Interface internal immutable _t3feed0;
    AggregatorV3Interface internal immutable _t3feed1;
    uint48 internal immutable _t3timeout0; // {s}
    uint48 internal immutable _t3timeout1; // {s}
    uint192 internal immutable _t3error0; // {1}
    uint192 internal immutable _t3error1; // {1}
 
    // === Config ===
 
    struct PTConfiguration {
        uint8 nTokens;
        ICurvePool curvePool;
        IERC20Metadata lpToken;
        CurvePoolType poolType;
        AggregatorV3Interface[][] feeds; // row should multiply to give {UoA/ref}; max columns is 2
        uint48[][] oracleTimeouts; // {s} same order as feeds
        uint192[][] oracleErrors; // {1} same order as feeds
    }
 
    constructor(PTConfiguration memory config) {
        require(config.nTokens <= 4, "up to 4 tokens max");
        require(maxFeedsLength(config.feeds) <= 2, "price feeds limited to 2");
        require(
            config.feeds.length == config.nTokens && minFeedsLength(config.feeds) > 0,
            "each token needs at least 1 price feed"
        );
        require(address(config.curvePool) != address(0), "curvePool address is zero");
 
        curvePool = config.curvePool;
        nTokens = config.nTokens;
        lpToken = config.lpToken;
 
        // Solidity does not support immutable arrays. This is a hack to get the equivalent of
        // an immutable array so we do not have store the token feeds in the blockchain. This is
        // a gas optimization since it is significantly more expensive to read and write on the
        // blockchain than it is to use embedded values in the bytecode.
 
        // === Tokens ===
 
        IERC20Metadata[] memory tokens = new IERC20Metadata[](nTokens);
        for (uint8 i = 0; i < nTokens; ++i) {
            Eif (config.poolType == CurvePoolType.Plain) {
                tokens[i] = IERC20Metadata(curvePool.coins(i));
            } else if (config.poolType == CurvePoolType.Lending) {
                tokens[i] = IERC20Metadata(curvePool.underlying_coins(i));
            } else {
                revert("Use MetaPoolTokens class");
            }
        }
 
        token0 = tokens[0];
        token1 = tokens[1];
        token2 = (nTokens > 2) ? tokens[2] : IERC20Metadata(address(0));
        token3 = (nTokens > 3) ? Itokens[3] : IERC20Metadata(address(0));
 
        // === Feeds + timeouts ===
        // I know this lots extremely verbose and quite silly, but it actually makes sense:
        //   - immutable variables cannot be conditionally written to
        //   - a struct or an array would not be able to be immutable
        //   - immutable variables means values get in-lined in the bytecode
 
        // token0
        bool more = config.feeds[0].length > 0;
        _t0feed0 = more ? config.feeds[0][0] : EAggregatorV3Interface(address(0));
        _t0timeout0 = more && config.oracleTimeouts[0].length > 0 ? config.oracleTimeouts[0][0] : E0;
        _t0error0 = more && config.oracleErrors[0].length > 0 ? config.oracleErrors[0][0] : E0;
        Eif (more) {
            require(address(_t0feed0) != address(0), "t0feed0 empty");
            require(_t0timeout0 > 0, "t0timeout0 zero");
            require(_t0error0 < FIX_ONE, "t0error0 too large");
        }
 
        more = config.feeds[0].length > 1;
        _t0feed1 = more ? config.feeds[0][1] : AggregatorV3Interface(address(0));
        _t0timeout1 = more && config.oracleTimeouts[0].length > 1 ? Iconfig.oracleTimeouts[0][1] : 0;
        _t0error1 = more && config.oracleErrors[0].length > 1 ? Iconfig.oracleErrors[0][1] : 0;
        if (more) {
            Irequire(address(_t0feed1) != address(0), "t0feed1 empty");
            require(_t0timeout1 > 0, "t0timeout1 zero");
            require(_t0error1 < FIX_ONE, "t0error1 too large");
        }
 
        // token1
        more = config.feeds[1].length > 0;
        _t1feed0 = more ? config.feeds[1][0] : EAggregatorV3Interface(address(0));
        _t1timeout0 = more && config.oracleTimeouts[1].length > 0 ? config.oracleTimeouts[1][0] : E0;
        _t1error0 = more && config.oracleErrors[1].length > 0 ? config.oracleErrors[1][0] : E0;
        Eif (more) {
            require(address(_t1feed0) != address(0), "t1feed0 empty");
            require(_t1timeout0 > 0, "t1timeout0 zero");
            require(_t1error0 < FIX_ONE, "t1error0 too large");
        }
 
        more = config.feeds[1].length > 1;
        _t1feed1 = more ? config.feeds[1][1] : AggregatorV3Interface(address(0));
        _t1timeout1 = more && config.oracleTimeouts[1].length > 1 ? config.oracleTimeouts[1][1] : 0;
        _t1error1 = more && config.oracleErrors[1].length > 1 ? config.oracleErrors[1][1] : 0;
        if (more) {
            require(address(_t1feed1) != address(0), "t1feed1 empty");
            Erequire(_t1timeout1 > 0, "t1timeout1 zero");
            Erequire(_t1error1 < FIX_ONE, "t1error1 too large");
        }
 
        // token2
        more = config.feeds.length > 2 && config.feeds[2].length > 0;
        _t2feed0 = more ? config.feeds[2][0] : AggregatorV3Interface(address(0));
        _t2timeout0 = more && config.oracleTimeouts[2].length > 0 ? config.oracleTimeouts[2][0] : 0;
        _t2error0 = more && config.oracleErrors[2].length > 0 ? config.oracleErrors[2][0] : 0;
        if (more) {
            require(address(_t2feed0) != address(0), "t2feed0 empty");
            require(_t2timeout0 > 0, "t2timeout0 zero");
            require(_t2error0 < FIX_ONE, "t2error0 too large");
        }
 
        more = config.feeds.length > 2 && config.feeds[2].length > 1;
        _t2feed1 = more ? config.feeds[2][1] : AggregatorV3Interface(address(0));
        _t2timeout1 = more && config.oracleTimeouts[2].length > 1 ? Iconfig.oracleTimeouts[2][1] : 0;
        _t2error1 = more && config.oracleErrors[2].length > 1 ? Iconfig.oracleErrors[2][1] : 0;
        if (more) {
            Irequire(address(_t2feed1) != address(0), "t2feed1 empty");
            require(_t2timeout1 > 0, "t2timeout1 zero");
            require(_t2error1 < FIX_ONE, "t2error1 too large");
        }
 
        // token3
        more = config.feeds.length > 3 && config.feeds[3].length > 0;
        _t3feed0 = more ? Iconfig.feeds[3][0] : AggregatorV3Interface(address(0));
        _t3timeout0 = more && config.oracleTimeouts[3].length > 0 ? Iconfig.oracleTimeouts[3][0] : 0;
        _t3error0 = more && config.oracleErrors[3].length > 0 ? Iconfig.oracleErrors[3][0] : 0;
        Iif (more) {
            require(address(_t3feed0) != address(0), "t3feed0 empty");
            require(_t3timeout0 > 0, "t3timeout0 zero");
            require(_t3error0 < FIX_ONE, "t3error0 too large");
        }
 
        more = config.feeds.length > 3 && config.feeds[3].length > 1;
        _t3feed1 = more ? Iconfig.feeds[3][1] : AggregatorV3Interface(address(0));
        _t3timeout1 = more && config.oracleTimeouts[3].length > 1 ? Iconfig.oracleTimeouts[3][1] : 0;
        _t3error1 = more && config.oracleErrors[3].length > 1 ? Iconfig.oracleErrors[3][1] : 0;
        Iif (more) {
            require(address(_t3feed1) != address(0), "t3feed1 empty");
            require(_t3timeout1 > 0, "t3timeout1 zero");
            require(_t3error1 < FIX_ONE, "t3error1 too large");
        }
    }
 
    /// @param index The index of the token: 0, 1, 2, or 3
    /// @return low {UoA/ref_index}
    /// @return high {UoA/ref_index}
    function tokenPrice(uint8 index) public view returns (uint192 low, uint192 high) {
        Iif (index >= nTokens) revert WrongIndex(nTokens - 1);
 
        // Use only 1 feed if 2nd feed not defined
        // otherwise: multiply feeds together, e.g; {UoA/ref} = {UoA/target} * {target/ref}
        uint192 x;
        uint192 y = FIX_ONE;
        uint192 xErr; // {1}
        uint192 yErr; // {1}
        // if only 1 feed: `y` is FIX_ONE and `yErr` is 0
 
        if (index == 0) {
            x = _t0feed0.price(_t0timeout0);
            xErr = _t0error0;
            Iif (address(_t0feed1) != address(0)) {
                y = _t0feed1.price(_t0timeout1);
                yErr = _t0error1;
            }
        } else if (index == 1) {
            x = _t1feed0.price(_t1timeout0);
            xErr = _t1error0;
            if (address(_t1feed1) != address(0)) {
                y = _t1feed1.price(_t1timeout1);
                yErr = _t1error1;
            }
        } else Eif (index == 2) {
            x = _t2feed0.price(_t2timeout0);
            xErr = _t2error0;
            Iif (address(_t2feed1) != address(0)) {
                y = _t2feed1.price(_t2timeout1);
                yErr = _t2error1;
            }
        } else {
            x = _t3feed0.price(_t3timeout0);
            xErr = _t3error0;
            if (address(_t3feed1) != address(0)) {
                y = _t3feed1.price(_t3timeout1);
                yErr = _t3error1;
            }
        }
 
        return toRange(x, y, xErr, yErr);
    }
 
    // === Internal ===
 
    /// @return low {UoA}
    /// @return high {UoA}
    function totalBalancesValue() internal view returns (uint192 low, uint192 high) {
        for (uint8 i = 0; i < nTokens; ++i) {
            IERC20Metadata token = getToken(i);
            uint192 balance = shiftl_toFix(curvePool.balances(i), -int8(token.decimals()));
            (uint192 lowP, uint192 highP) = tokenPrice(i);
 
            low += balance.mul(lowP, FLOOR);
            high += balance.mul(highP, CEIL);
        }
    }
 
    /// @return [{tok}]
    function getBalances() internal view virtual returns (uint192[] memory) {
        uint192[] memory balances = new uint192[](nTokens);
 
        for (uint8 i = 0; i < nTokens; ++i) {
            IERC20Metadata token = getToken(i);
            uint192 balance = shiftl_toFix(curvePool.balances(i), -int8(token.decimals()));
            balances[i] = (balance);
        }
 
        return balances;
    }
 
    // === Private ===
 
    function getToken(uint8 index) private view returns (IERC20Metadata) {
        Iif (index >= nTokens) revert WrongIndex(nTokens - 1);
        if (index == 0) return token0;
        if (index == 1) return token1;
        Eif (index == 2) return token2;
        return token3;
    }
 
    function minFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) {
        uint8 minLength = type(uint8).max;
        for (uint8 i = 0; i < feeds.length; ++i) {
            minLength = uint8(Math.min(minLength, feeds[i].length));
        }
        return minLength;
    }
 
    function maxFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) {
        uint8 maxLength;
        for (uint8 i = 0; i < feeds.length; ++i) {
            maxLength = uint8(Math.max(maxLength, feeds[i].length));
        }
        return maxLength;
    }
 
    /// x and y can be any two fixes that can be multiplied
    /// @param xErr {1} error associated with x
    /// @param yErr {1} error associated with y
    /// returns low and high extremes of x * y, given errors
    function toRange(
        uint192 x,
        uint192 y,
        uint192 xErr,
        uint192 yErr
    ) private pure returns (uint192 low, uint192 high) {
        low = x.mul(FIX_ONE - xErr).mul(y.mul(FIX_ONE - yErr), FLOOR);
        high = x.mul(FIX_ONE + xErr).mul(y.mul(FIX_ONE + yErr), CEIL);
    }
}