Morpho Blue

  1// SPDX-License-Identifier: BUSL-1.1
  2pragma solidity 0.8.19;
  3
  4import {
  5    Id,
  6    IMorphoStaticTyping,
  7    IMorphoBase,
  8    MarketParams,
  9    Position,
 10    Market,
 11    Authorization,
 12    Signature
 13} from "./interfaces/IMorpho.sol";
 14import {
 15    IMorphoLiquidateCallback,
 16    IMorphoRepayCallback,
 17    IMorphoSupplyCallback,
 18    IMorphoSupplyCollateralCallback,
 19    IMorphoFlashLoanCallback
 20} from "./interfaces/IMorphoCallbacks.sol";
 21import {IIrm} from "./interfaces/IIrm.sol";
 22import {IERC20} from "./interfaces/IERC20.sol";
 23import {IOracle} from "./interfaces/IOracle.sol";
 24
 25import "./libraries/ConstantsLib.sol";
 26import {UtilsLib} from "./libraries/UtilsLib.sol";
 27import {EventsLib} from "./libraries/EventsLib.sol";
 28import {ErrorsLib} from "./libraries/ErrorsLib.sol";
 29import {MathLib, WAD} from "./libraries/MathLib.sol";
 30import {SharesMathLib} from "./libraries/SharesMathLib.sol";
 31import {MarketParamsLib} from "./libraries/MarketParamsLib.sol";
 32import {SafeTransferLib} from "./libraries/SafeTransferLib.sol";
 33
 34/// @title Morpho
 35/// @author Morpho Labs
 36/// @custom:contact security@morpho.org
 37/// @notice The Morpho contract.
 38contract Morpho is IMorphoStaticTyping {
 39    using MathLib for uint128;
 40    using MathLib for uint256;
 41    using UtilsLib for uint256;
 42    using SharesMathLib for uint256;
 43    using SafeTransferLib for IERC20;
 44    using MarketParamsLib for MarketParams;
 45
 46    /* IMMUTABLES */
 47
 48    /// @inheritdoc IMorphoBase
 49    bytes32 public immutable DOMAIN_SEPARATOR;
 50
 51    /* STORAGE */
 52
 53    /// @inheritdoc IMorphoBase
 54    address public owner;
 55    /// @inheritdoc IMorphoBase
 56    address public feeRecipient;
 57    /// @inheritdoc IMorphoStaticTyping
 58    mapping(Id => mapping(address => Position)) public position;
 59    /// @inheritdoc IMorphoStaticTyping
 60    mapping(Id => Market) public market;
 61    /// @inheritdoc IMorphoBase
 62    mapping(address => bool) public isIrmEnabled;
 63    /// @inheritdoc IMorphoBase
 64    mapping(uint256 => bool) public isLltvEnabled;
 65    /// @inheritdoc IMorphoBase
 66    mapping(address => mapping(address => bool)) public isAuthorized;
 67    /// @inheritdoc IMorphoBase
 68    mapping(address => uint256) public nonce;
 69    /// @inheritdoc IMorphoStaticTyping
 70    mapping(Id => MarketParams) public idToMarketParams;
 71
 72    /* CONSTRUCTOR */
 73
 74    /// @param newOwner The new owner of the contract.
 75    constructor(address newOwner) {
 76        require(newOwner != address(0), ErrorsLib.ZERO_ADDRESS);
 77
 78        DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, block.chainid, address(this)));
 79        owner = newOwner;
 80
 81        emit EventsLib.SetOwner(newOwner);
 82    }
 83
 84    /* MODIFIERS */
 85
 86    /// @dev Reverts if the caller is not the owner.
 87    modifier onlyOwner() {
 88        require(msg.sender == owner, ErrorsLib.NOT_OWNER);
 89        _;
 90    }
 91
 92    /* ONLY OWNER FUNCTIONS */
 93
 94    /// @inheritdoc IMorphoBase
 95    function setOwner(address newOwner) external onlyOwner {
 96        require(newOwner != owner, ErrorsLib.ALREADY_SET);
 97
 98        owner = newOwner;
 99
100        emit EventsLib.SetOwner(newOwner);
101    }
102
103    /// @inheritdoc IMorphoBase
104    function enableIrm(address irm) external onlyOwner {
105        require(!isIrmEnabled[irm], ErrorsLib.ALREADY_SET);
106
107        isIrmEnabled[irm] = true;
108
109        emit EventsLib.EnableIrm(irm);
110    }
111
112    /// @inheritdoc IMorphoBase
113    function enableLltv(uint256 lltv) external onlyOwner {
114        require(!isLltvEnabled[lltv], ErrorsLib.ALREADY_SET);
115        require(lltv < WAD, ErrorsLib.MAX_LLTV_EXCEEDED);
116
117        isLltvEnabled[lltv] = true;
118
119        emit EventsLib.EnableLltv(lltv);
120    }
121
122    /// @inheritdoc IMorphoBase
123    function setFee(MarketParams memory marketParams, uint256 newFee) external onlyOwner {
124        Id id = marketParams.id();
125        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
126        require(newFee != market[id].fee, ErrorsLib.ALREADY_SET);
127        require(newFee <= MAX_FEE, ErrorsLib.MAX_FEE_EXCEEDED);
128
129        // Accrue interest using the previous fee set before changing it.
130        _accrueInterest(marketParams, id);
131
132        // Safe "unchecked" cast.
133        market[id].fee = uint128(newFee);
134
135        emit EventsLib.SetFee(id, newFee);
136    }
137
138    /// @inheritdoc IMorphoBase
139    function setFeeRecipient(address newFeeRecipient) external onlyOwner {
140        require(newFeeRecipient != feeRecipient, ErrorsLib.ALREADY_SET);
141
142        feeRecipient = newFeeRecipient;
143
144        emit EventsLib.SetFeeRecipient(newFeeRecipient);
145    }
146
147    /* MARKET CREATION */
148
149    /// @inheritdoc IMorphoBase
150    function createMarket(MarketParams memory marketParams) external {
151        Id id = marketParams.id();
152        require(isIrmEnabled[marketParams.irm], ErrorsLib.IRM_NOT_ENABLED);
153        require(isLltvEnabled[marketParams.lltv], ErrorsLib.LLTV_NOT_ENABLED);
154        require(market[id].lastUpdate == 0, ErrorsLib.MARKET_ALREADY_CREATED);
155
156        // Safe "unchecked" cast.
157        market[id].lastUpdate = uint128(block.timestamp);
158        idToMarketParams[id] = marketParams;
159
160        emit EventsLib.CreateMarket(id, marketParams);
161
162        // Call to initialize the IRM in case it is stateful.
163        if (marketParams.irm != address(0)) IIrm(marketParams.irm).borrowRate(marketParams, market[id]);
164    }
165
166    /* SUPPLY MANAGEMENT */
167
168    /// @inheritdoc IMorphoBase
169    function supply(
170        MarketParams memory marketParams,
171        uint256 assets,
172        uint256 shares,
173        address onBehalf,
174        bytes calldata data
175    ) external returns (uint256, uint256) {
176        Id id = marketParams.id();
177        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
178        require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);
179        require(onBehalf != address(0), ErrorsLib.ZERO_ADDRESS);
180
181        _accrueInterest(marketParams, id);
182
183        if (assets > 0) shares = assets.toSharesDown(market[id].totalSupplyAssets, market[id].totalSupplyShares);
184        else assets = shares.toAssetsUp(market[id].totalSupplyAssets, market[id].totalSupplyShares);
185
186        position[id][onBehalf].supplyShares += shares;
187        market[id].totalSupplyShares += shares.toUint128();
188        market[id].totalSupplyAssets += assets.toUint128();
189
190        emit EventsLib.Supply(id, msg.sender, onBehalf, assets, shares);
191
192        if (data.length > 0) IMorphoSupplyCallback(msg.sender).onMorphoSupply(assets, data);
193
194        IERC20(marketParams.loanToken).safeTransferFrom(msg.sender, address(this), assets);
195
196        return (assets, shares);
197    }
198
199    /// @inheritdoc IMorphoBase
200    function withdraw(
201        MarketParams memory marketParams,
202        uint256 assets,
203        uint256 shares,
204        address onBehalf,
205        address receiver
206    ) external returns (uint256, uint256) {
207        Id id = marketParams.id();
208        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
209        require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);
210        require(receiver != address(0), ErrorsLib.ZERO_ADDRESS);
211        // No need to verify that onBehalf != address(0) thanks to the following authorization check.
212        require(_isSenderAuthorized(onBehalf), ErrorsLib.UNAUTHORIZED);
213
214        _accrueInterest(marketParams, id);
215
216        if (assets > 0) shares = assets.toSharesUp(market[id].totalSupplyAssets, market[id].totalSupplyShares);
217        else assets = shares.toAssetsDown(market[id].totalSupplyAssets, market[id].totalSupplyShares);
218
219        position[id][onBehalf].supplyShares -= shares;
220        market[id].totalSupplyShares -= shares.toUint128();
221        market[id].totalSupplyAssets -= assets.toUint128();
222
223        require(market[id].totalBorrowAssets <= market[id].totalSupplyAssets, ErrorsLib.INSUFFICIENT_LIQUIDITY);
224
225        emit EventsLib.Withdraw(id, msg.sender, onBehalf, receiver, assets, shares);
226
227        IERC20(marketParams.loanToken).safeTransfer(receiver, assets);
228
229        return (assets, shares);
230    }
231
232    /* BORROW MANAGEMENT */
233
234    /// @inheritdoc IMorphoBase
235    function borrow(
236        MarketParams memory marketParams,
237        uint256 assets,
238        uint256 shares,
239        address onBehalf,
240        address receiver
241    ) external returns (uint256, uint256) {
242        Id id = marketParams.id();
243        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
244        require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);
245        require(receiver != address(0), ErrorsLib.ZERO_ADDRESS);
246        // No need to verify that onBehalf != address(0) thanks to the following authorization check.
247        require(_isSenderAuthorized(onBehalf), ErrorsLib.UNAUTHORIZED);
248
249        _accrueInterest(marketParams, id);
250
251        if (assets > 0) shares = assets.toSharesUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);
252        else assets = shares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);
253
254        position[id][onBehalf].borrowShares += shares.toUint128();
255        market[id].totalBorrowShares += shares.toUint128();
256        market[id].totalBorrowAssets += assets.toUint128();
257
258        require(_isHealthy(marketParams, id, onBehalf), ErrorsLib.INSUFFICIENT_COLLATERAL);
259        require(market[id].totalBorrowAssets <= market[id].totalSupplyAssets, ErrorsLib.INSUFFICIENT_LIQUIDITY);
260
261        emit EventsLib.Borrow(id, msg.sender, onBehalf, receiver, assets, shares);
262
263        IERC20(marketParams.loanToken).safeTransfer(receiver, assets);
264
265        return (assets, shares);
266    }
267
268    /// @inheritdoc IMorphoBase
269    function repay(
270        MarketParams memory marketParams,
271        uint256 assets,
272        uint256 shares,
273        address onBehalf,
274        bytes calldata data
275    ) external returns (uint256, uint256) {
276        Id id = marketParams.id();
277        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
278        require(UtilsLib.exactlyOneZero(assets, shares), ErrorsLib.INCONSISTENT_INPUT);
279        require(onBehalf != address(0), ErrorsLib.ZERO_ADDRESS);
280
281        _accrueInterest(marketParams, id);
282
283        if (assets > 0) shares = assets.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);
284        else assets = shares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);
285
286        position[id][onBehalf].borrowShares -= shares.toUint128();
287        market[id].totalBorrowShares -= shares.toUint128();
288        market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, assets).toUint128();
289
290        // `assets` may be greater than `totalBorrowAssets` by 1.
291        emit EventsLib.Repay(id, msg.sender, onBehalf, assets, shares);
292
293        if (data.length > 0) IMorphoRepayCallback(msg.sender).onMorphoRepay(assets, data);
294
295        IERC20(marketParams.loanToken).safeTransferFrom(msg.sender, address(this), assets);
296
297        return (assets, shares);
298    }
299
300    /* COLLATERAL MANAGEMENT */
301
302    /// @inheritdoc IMorphoBase
303    function supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes calldata data)
304        external
305    {
306        Id id = marketParams.id();
307        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
308        require(assets != 0, ErrorsLib.ZERO_ASSETS);
309        require(onBehalf != address(0), ErrorsLib.ZERO_ADDRESS);
310
311        // Don't accrue interest because it's not required and it saves gas.
312
313        position[id][onBehalf].collateral += assets.toUint128();
314
315        emit EventsLib.SupplyCollateral(id, msg.sender, onBehalf, assets);
316
317        if (data.length > 0) IMorphoSupplyCollateralCallback(msg.sender).onMorphoSupplyCollateral(assets, data);
318
319        IERC20(marketParams.collateralToken).safeTransferFrom(msg.sender, address(this), assets);
320    }
321
322    /// @inheritdoc IMorphoBase
323    function withdrawCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, address receiver)
324        external
325    {
326        Id id = marketParams.id();
327        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
328        require(assets != 0, ErrorsLib.ZERO_ASSETS);
329        require(receiver != address(0), ErrorsLib.ZERO_ADDRESS);
330        // No need to verify that onBehalf != address(0) thanks to the following authorization check.
331        require(_isSenderAuthorized(onBehalf), ErrorsLib.UNAUTHORIZED);
332
333        _accrueInterest(marketParams, id);
334
335        position[id][onBehalf].collateral -= assets.toUint128();
336
337        require(_isHealthy(marketParams, id, onBehalf), ErrorsLib.INSUFFICIENT_COLLATERAL);
338
339        emit EventsLib.WithdrawCollateral(id, msg.sender, onBehalf, receiver, assets);
340
341        IERC20(marketParams.collateralToken).safeTransfer(receiver, assets);
342    }
343
344    /* LIQUIDATION */
345
346    /// @inheritdoc IMorphoBase
347    function liquidate(
348        MarketParams memory marketParams,
349        address borrower,
350        uint256 seizedAssets,
351        uint256 repaidShares,
352        bytes calldata data
353    ) external returns (uint256, uint256) {
354        Id id = marketParams.id();
355        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
356        require(UtilsLib.exactlyOneZero(seizedAssets, repaidShares), ErrorsLib.INCONSISTENT_INPUT);
357
358        _accrueInterest(marketParams, id);
359
360        {
361            uint256 collateralPrice = IOracle(marketParams.oracle).price();
362
363            require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION);
364
365            // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))).
366            uint256 liquidationIncentiveFactor = UtilsLib.min(
367                MAX_LIQUIDATION_INCENTIVE_FACTOR,
368                WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv))
369            );
370
371            if (seizedAssets > 0) {
372                uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE);
373
374                repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesUp(
375                    market[id].totalBorrowAssets, market[id].totalBorrowShares
376                );
377            } else {
378                seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares)
379                    .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
380            }
381        }
382        uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);
383
384        position[id][borrower].borrowShares -= repaidShares.toUint128();
385        market[id].totalBorrowShares -= repaidShares.toUint128();
386        market[id].totalBorrowAssets = UtilsLib.zeroFloorSub(market[id].totalBorrowAssets, repaidAssets).toUint128();
387
388        position[id][borrower].collateral -= seizedAssets.toUint128();
389
390        uint256 badDebtShares;
391        uint256 badDebtAssets;
392        if (position[id][borrower].collateral == 0) {
393            badDebtShares = position[id][borrower].borrowShares;
394            badDebtAssets = UtilsLib.min(
395                market[id].totalBorrowAssets,
396                badDebtShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares)
397            );
398
399            market[id].totalBorrowAssets -= badDebtAssets.toUint128();
400            market[id].totalSupplyAssets -= badDebtAssets.toUint128();
401            market[id].totalBorrowShares -= badDebtShares.toUint128();
402            position[id][borrower].borrowShares = 0;
403        }
404
405        // `repaidAssets` may be greater than `totalBorrowAssets` by 1.
406        emit EventsLib.Liquidate(
407            id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtAssets, badDebtShares
408        );
409
410        IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets);
411
412        if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data);
413
414        IERC20(marketParams.loanToken).safeTransferFrom(msg.sender, address(this), repaidAssets);
415
416        return (seizedAssets, repaidAssets);
417    }
418
419    /* FLASH LOANS */
420
421    /// @inheritdoc IMorphoBase
422    function flashLoan(address token, uint256 assets, bytes calldata data) external {
423        require(assets != 0, ErrorsLib.ZERO_ASSETS);
424
425        emit EventsLib.FlashLoan(msg.sender, token, assets);
426
427        IERC20(token).safeTransfer(msg.sender, assets);
428
429        IMorphoFlashLoanCallback(msg.sender).onMorphoFlashLoan(assets, data);
430
431        IERC20(token).safeTransferFrom(msg.sender, address(this), assets);
432    }
433
434    /* AUTHORIZATION */
435
436    /// @inheritdoc IMorphoBase
437    function setAuthorization(address authorized, bool newIsAuthorized) external {
438        require(newIsAuthorized != isAuthorized[msg.sender][authorized], ErrorsLib.ALREADY_SET);
439
440        isAuthorized[msg.sender][authorized] = newIsAuthorized;
441
442        emit EventsLib.SetAuthorization(msg.sender, msg.sender, authorized, newIsAuthorized);
443    }
444
445    /// @inheritdoc IMorphoBase
446    function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external {
447        /// Do not check whether authorization is already set because the nonce increment is a desired side effect.
448        require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED);
449        require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE);
450
451        bytes32 hashStruct = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, authorization));
452        bytes32 digest = keccak256(bytes.concat("\x19\x01", DOMAIN_SEPARATOR, hashStruct));
453        address signatory = ecrecover(digest, signature.v, signature.r, signature.s);
454
455        require(signatory != address(0) && authorization.authorizer == signatory, ErrorsLib.INVALID_SIGNATURE);
456
457        emit EventsLib.IncrementNonce(msg.sender, authorization.authorizer, authorization.nonce);
458
459        isAuthorized[authorization.authorizer][authorization.authorized] = authorization.isAuthorized;
460
461        emit EventsLib.SetAuthorization(
462            msg.sender, authorization.authorizer, authorization.authorized, authorization.isAuthorized
463        );
464    }
465
466    /// @dev Returns whether the sender is authorized to manage `onBehalf`'s positions.
467    function _isSenderAuthorized(address onBehalf) internal view returns (bool) {
468        return msg.sender == onBehalf || isAuthorized[onBehalf][msg.sender];
469    }
470
471    /* INTEREST MANAGEMENT */
472
473    /// @inheritdoc IMorphoBase
474    function accrueInterest(MarketParams memory marketParams) external {
475        Id id = marketParams.id();
476        require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
477
478        _accrueInterest(marketParams, id);
479    }
480
481    /// @dev Accrues interest for the given market `marketParams`.
482    /// @dev Assumes that the inputs `marketParams` and `id` match.
483    function _accrueInterest(MarketParams memory marketParams, Id id) internal {
484        uint256 elapsed = block.timestamp - market[id].lastUpdate;
485        if (elapsed == 0) return;
486
487        if (marketParams.irm != address(0)) {
488            uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]);
489            uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed));
490            market[id].totalBorrowAssets += interest.toUint128();
491            market[id].totalSupplyAssets += interest.toUint128();
492
493            uint256 feeShares;
494            if (market[id].fee != 0) {
495                uint256 feeAmount = interest.wMulDown(market[id].fee);
496                // The fee amount is subtracted from the total supply in this calculation to compensate for the fact
497                // that total supply is already increased by the full interest (including the fee amount).
498                feeShares =
499                    feeAmount.toSharesDown(market[id].totalSupplyAssets - feeAmount, market[id].totalSupplyShares);
500                position[id][feeRecipient].supplyShares += feeShares;
501                market[id].totalSupplyShares += feeShares.toUint128();
502            }
503
504            emit EventsLib.AccrueInterest(id, borrowRate, interest, feeShares);
505        }
506
507        // Safe "unchecked" cast.
508        market[id].lastUpdate = uint128(block.timestamp);
509    }
510
511    /* HEALTH CHECK */
512
513    /// @dev Returns whether the position of `borrower` in the given market `marketParams` is healthy.
514    /// @dev Assumes that the inputs `marketParams` and `id` match.
515    function _isHealthy(MarketParams memory marketParams, Id id, address borrower) internal view returns (bool) {
516        if (position[id][borrower].borrowShares == 0) return true;
517
518        uint256 collateralPrice = IOracle(marketParams.oracle).price();
519
520        return _isHealthy(marketParams, id, borrower, collateralPrice);
521    }
522
523    /// @dev Returns whether the position of `borrower` in the given market `marketParams` with the given
524    /// `collateralPrice` is healthy.
525    /// @dev Assumes that the inputs `marketParams` and `id` match.
526    /// @dev Rounds in favor of the protocol, so one might not be able to borrow exactly `maxBorrow` but one unit less.
527    function _isHealthy(MarketParams memory marketParams, Id id, address borrower, uint256 collateralPrice)
528        internal
529        view
530        returns (bool)
531    {
532        uint256 borrowed = uint256(position[id][borrower].borrowShares).toAssetsUp(
533            market[id].totalBorrowAssets, market[id].totalBorrowShares
534        );
535        uint256 maxBorrow = uint256(position[id][borrower].collateral).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE)
536            .wMulDown(marketParams.lltv);
537
538        return maxBorrow >= borrowed;
539    }
540
541    /* STORAGE VIEW */
542
543    /// @inheritdoc IMorphoBase
544    function extSloads(bytes32[] calldata slots) external view returns (bytes32[] memory res) {
545        uint256 nSlots = slots.length;
546
547        res = new bytes32[](nSlots);
548
549        for (uint256 i; i < nSlots;) {
550            bytes32 slot = slots[i++];
551
552            assembly ("memory-safe") {
553                mstore(add(res, mul(i, 32)), sload(slot))
554            }
555        }
556    }
557}