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}