From 64a999cebef5ce61162c64f3e284b831e6cad1db Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 16 Dec 2022 18:39:22 -0500 Subject: [PATCH] Implement a proposed contract structure Implement a proposed contract structure Update Update Add more internal Add and rework more internal format Format Add more internal Add more internal Remove timestamp usage Move stuff around Remove unnecessary constants Address comments Remove some unused libs and test signed commit --- .gitmodules | 9 + lib/aave-v3-core | 1 + lib/aave-v3-periphery | 1 + lib/morpho-utils | 1 + remappings.txt | 13 +- src/EntryPositionsManager.sol | 8 + src/ExitPositionsManager.sol | 8 + src/MatchingEngine.sol | 171 +++++++ src/Morpho.sol | 43 +- src/MorphoGettersAndSetters.sol | 72 +++ src/MorphoInternal.sol | 452 ++++++++++++++++++ src/MorphoStorage.sol | 41 ++ src/interfaces/IERC1155.sol | 113 +++++ src/interfaces/Interfaces.sol | 12 + src/interfaces/aave/IAToken.sol | 6 + src/interfaces/aave/IPool.sol | 76 +++ .../aave/IPoolAddressesProvider.sol | 10 + src/interfaces/aave/IVariableDebtToken.sol | 8 + src/libraries/Constants.sol | 15 + src/libraries/Errors.sol | 4 +- src/libraries/Events.sol | 20 +- src/libraries/Libraries.sol | 19 + src/libraries/MarketLib.sol | 61 +++ src/libraries/MarketMaskLib.sol | 93 ++++ src/libraries/PoolInteractions.sol | 35 ++ src/libraries/Types.sol | 100 +++- src/libraries/aave/DataTypes.sol | 30 ++ src/libraries/aave/ReserveConfiguration.sol | 77 +++ src/libraries/aave/UserConfiguration.sol | 16 + yarn.lock | 14 +- 30 files changed, 1474 insertions(+), 55 deletions(-) create mode 160000 lib/aave-v3-core create mode 160000 lib/aave-v3-periphery create mode 160000 lib/morpho-utils create mode 100644 src/EntryPositionsManager.sol create mode 100644 src/ExitPositionsManager.sol create mode 100644 src/MatchingEngine.sol create mode 100644 src/MorphoGettersAndSetters.sol create mode 100644 src/MorphoInternal.sol create mode 100644 src/MorphoStorage.sol create mode 100644 src/interfaces/IERC1155.sol create mode 100644 src/interfaces/Interfaces.sol create mode 100644 src/interfaces/aave/IAToken.sol create mode 100644 src/interfaces/aave/IPool.sol create mode 100644 src/interfaces/aave/IPoolAddressesProvider.sol create mode 100644 src/interfaces/aave/IVariableDebtToken.sol create mode 100644 src/libraries/Constants.sol create mode 100644 src/libraries/Libraries.sol create mode 100644 src/libraries/MarketLib.sol create mode 100644 src/libraries/MarketMaskLib.sol create mode 100644 src/libraries/PoolInteractions.sol create mode 100644 src/libraries/aave/DataTypes.sol create mode 100644 src/libraries/aave/ReserveConfiguration.sol create mode 100644 src/libraries/aave/UserConfiguration.sol diff --git a/.gitmodules b/.gitmodules index 4971881cd..bfef8b2de 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,12 @@ [submodule "lib/morpho-data-structures"] path = lib/morpho-data-structures url = https://github.com/morpho-dao/morpho-data-structures +[submodule "lib/morpho-utils"] + path = lib/morpho-utils + url = https://github.com/morpho-dao/morpho-utils +[submodule "lib/aave-v3-core"] + path = lib/aave-v3-core + url = https://github.com/aave/aave-v3-core +[submodule "lib/aave-v3-periphery"] + path = lib/aave-v3-periphery + url = https://github.com/aave/aave-v3-periphery diff --git a/lib/aave-v3-core b/lib/aave-v3-core new file mode 160000 index 000000000..f3e037b36 --- /dev/null +++ b/lib/aave-v3-core @@ -0,0 +1 @@ +Subproject commit f3e037b3638e3b7c98f0c09c56c5efde54f7c5d2 diff --git a/lib/aave-v3-periphery b/lib/aave-v3-periphery new file mode 160000 index 000000000..932f362fe --- /dev/null +++ b/lib/aave-v3-periphery @@ -0,0 +1 @@ +Subproject commit 932f362feafb3df2c2d59a852582fa7f1f4f1930 diff --git a/lib/morpho-utils b/lib/morpho-utils new file mode 160000 index 000000000..dbf66924e --- /dev/null +++ b/lib/morpho-utils @@ -0,0 +1 @@ +Subproject commit dbf66924e2c302171fcccb87fb23a9e2fd14e3cc diff --git a/remappings.txt b/remappings.txt index 4615f537f..dca2a615b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,10 @@ -ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ -openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts -morpho-data-structures/=lib/morpho-data-structures/contracts/ +@ds-test/=lib/forge-std/lib/ds-test/src/ +@forge-std/=lib/forge-std/src/ + +@morpho-data-structures/=lib/morpho-data-structures/contracts/ +@morpho-utils/=lib/morpho-utils/src/ + +@openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts @openzeppelin/=node_modules/@openzeppelin/ +@aave/core-v3/=lib/aave-v3-core/ +@aave/periphery-v3/=lib/aave-v3-periphery/ \ No newline at end of file diff --git a/src/EntryPositionsManager.sol b/src/EntryPositionsManager.sol new file mode 100644 index 000000000..1fbc2351d --- /dev/null +++ b/src/EntryPositionsManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types, Events, Errors, MarketLib} from "./libraries/Libraries.sol"; + +import {MatchingEngine} from "./MatchingEngine.sol"; + +contract EntryPositionsManager is MatchingEngine {} diff --git a/src/ExitPositionsManager.sol b/src/ExitPositionsManager.sol new file mode 100644 index 000000000..38a082801 --- /dev/null +++ b/src/ExitPositionsManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types, Events, Errors, MarketLib} from "./libraries/Libraries.sol"; + +import {MatchingEngine} from "./MatchingEngine.sol"; + +contract ExitPositionsManager is MatchingEngine {} diff --git a/src/MatchingEngine.sol b/src/MatchingEngine.sol new file mode 100644 index 000000000..94e23ab5c --- /dev/null +++ b/src/MatchingEngine.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types, Events, ThreeHeapOrdering, Math, WadRayMath} from "./libraries/Libraries.sol"; + +import {MorphoInternal} from "./MorphoInternal.sol"; + +abstract contract MatchingEngine is MorphoInternal { + using Math for uint256; + using ThreeHeapOrdering for ThreeHeapOrdering.HeapArray; + using WadRayMath for uint256; + + function _matchSuppliers(address poolToken, uint256 amount, uint256 maxLoops) + internal + returns (uint256 matched, uint256 loopsDone) + { + Types.Market storage market = _market[poolToken]; + return _matchOrUnmatch( + _marketBalances[poolToken].suppliersPool, + _marketBalances[poolToken].suppliersP2P, + Types.MatchVars({ + poolToken: poolToken, + poolIndex: market.poolSupplyIndex, + p2pIndex: market.p2pSupplyIndex, + amount: amount, + maxLoops: maxLoops, + borrow: false, + matching: true + }) + ); + } + + function _matchBorrowers(address poolToken, uint256 amount, uint256 maxLoops) + internal + returns (uint256 matched, uint256 loopsDone) + { + Types.Market storage market = _market[poolToken]; + return _matchOrUnmatch( + _marketBalances[poolToken].borrowersPool, + _marketBalances[poolToken].borrowersP2P, + Types.MatchVars({ + poolToken: poolToken, + poolIndex: market.poolBorrowIndex, + p2pIndex: market.p2pBorrowIndex, + amount: amount, + maxLoops: maxLoops, + borrow: true, + matching: true + }) + ); + } + + function _unmatchSuppliers(address poolToken, uint256 amount, uint256 maxLoops) + internal + returns (uint256 unmatched) + { + Types.Market storage market = _market[poolToken]; + (unmatched,) = _matchOrUnmatch( + _marketBalances[poolToken].suppliersPool, + _marketBalances[poolToken].suppliersP2P, + Types.MatchVars({ + poolToken: poolToken, + poolIndex: market.poolSupplyIndex, + p2pIndex: market.p2pSupplyIndex, + amount: amount, + maxLoops: maxLoops, + borrow: false, + matching: false + }) + ); + } + + function _unmatchBorrowers(address poolToken, uint256 amount, uint256 maxLoops) + internal + returns (uint256 unmatched) + { + Types.Market storage market = _market[poolToken]; + (unmatched,) = _matchOrUnmatch( + _marketBalances[poolToken].borrowersPool, + _marketBalances[poolToken].borrowersP2P, + Types.MatchVars({ + poolToken: poolToken, + poolIndex: market.poolBorrowIndex, + p2pIndex: market.p2pBorrowIndex, + amount: amount, + maxLoops: maxLoops, + borrow: true, + matching: false + }) + ); + } + + function _matchOrUnmatch( + ThreeHeapOrdering.HeapArray storage heapOnPool, + ThreeHeapOrdering.HeapArray storage heapInP2P, + Types.MatchVars memory vars + ) internal returns (uint256 matched, uint256 loopsDone) { + if (vars.maxLoops == 0) return (0, 0); + + uint256 remainingToMatch = vars.amount; + + // prettier-ignore + // This function will be used to decide whether to use the algorithm for matching or for unmatching. + function(uint256, uint256, uint256, uint256, uint256) + pure returns (uint256, uint256, uint256) f; + ThreeHeapOrdering.HeapArray storage workingHeap; + + if (vars.matching) { + workingHeap = heapOnPool; + f = _matchStep; + } else { + workingHeap = heapInP2P; + f = _unmatchStep; + } + + for (; loopsDone < vars.maxLoops; ++loopsDone) { + // Safe unchecked because `gasLeftAtTheBeginning` >= gas left now. + address firstUser = workingHeap.getHead(); + if (firstUser == address(0)) break; + + uint256 onPool; + uint256 inP2P; + + (onPool, inP2P, remainingToMatch) = f( + heapOnPool.getValueOf(firstUser), + heapInP2P.getValueOf(firstUser), + vars.poolIndex, + vars.p2pIndex, + remainingToMatch + ); + + if (!vars.borrow) { + _updateSupplierInDS(vars.poolToken, firstUser, onPool, inP2P); + } else { + _updateBorrowerInDS(vars.poolToken, firstUser, onPool, inP2P); + } + + emit Events.PositionUpdated(vars.borrow, firstUser, vars.poolToken, onPool, inP2P); + } + + // Safe unchecked because `gasLeftAtTheBeginning` >= gas left now. + // And _amount >= remainingToMatch. + unchecked { + matched = vars.amount - remainingToMatch; + } + } + + function _matchStep(uint256 poolBalance, uint256 p2pBalance, uint256 poolIndex, uint256 p2pIndex, uint256 remaining) + internal + pure + returns (uint256 newPoolBalance, uint256 newP2PBalance, uint256 newRemaining) + { + uint256 toProcess = Math.min(poolBalance.rayMul(poolIndex), remaining); + newRemaining = remaining - toProcess; + newPoolBalance = poolBalance - toProcess.rayDiv(poolIndex); + newP2PBalance = p2pBalance + toProcess.rayDiv(p2pIndex); + } + + function _unmatchStep( + uint256 poolBalance, + uint256 p2pBalance, + uint256 poolIndex, + uint256 p2pIndex, + uint256 remaining + ) internal pure returns (uint256 newPoolBalance, uint256 newP2PBalance, uint256 newRemaining) { + uint256 toProcess = Math.min(p2pBalance.rayMul(p2pIndex), remaining); + newRemaining = remaining - toProcess; + newPoolBalance = poolBalance + toProcess.rayDiv(poolIndex); + newP2PBalance = p2pBalance - toProcess.rayDiv(p2pIndex); + } +} diff --git a/src/Morpho.sol b/src/Morpho.sol index 33928e383..cf9ce53f1 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -1,20 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -import {ThreeHeapOrdering} from "morpho-data-structures/ThreeHeapOrdering.sol"; -import {Events} from "./libraries/Events.sol"; -import {Errors} from "./libraries/Errors.sol"; -import {Types} from "./libraries/Types.sol"; +import {Types, Events, Errors, MarketLib} from "./libraries/Libraries.sol"; -import {ERC1155Upgradeable} from "openzeppelin-contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; -import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {MorphoGettersAndSetters} from "./MorphoGettersAndSetters.sol"; -contract Morpho is ERC1155Upgradeable, OwnableUpgradeable { - using ThreeHeapOrdering for ThreeHeapOrdering.HeapArray; +// import {IERC1155} from "./interfaces/IERC1155.sol"; +// import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; - /// STORAGE /// - - mapping(address => Types.Market) internal markets; +// @note: To add: IERC1155, Ownable +contract Morpho is MorphoGettersAndSetters { + using MarketLib for Types.MarketBalances; + using MarketLib for Types.Market; /// EXTERNAL /// @@ -42,28 +39,4 @@ contract Morpho is ERC1155Upgradeable, OwnableUpgradeable { external returns (uint256 repaid, uint256 seized) {} - - /// PUBLIC /// - - function decodeId(uint256 _id) public pure returns (address underlying, Types.PositionType positionType) { - underlying = address(uint160(_id)); - positionType = Types.PositionType(_id & 0xf); - } - - /// ERC1155 /// - - function balanceOf(address _user, uint256 _id) public view virtual override returns (uint256) { - (address underlying, Types.PositionType positionType) = decodeId(_id); - Types.Market storage market = markets[underlying]; - - if (positionType == Types.PositionType.COLLATERAL) { - return market.collateralScaledBalance[_user]; - } else if (positionType == Types.PositionType.SUPPLY) { - return market.suppliersP2P.getValueOf(_user) + market.suppliersP2P.getValueOf(_user); // TODO: take into account indexes. - } else if (positionType == Types.PositionType.BORROW) { - return market.borrowersP2P.getValueOf(_user) + market.borrowersP2P.getValueOf(_user); // TODO: take into account indexes. - } else { - return 0; - } - } } diff --git a/src/MorphoGettersAndSetters.sol b/src/MorphoGettersAndSetters.sol new file mode 100644 index 000000000..752129ba5 --- /dev/null +++ b/src/MorphoGettersAndSetters.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types, Events, Errors, MarketLib} from "./libraries/Libraries.sol"; + +import {MorphoInternal} from "./MorphoInternal.sol"; + +abstract contract MorphoGettersAndSetters is MorphoInternal { + using MarketLib for Types.MarketBalances; + using MarketLib for Types.Market; + + /// STORAGE /// + + function market(address poolToken) external view returns (Types.Market memory) { + return _market[poolToken]; + } + + function scaledPoolSupplyBalance(address poolToken, address user) external view returns (uint256) { + return _marketBalances[poolToken].scaledPoolSupplyBalance(user); + } + + function scaledP2PSupplyBalance(address poolToken, address user) external view returns (uint256) { + return _marketBalances[poolToken].scaledP2PSupplyBalance(user); + } + + function scaledPoolBorrowBalance(address poolToken, address user) external view returns (uint256) { + return _marketBalances[poolToken].scaledPoolBorrowBalance(user); + } + + function scaledP2PBorrowBalance(address poolToken, address user) external view returns (uint256) { + return _marketBalances[poolToken].scaledP2PBorrowBalance(user); + } + + function scaledCollateralBalance(address poolToken, address user) external view returns (uint256) { + return _marketBalances[poolToken].scaledCollateralBalance(user); + } + + function userMarkets(address user) external view returns (Types.UserMarkets memory) { + return _userMarkets[user]; + } + + function maxSortedUsers() external view returns (uint256) { + return _maxSortedUsers; + } + + function isClaimRewardsPaused() external view returns (bool) { + return _isClaimRewardsPaused; + } + + /// UTILITY /// + + function decodeId(uint256 id) external pure returns (address poolToken, Types.PositionType positionType) { + return _decodeId(id); + } + + /// ERC1155 /// + + function balanceOf(address user, uint256 id) external view returns (uint256) { + (address poolToken, Types.PositionType positionType) = _decodeId(id); + Types.MarketBalances storage marketBalances = _marketBalances[poolToken]; + + if (positionType == Types.PositionType.COLLATERAL) { + return marketBalances.scaledCollateralBalance(user); + } else if (positionType == Types.PositionType.SUPPLY) { + return marketBalances.scaledP2PSupplyBalance(user) + marketBalances.scaledPoolSupplyBalance(user); // TODO: take into account indexes. + } else if (positionType == Types.PositionType.BORROW) { + return marketBalances.scaledP2PBorrowBalance(user) + marketBalances.scaledPoolBorrowBalance(user); // TODO: take into account indexes. + } else { + return 0; + } + } +} diff --git a/src/MorphoInternal.sol b/src/MorphoInternal.sol new file mode 100644 index 000000000..06c8a5abc --- /dev/null +++ b/src/MorphoInternal.sol @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {MorphoStorage} from "./MorphoStorage.sol"; + +import { + Types, + Events, + Errors, + MarketLib, + MarketMaskLib, + WadRayMath, + Math, + PercentageMath, + DataTypes, + ReserveConfiguration, + UserConfiguration, + ThreeHeapOrdering +} from "./libraries/Libraries.sol"; +import {IPriceOracleGetter, IVariableDebtToken, IAToken, IPriceOracleSentinel} from "./interfaces/Interfaces.sol"; + +abstract contract MorphoInternal is MorphoStorage { + using MarketLib for Types.Market; + using MarketLib for Types.MarketBalances; + using MarketMaskLib for Types.UserMarkets; + using WadRayMath for uint256; + using Math for uint256; + using PercentageMath for uint256; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using UserConfiguration for DataTypes.UserConfigurationMap; + using ThreeHeapOrdering for ThreeHeapOrdering.HeapArray; + + /// @notice Prevents to update a market not created yet. + /// @param _poolToken The address of the market to check. + modifier isMarketCreated(address _poolToken) { + if (!_market[_poolToken].isCreated()) revert Errors.MarketNotCreated(); + _; + } + + function _decodeId(uint256 _id) internal pure returns (address underlying, Types.PositionType positionType) { + underlying = address(uint160(_id)); + positionType = Types.PositionType(_id & 0xf); + } + + /// @dev Returns the supply balance of `_user` in the `_poolToken` market. + /// @dev Note: Computes the result with the stored indexes, which are not always the most up to date ones. + /// @param user The address of the user. + /// @param poolToken The market where to get the supply amount. + /// @return The supply balance of the user (in underlying). + function _getUserSupplyBalance(address poolToken, address user) internal view returns (uint256) { + Types.MarketBalances storage marketBalances = _marketBalances[poolToken]; + Types.Market storage market = _market[poolToken]; + return marketBalances.scaledP2PSupplyBalance(user).rayMul(market.p2pSupplyIndex) + + marketBalances.scaledPoolSupplyBalance(user).rayMul(market.poolSupplyIndex); + } + + /// @dev Returns the borrow balance of `_user` in the `_poolToken` market. + /// @dev Note: Computes the result with the stored indexes, which are not always the most up to date ones. + /// @param user The address of the user. + /// @param poolToken The market where to get the borrow amount. + /// @return The borrow balance of the user (in underlying). + function _getUserBorrowBalance(address poolToken, address user) internal view returns (uint256) { + Types.MarketBalances storage marketBalances = _marketBalances[poolToken]; + Types.Market storage market = _market[poolToken]; + return marketBalances.scaledP2PBorrowBalance(user).rayMul(market.p2pBorrowIndex) + + marketBalances.scaledPoolBorrowBalance(user).rayMul(market.poolBorrowIndex); + } + + /// @dev Calculates the value of the collateral. + /// @param poolToken The pool token to calculate the value for. + /// @param user The user address. + /// @param underlyingPrice The underlying price. + /// @param tokenUnit The token unit. + function _collateralValue(address poolToken, address user, uint256 underlyingPrice, uint256 tokenUnit) + internal + view + returns (uint256) + { + return (_marketBalances[poolToken].scaledCollateralBalance(user) * underlyingPrice) / tokenUnit; // TODO: Multiply by an index or make collateral balance unscaled + } + + /// @dev Calculates the value of the debt. + /// @param poolToken The pool token to calculate the value for. + /// @param user The user address. + /// @param underlyingPrice The underlying price. + /// @param tokenUnit The token unit. + function _debtValue(address poolToken, address user, uint256 underlyingPrice, uint256 tokenUnit) + internal + view + returns (uint256) + { + return (_getUserBorrowBalance(poolToken, user) * underlyingPrice).divUp(tokenUnit); + } + + /// @dev Calculates the total value of the collateral, debt, and LTV/LT value depending on the calculation type. + /// @param user The user address. + /// @param poolToken The pool token that is being borrowed or withdrawn. + /// @param amountWithdrawn The amount that is being withdrawn. + /// @param amountBorrowed The amount that is being borrowed. + /// @return liquidityData The struct containing health factor, collateral, debt, ltv, liquidation threshold values. + function _liquidityData(address user, address poolToken, uint256 amountWithdrawn, uint256 amountBorrowed) + internal + view + returns (Types.LiquidityData memory liquidityData) + { + IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle()); + Types.UserMarkets memory userMarkets = _userMarkets[user]; + DataTypes.UserConfigurationMap memory morphoPoolConfig = _pool.getUserConfiguration(address(this)); + + uint256 poolTokensLength = _marketsCreated.length; + + for (uint256 i; i < poolTokensLength; ++i) { + address currentMarket = _marketsCreated[i]; + + if (userMarkets.isSupplyingOrBorrowing(_market[currentMarket].borrowMask)) { + uint256 withdrawnSingle; + uint256 borrowedSingle; + + if (poolToken == currentMarket) { + withdrawnSingle = amountWithdrawn; + borrowedSingle = amountBorrowed; + } + // else { + // _updateIndexes(currentMarket); + // } + + Types.AssetLiquidityData memory assetLiquidityData = + _assetLiquidityData(_market[currentMarket].underlying, oracle, morphoPoolConfig); + Types.LiquidityData memory liquidityDataSingle = _liquidityDataSingle( + currentMarket, user, userMarkets, assetLiquidityData, withdrawnSingle, borrowedSingle + ); + liquidityData.collateral += liquidityDataSingle.collateral; + liquidityData.maxDebt += liquidityDataSingle.maxDebt; + liquidityData.liquidationThresholdValue += liquidityDataSingle.liquidationThresholdValue; + liquidityData.debt += liquidityDataSingle.debt; + } + } + } + + function _liquidityDataSingle( + address poolToken, + address user, + Types.UserMarkets memory userMarkets, + Types.AssetLiquidityData memory assetLiquidityData, + uint256 amountBorrowed, + uint256 amountWithdrawn + ) internal view returns (Types.LiquidityData memory liquidityData) { + Types.Market storage market = _market[poolToken]; + + if (userMarkets.isBorrowing(market.borrowMask)) { + liquidityData.debt += + _debtValue(poolToken, user, assetLiquidityData.underlyingPrice, assetLiquidityData.tokenUnit); + } + + // Cache current asset collateral value. + uint256 assetCollateralValue; + if (userMarkets.isSupplying(market.borrowMask)) { + assetCollateralValue = + _collateralValue(poolToken, user, assetLiquidityData.underlyingPrice, assetLiquidityData.tokenUnit); + liquidityData.collateral += assetCollateralValue; + // Calculate LTV for borrow. + liquidityData.maxDebt += assetCollateralValue.percentMul(assetLiquidityData.ltv); + } + + // Update debt variable for borrowed token. + if (amountBorrowed > 0) { + liquidityData.debt += + (amountBorrowed * assetLiquidityData.underlyingPrice).divUp(assetLiquidityData.tokenUnit); + } + + // Update LT variable for withdraw. + if (assetCollateralValue > 0) { + liquidityData.liquidationThresholdValue += + assetCollateralValue.percentMul(assetLiquidityData.liquidationThreshold); + } + + // Subtract withdrawn amount from liquidation threshold and collateral. + if (amountWithdrawn > 0) { + uint256 withdrawn = (amountWithdrawn * assetLiquidityData.underlyingPrice) / assetLiquidityData.tokenUnit; + liquidityData.collateral -= withdrawn; + liquidityData.liquidationThresholdValue -= withdrawn.percentMul(assetLiquidityData.liquidationThreshold); + liquidityData.maxDebt -= withdrawn.percentMul(assetLiquidityData.ltv); + } + } + + function _assetLiquidityData( + address underlying, + IPriceOracleGetter oracle, + DataTypes.UserConfigurationMap memory morphoPoolConfig + ) internal view returns (Types.AssetLiquidityData memory assetLiquidityData) { + assetLiquidityData.underlyingPrice = oracle.getAssetPrice(underlying); + + (assetLiquidityData.ltv, assetLiquidityData.liquidationThreshold,, assetLiquidityData.decimals,,) = + _pool.getConfiguration(underlying).getParams(); + + // LTV should be zero if Morpho has not enabled this asset as collateral + if (!morphoPoolConfig.isUsingAsCollateral(_pool.getReserveData(underlying).id)) { + assetLiquidityData.ltv = 0; + } + + // If a LTV has been reduced to 0 on Aave v3, the other assets of the collateral are frozen. + // In response, Morpho disables the asset as collateral and sets its liquidation threshold to 0. + if (assetLiquidityData.ltv == 0) { + assetLiquidityData.liquidationThreshold = 0; + } + + unchecked { + assetLiquidityData.tokenUnit = 10 ** assetLiquidityData.decimals; + } + } + + function _updateInDS( + address, // Note: token unused for now until more functionality added in + address user, + ThreeHeapOrdering.HeapArray storage marketOnPool, + ThreeHeapOrdering.HeapArray storage marketInP2P, + uint256 onPool, + uint256 inP2P + ) internal { + uint256 formerOnPool = marketOnPool.getValueOf(user); + + if (onPool != formerOnPool) { + // if (address(rewardsManager) != address(0)) + // rewardsManager.updateUserAssetAndAccruedRewards( + // rewardsController, + // user, + // token, + // formerOnPool, + // IScaledBalanceToken(token).scaledTotalSupply() + // ); + marketOnPool.update(user, formerOnPool, onPool, _maxSortedUsers); + } + marketInP2P.update(user, marketInP2P.getValueOf(user), inP2P, _maxSortedUsers); + } + + function _updateSupplierInDS(address poolToken, address user, uint256 onPool, uint256 inP2P) internal { + _updateInDS( + poolToken, + user, + _marketBalances[poolToken].suppliersPool, + _marketBalances[poolToken].suppliersP2P, + onPool, + inP2P + ); + } + + function _updateBorrowerInDS(address poolToken, address user, uint256 onPool, uint256 inP2P) internal { + _updateInDS( + _market[poolToken].variableDebtToken, + user, + _marketBalances[poolToken].borrowersPool, + _marketBalances[poolToken].borrowersP2P, + onPool, + inP2P + ); + } + + function _setPauseStatus(address poolToken, bool isPaused) internal { + Types.PauseStatuses storage pauseStatuses = _market[poolToken].pauseStatuses; + + pauseStatuses.isSupplyPaused = isPaused; + pauseStatuses.isBorrowPaused = isPaused; + pauseStatuses.isWithdrawPaused = isPaused; + pauseStatuses.isRepayPaused = isPaused; + pauseStatuses.isLiquidateCollateralPaused = isPaused; + pauseStatuses.isLiquidateBorrowPaused = isPaused; + + emit Events.IsSupplyPausedSet(poolToken, isPaused); + emit Events.IsBorrowPausedSet(poolToken, isPaused); + emit Events.IsWithdrawPausedSet(poolToken, isPaused); + emit Events.IsRepayPausedSet(poolToken, isPaused); + emit Events.IsLiquidateCollateralPausedSet(poolToken, isPaused); + emit Events.IsLiquidateBorrowPausedSet(poolToken, isPaused); + } + + /// @notice allows computing indexes in the future + function _computeIndexes(address poolToken) + internal + view + returns ( + uint256 newPoolSupplyIndex, + uint256 newPoolBorrowIndex, + uint256 newP2PSupplyIndex, + uint256 newP2PBorrowIndex + ) + { + Types.Market storage market = _market[poolToken]; + if (block.timestamp == market.lastUpdateTimestamp) { + return ( + uint256(market.poolSupplyIndex), + uint256(market.poolBorrowIndex), + market.p2pSupplyIndex, + market.p2pBorrowIndex + ); + } + + address underlying = market.underlying; + newPoolSupplyIndex = _pool.getReserveNormalizedIncome(underlying); + newPoolBorrowIndex = _pool.getReserveNormalizedVariableDebt(underlying); + + Types.IRMParams memory params = Types.IRMParams( + market.p2pSupplyIndex, + market.p2pBorrowIndex, + newPoolSupplyIndex, + newPoolBorrowIndex, + market.poolSupplyIndex, + market.poolBorrowIndex, + market.reserveFactor, + market.p2pIndexCursor, + market.deltas + ); + + (newP2PSupplyIndex, newP2PBorrowIndex) = _computeP2PIndexes(params); + } + + function _computeP2PIndexes(Types.IRMParams memory params) + internal + pure + returns (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) + { + // Compute pool growth factors + + uint256 poolSupplyGrowthFactor = params.poolSupplyIndex.rayDiv(params.lastPoolSupplyIndex); + uint256 poolBorrowGrowthFactor = params.poolBorrowIndex.rayDiv(params.lastPoolBorrowIndex); + + // Compute peer-to-peer growth factors. + + uint256 p2pSupplyGrowthFactor; + uint256 p2pBorrowGrowthFactor; + if (poolSupplyGrowthFactor <= poolBorrowGrowthFactor) { + uint256 p2pGrowthFactor = + PercentageMath.weightedAvg(poolSupplyGrowthFactor, poolBorrowGrowthFactor, params.p2pIndexCursor); + + p2pSupplyGrowthFactor = + p2pGrowthFactor - (p2pGrowthFactor - poolSupplyGrowthFactor).percentMul(params.reserveFactor); + p2pBorrowGrowthFactor = + p2pGrowthFactor + (poolBorrowGrowthFactor - p2pGrowthFactor).percentMul(params.reserveFactor); + } else { + // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone has done a flashloan on Aave, or because the interests + // generated by the stable rate borrowing are high (making the supply rate higher than the variable borrow rate). In this case the peer-to-peer + // growth factors are set to the pool borrow growth factor. + p2pSupplyGrowthFactor = poolBorrowGrowthFactor; + p2pBorrowGrowthFactor = poolBorrowGrowthFactor; + } + + // Compute new peer-to-peer supply index. + + if (params.delta.p2pSupplyAmount == 0 || params.delta.p2pSupplyDelta == 0) { + newP2PSupplyIndex = params.lastP2PSupplyIndex.rayMul(p2pSupplyGrowthFactor); + } else { + uint256 shareOfTheDelta = Math.min( + (params.delta.p2pSupplyDelta.rayMul(params.lastPoolSupplyIndex)).rayDiv( + params.delta.p2pSupplyAmount.rayMul(params.lastP2PSupplyIndex) + ), // Using ray division of an amount in underlying decimals by an amount in underlying decimals yields a value in ray. + WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. + ); // In ray. + + newP2PSupplyIndex = params.lastP2PSupplyIndex.rayMul( + (WadRayMath.RAY - shareOfTheDelta).rayMul(p2pSupplyGrowthFactor) + + shareOfTheDelta.rayMul(poolSupplyGrowthFactor) + ); + } + + // Compute new peer-to-peer borrow index. + + if (params.delta.p2pBorrowAmount == 0 || params.delta.p2pBorrowDelta == 0) { + newP2PBorrowIndex = params.lastP2PBorrowIndex.rayMul(p2pBorrowGrowthFactor); + } else { + uint256 shareOfTheDelta = Math.min( + (params.delta.p2pBorrowDelta.rayMul(params.lastPoolBorrowIndex)).rayDiv( + params.delta.p2pBorrowAmount.rayMul(params.lastP2PBorrowIndex) + ), // Using ray division of an amount in underlying decimals by an amount in underlying decimals yields a value in ray. + WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. + ); // In ray. + + newP2PBorrowIndex = params.lastP2PBorrowIndex.rayMul( + (WadRayMath.RAY - shareOfTheDelta).rayMul(p2pBorrowGrowthFactor) + + shareOfTheDelta.rayMul(poolBorrowGrowthFactor) + ); + } + } + + function _borrowAllowed(address user, address poolToken, uint256 borrowedAmount) internal view returns (bool) { + // Aave can enable an oracle sentinel in specific circumstances which can prevent users to borrow. + // In response, Morpho mirrors this behavior. + address priceOracleSentinel = _addressesProvider.getPriceOracleSentinel(); + if (priceOracleSentinel != address(0) && !IPriceOracleSentinel(priceOracleSentinel).isBorrowAllowed()) { + return false; + } + + Types.LiquidityData memory values = _liquidityData(user, poolToken, 0, borrowedAmount); + return values.debt <= values.maxDebt; + } + + function _getUserHealthFactor(address user, address poolToken, uint256 withdrawnAmount) + internal + view + returns (uint256) + { + // If the user is not borrowing any asset, return an infinite health factor. + if (!_userMarkets[user].isBorrowingAny()) return type(uint256).max; + + Types.LiquidityData memory liquidityData = _liquidityData(user, poolToken, withdrawnAmount, 0); + + return liquidityData.debt > 0 + ? liquidityData.liquidationThresholdValue.wadDiv(liquidityData.debt) + : type(uint256).max; + } + + function _withdrawAllowed(address user, address poolToken, uint256 withdrawnAmount) internal view returns (bool) { + // Aave can enable an oracle sentinel in specific circumstances which can prevent users to borrow. + // For safety concerns and as a withdraw on Morpho can trigger a borrow on pool, Morpho prevents withdrawals in such circumstances. + address priceOracleSentinel = _addressesProvider.getPriceOracleSentinel(); + if (priceOracleSentinel != address(0) && !IPriceOracleSentinel(priceOracleSentinel).isBorrowAllowed()) { + return false; + } + + return _getUserHealthFactor(user, poolToken, withdrawnAmount) >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + } + + function _liquidationAllowed(address user, bool isDeprecated) + internal + view + returns (bool liquidationAllowed, uint256 closeFactor) + { + if (isDeprecated) { + liquidationAllowed = true; + closeFactor = MAX_BASIS_POINTS; // Allow liquidation of the whole debt. + } else { + uint256 healthFactor = _getUserHealthFactor(user, address(0), 0); + address priceOracleSentinel = _addressesProvider.getPriceOracleSentinel(); + + if (priceOracleSentinel != address(0)) { + liquidationAllowed = ( + healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD + || ( + IPriceOracleSentinel(priceOracleSentinel).isLiquidationAllowed() + && healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) + ); + } else { + liquidationAllowed = healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + } + + if (liquidationAllowed) { + closeFactor = healthFactor > MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ? DEFAULT_LIQUIDATION_CLOSE_FACTOR + : MAX_LIQUIDATION_CLOSE_FACTOR; + } + } + } +} diff --git a/src/MorphoStorage.sol b/src/MorphoStorage.sol new file mode 100644 index 000000000..ce016465c --- /dev/null +++ b/src/MorphoStorage.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types} from "./libraries/Types.sol"; +import {Constants} from "./libraries/Constants.sol"; +import {IPool, IPoolAddressesProvider} from "./interfaces/Interfaces.sol"; + +contract MorphoStorage { + /// CONSTANTS /// + + uint256 internal constant MAX_BASIS_POINTS = Constants.MAX_BASIS_POINTS; + uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = Constants.DEFAULT_LIQUIDATION_CLOSE_FACTOR; + uint256 internal constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = Constants.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; // Health factor below which the positions can be liquidated. + uint256 internal constant MAX_NB_OF_MARKETS = Constants.MAX_NB_OF_MARKETS; + uint256 internal constant MAX_LIQUIDATION_CLOSE_FACTOR = Constants.MAX_LIQUIDATION_CLOSE_FACTOR; // 100% in basis points. + uint256 internal constant MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD = + Constants.MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD; // Health factor below which the positions can be liquidated, whether or not the price oracle sentinel allows the liquidation. + bytes32 internal constant BORROWING_MASK = Constants.BORROWING_MASK; + bytes32 internal constant ONE = Constants.ONE; + + /// STORAGE /// + + address[] internal _marketsCreated; // Keeps track of the created markets. + mapping(address => Types.Market) internal _market; + mapping(address => Types.MarketBalances) internal _marketBalances; + mapping(address => Types.UserMarkets) internal _userMarkets; // The markets entered by a user as a bitmask. + + uint256 internal _maxSortedUsers; // The max number of users to sort in the data structure. + + IPoolAddressesProvider internal _addressesProvider; + IPool internal _pool; + // IEntryPositionsManager internal _entryPositionsManager; + // IExitPositionsManager internal _exitPositionsManager; + // IInterestRatesManager internal _interestRatesManager; + // IRewardsController internal _rewardsController; + // IIncentivesVault internal _incentivesVault; + // IRewardsManager internal _rewardsManager; + + address internal _treasuryVault; + bool internal _isClaimRewardsPaused; // Whether claiming rewards is paused or not. +} diff --git a/src/interfaces/IERC1155.sol b/src/interfaces/IERC1155.sol new file mode 100644 index 000000000..e2c85c115 --- /dev/null +++ b/src/interfaces/IERC1155.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +// Derived from Openzeppelin's 4.7.0 contracts but with IERC165 removed. + +pragma solidity ^0.8.0; + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch( + address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values + ); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/src/interfaces/Interfaces.sol b/src/interfaces/Interfaces.sol new file mode 100644 index 000000000..0acf0bf06 --- /dev/null +++ b/src/interfaces/Interfaces.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {IERC1155} from "./IERC1155.sol"; +import {IRewardsController} from "@aave/periphery-v3/contracts/rewards/interfaces/IRewardsController.sol"; +import {IPriceOracleGetter} from "@aave/core-v3/contracts/interfaces/IPriceOracleGetter.sol"; +import {IPriceOracleSentinel} from "@aave/core-v3/contracts/interfaces/IPriceOracleSentinel.sol"; + +// These cannot be imported from aave's repo because they depend on solidity v0.8.10. +import {IAToken} from "./aave/IAToken.sol"; +import {IVariableDebtToken} from "./aave/IVariableDebtToken.sol"; +import {IPoolAddressesProvider, IPool} from "./aave/IPool.sol"; diff --git a/src/interfaces/aave/IAToken.sol b/src/interfaces/aave/IAToken.sol new file mode 100644 index 000000000..669c642e8 --- /dev/null +++ b/src/interfaces/aave/IAToken.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.5.0; + +interface IAToken { + function balanceOf(address) external view returns (uint256); +} diff --git a/src/interfaces/aave/IPool.sol b/src/interfaces/aave/IPool.sol new file mode 100644 index 000000000..12e2436e6 --- /dev/null +++ b/src/interfaces/aave/IPool.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.5.0; + +import {IPoolAddressesProvider} from "./IPoolAddressesProvider.sol"; +import {DataTypes} from "../../libraries/aave/DataTypes.sol"; + +interface IPool { + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; + + function withdraw(address asset, uint256 amount, address to) external returns (uint256); + + function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) + external; + + function repay(address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf) + external + returns (uint256); + + function repayWithPermit( + address asset, + uint256 amount, + uint256 interestRateMode, + address onBehalfOf, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external returns (uint256); + + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; + + function getUserAccountData(address user) + external + view + returns ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + function getConfiguration(address asset) external view returns (DataTypes.ReserveConfigurationMap memory); + + function getUserConfiguration(address user) external view returns (DataTypes.UserConfigurationMap memory); + + function getReserveNormalizedIncome(address asset) external view returns (uint256); + + function getReserveNormalizedVariableDebt(address asset) external view returns (uint256); + + function getReserveData(address asset) external view returns (DataTypes.ReserveData memory); + + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); +} diff --git a/src/interfaces/aave/IPoolAddressesProvider.sol b/src/interfaces/aave/IPoolAddressesProvider.sol new file mode 100644 index 000000000..247d2e959 --- /dev/null +++ b/src/interfaces/aave/IPoolAddressesProvider.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.5.0; + +interface IPoolAddressesProvider { + function getPool() external view returns (address); + + function getPriceOracle() external view returns (address); + + function getPriceOracleSentinel() external view returns (address); +} diff --git a/src/interfaces/aave/IVariableDebtToken.sol b/src/interfaces/aave/IVariableDebtToken.sol new file mode 100644 index 000000000..d1bb266c3 --- /dev/null +++ b/src/interfaces/aave/IVariableDebtToken.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.5.0; + +import {DataTypes} from "../../libraries/aave/DataTypes.sol"; + +interface IVariableDebtToken { + function scaledBalanceOf(address) external view returns (uint256); +} diff --git a/src/libraries/Constants.sol b/src/libraries/Constants.sol new file mode 100644 index 000000000..28bc498f8 --- /dev/null +++ b/src/libraries/Constants.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +library Constants { + uint8 internal constant NO_REFERRAL_CODE = 0; + uint8 internal constant VARIABLE_INTEREST_MODE = 2; + uint256 internal constant MAX_BASIS_POINTS = 10_000; // 100% in basis points. + uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 5_000; // 50% in basis points. + uint256 internal constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; // Health factor below which the positions can be liquidated. + uint256 internal constant MAX_NB_OF_MARKETS = 128; + uint256 internal constant MAX_LIQUIDATION_CLOSE_FACTOR = 10_000; // 100% in basis points. + uint256 internal constant MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 0.95e18; // Health factor below which the positions can be liquidated, whether or not the price oracle sentinel allows the liquidation. + bytes32 internal constant BORROWING_MASK = 0x5555555555555555555555555555555555555555555555555555555555555555; + bytes32 internal constant ONE = 0x0000000000000000000000000000000000000000000000000000000000000001; +} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index e545baf1e..ce1f5c6c5 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -library Errors {} +library Errors { + error MarketNotCreated(); +} diff --git a/src/libraries/Events.sol b/src/libraries/Events.sol index f899ed2bf..90201f62c 100644 --- a/src/libraries/Events.sol +++ b/src/libraries/Events.sol @@ -1,4 +1,20 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; +pragma solidity ^0.8.0; -library Events {} +library Events { + event PositionUpdated( + bool borrow, address indexed user, address indexed poolToken, uint256 balanceOnPool, uint256 balanceInP2P + ); + + event IsSupplyPausedSet(address indexed poolToken, bool isPaused); + + event IsBorrowPausedSet(address indexed poolToken, bool isPaused); + + event IsWithdrawPausedSet(address indexed poolToken, bool isPaused); + + event IsRepayPausedSet(address indexed poolToken, bool isPaused); + + event IsLiquidateCollateralPausedSet(address indexed poolToken, bool isPaused); + + event IsLiquidateBorrowPausedSet(address indexed poolToken, bool isPaused); +} diff --git a/src/libraries/Libraries.sol b/src/libraries/Libraries.sol new file mode 100644 index 000000000..54815878f --- /dev/null +++ b/src/libraries/Libraries.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types} from "./Types.sol"; +import {Events} from "./Events.sol"; +import {Errors} from "./Errors.sol"; +import {MarketLib} from "./MarketLib.sol"; +import {MarketMaskLib} from "./MarketMaskLib.sol"; +import {PoolInteractions} from "./PoolInteractions.sol"; + +import {WadRayMath} from "morpho-utils/math/WadRayMath.sol"; +import {Math} from "morpho-utils/math/Math.sol"; +import {PercentageMath} from "morpho-utils/math/PercentageMath.sol"; + +import {ThreeHeapOrdering} from "morpho-data-structures/ThreeHeapOrdering.sol"; + +import {DataTypes} from "./aave/DataTypes.sol"; +import {ReserveConfiguration} from "./aave/ReserveConfiguration.sol"; +import {UserConfiguration} from "./aave/UserConfiguration.sol"; diff --git a/src/libraries/MarketLib.sol b/src/libraries/MarketLib.sol new file mode 100644 index 000000000..aa253cd81 --- /dev/null +++ b/src/libraries/MarketLib.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Types} from "./Types.sol"; +import {ThreeHeapOrdering} from "morpho-data-structures/ThreeHeapOrdering.sol"; + +library MarketLib { + using ThreeHeapOrdering for ThreeHeapOrdering.HeapArray; + + // MARKET + + function isCreated(Types.Market storage market) internal view returns (bool) { + return market.underlying != address(0); + } + + function isCreatedMem(Types.Market memory market) internal pure returns (bool) { + return market.underlying != address(0); + } + + // MARKET BALANCES + + function scaledP2PSupplyBalance(Types.MarketBalances storage marketBalances, address user) + internal + view + returns (uint256) + { + return marketBalances.suppliersP2P.getValueOf(user); + } + + function scaledPoolSupplyBalance(Types.MarketBalances storage marketBalances, address user) + internal + view + returns (uint256) + { + return marketBalances.suppliersPool.getValueOf(user); + } + + function scaledP2PBorrowBalance(Types.MarketBalances storage marketBalances, address user) + internal + view + returns (uint256) + { + return marketBalances.borrowersP2P.getValueOf(user); + } + + function scaledPoolBorrowBalance(Types.MarketBalances storage marketBalances, address user) + internal + view + returns (uint256) + { + return marketBalances.borrowersPool.getValueOf(user); + } + + function scaledCollateralBalance(Types.MarketBalances storage marketBalances, address user) + internal + view + returns (uint256) + { + return marketBalances.collateral[user]; + } +} diff --git a/src/libraries/MarketMaskLib.sol b/src/libraries/MarketMaskLib.sol new file mode 100644 index 000000000..8b9873b61 --- /dev/null +++ b/src/libraries/MarketMaskLib.sol @@ -0,0 +1,93 @@ +pragma solidity ^0.8.17; + +import {Types} from "./Types.sol"; +import {Constants} from "./Constants.sol"; + +library MarketMaskLib { + bytes32 internal constant BORROWING_MASK = Constants.BORROWING_MASK; + + /// @dev Returns if a user has been borrowing or supplying on a given market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowMask The borrow mask of the market to check. + /// @return True if the user has been supplying or borrowing on this market, false otherwise. + function isSupplyingOrBorrowing(Types.UserMarkets memory userMarkets, Types.BorrowMask memory borrowMask) + internal + pure + returns (bool) + { + return userMarkets.data & (borrowMask.data | (borrowMask.data << 1)) != 0; + } + + /// @dev Returns if a user is borrowing on a given market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowMask The borrow mask of the market to check. + /// @return True if the user has been borrowing on this market, false otherwise. + function isBorrowing(Types.UserMarkets memory userMarkets, Types.BorrowMask memory borrowMask) + internal + pure + returns (bool) + { + return userMarkets.data & borrowMask.data != 0; + } + + /// @dev Returns if a user is supplying on a given market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowMask The borrow mask of the market to check. + /// @return True if the user has been supplying on this market, false otherwise. + function isSupplying(Types.UserMarkets memory userMarkets, Types.BorrowMask memory borrowMask) + internal + pure + returns (bool) + { + return userMarkets.data & (borrowMask.data << 1) != 0; + } + + /// @dev Returns if a user has been borrowing from any market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @return True if the user has been borrowing on any market, false otherwise. + function isBorrowingAny(Types.UserMarkets memory userMarkets) internal pure returns (bool) { + return userMarkets.data & BORROWING_MASK != 0; + } + + /// @dev Returns if a user is borrowing on a given market and supplying on another given market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowedBorrowMask The borrow mask of the market to check whether the user is borrowing. + /// @param suppliedBorrowMask The borrow mask of the market to check whether the user is supplying. + /// @return True if the user is borrowing on the given market and supplying on the other given market, false otherwise. + function isBorrowingAndSupplying( + Types.UserMarkets memory userMarkets, + Types.BorrowMask memory borrowedBorrowMask, + Types.BorrowMask memory suppliedBorrowMask + ) internal pure returns (bool) { + Types.BorrowMask memory combinedBorrowMask; + combinedBorrowMask.data = borrowedBorrowMask.data | (suppliedBorrowMask.data << 1); + return userMarkets.data & combinedBorrowMask.data == combinedBorrowMask.data; + } + + /// @notice Sets if the user is borrowing on a market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowMask The borrow mask of the market to mark as borrowed. + /// @param borrowing True if the user is borrowing, false otherwise. + /// @return newUserMarket The new user bitmask. + function setBorrowing(Types.UserMarkets memory userMarkets, Types.BorrowMask memory borrowMask, bool borrowing) + internal + pure + returns (Types.UserMarkets memory newUserMarket) + { + newUserMarket.data = borrowing ? userMarkets.data | borrowMask.data : userMarkets.data & (~borrowMask.data); + } + + /// @notice Sets if the user is supplying on a market. + /// @param userMarkets The bitmask encoding the markets entered by the user. + /// @param borrowMask The borrow mask of the market to mark as supplied. + /// @param supplying True if the user is supplying, false otherwise. + /// @return newUserMarket The new user bitmask. + function setSupplying(Types.UserMarkets memory userMarkets, Types.BorrowMask memory borrowMask, bool supplying) + internal + pure + returns (Types.UserMarkets memory newUserMarket) + { + newUserMarket.data = + supplying ? userMarkets.data | (borrowMask.data << 1) : userMarkets.data & (~(borrowMask.data << 1)); + } +} diff --git a/src/libraries/PoolInteractions.sol b/src/libraries/PoolInteractions.sol new file mode 100644 index 000000000..7f4d2f607 --- /dev/null +++ b/src/libraries/PoolInteractions.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {IPool} from "../interfaces/aave/IPool.sol"; +import {IVariableDebtToken} from "../interfaces/aave/IVariableDebtToken.sol"; +import {IAToken} from "../interfaces/aave/IAToken.sol"; +import {Constants} from "./Constants.sol"; +import {Math} from "morpho-utils/math/Math.sol"; + +library PoolInteractions { + function supplyToPool(IPool pool, address underlying, uint256 amount) internal { + pool.supply(underlying, amount, address(this), Constants.NO_REFERRAL_CODE); + } + + function withdrawFromPool(IPool pool, address underlying, address poolToken, uint256 amount) internal { + // Withdraw only what is possible. The remaining dust is taken from the contract balance. + amount = Math.min(IAToken(poolToken).balanceOf(address(this)), amount); + pool.withdraw(underlying, amount, address(this)); + } + + function borrowFromPool(IPool pool, address underlying, uint256 amount) internal { + pool.borrow(underlying, amount, Constants.VARIABLE_INTEREST_MODE, Constants.NO_REFERRAL_CODE, address(this)); + } + + function repayToPool(IPool pool, address underlying, uint256 amount) internal { + if ( + amount == 0 + || IVariableDebtToken(pool.getReserveData(underlying).variableDebtTokenAddress).scaledBalanceOf( + address(this) + ) == 0 + ) return; + + pool.repay(underlying, amount, Constants.VARIABLE_INTEREST_MODE, address(this)); // Reverts if debt is 0. + } +} diff --git a/src/libraries/Types.sol b/src/libraries/Types.sol index ae9da6bfd..879fc937e 100644 --- a/src/libraries/Types.sol +++ b/src/libraries/Types.sol @@ -12,13 +12,101 @@ library Types { BORROW } - /// STRUCTS /// + /// NESTED STRUCTS /// + struct Delta { + uint256 p2pSupplyDelta; // Difference between the stored peer-to-peer supply amount and the real peer-to-peer supply amount (in pool supply unit). + uint256 p2pBorrowDelta; // Difference between the stored peer-to-peer borrow amount and the real peer-to-peer borrow amount (in pool borrow unit). + uint256 p2pSupplyAmount; // Sum of all stored peer-to-peer supply (in peer-to-peer supply unit). + uint256 p2pBorrowAmount; // Sum of all stored peer-to-peer borrow (in peer-to-peer borrow unit). + } + + struct PoolIndexes { + uint32 lastUpdateTimestamp; // The last time the local pool and peer-to-peer indexes were updated. + uint112 poolSupplyIndex; // Last pool supply index (in ray). + uint112 poolBorrowIndex; // Last pool borrow index (in ray). + } + + struct PauseStatuses { + bool isP2PDisabled; + bool isSupplyPaused; + bool isBorrowPaused; + bool isWithdrawPaused; + bool isRepayPaused; + bool isLiquidateCollateralPaused; + bool isLiquidateBorrowPaused; + bool isDeprecated; + } + + /// STORAGE STRUCTS /// + + // This market struct is able to be passed into memory. struct Market { - ThreeHeapOrdering.HeapArray suppliersP2P; - ThreeHeapOrdering.HeapArray suppliersPool; - ThreeHeapOrdering.HeapArray borrowersP2P; - ThreeHeapOrdering.HeapArray borrowersPool; - mapping(address => uint256) collateralScaledBalance; + uint256 p2pSupplyIndex; // 256 bits + uint256 p2pBorrowIndex; // 256 bits + BorrowMask borrowMask; // 256 bits + Delta deltas; // 1024 bits + uint32 lastUpdateTimestamp; // 32 bits + uint112 poolSupplyIndex; // 112 bits + uint112 poolBorrowIndex; // 112 bits + address underlying; // 168 bits + address variableDebtToken; // 168 bits + uint16 reserveFactor; // 16 bits + uint16 p2pIndexCursor; // 16 bits + PauseStatuses pauseStatuses; // 64 bits + } + + // Contains storage-only dynamic arrays and mappings. + struct MarketBalances { + ThreeHeapOrdering.HeapArray suppliersP2P; // in scaled unit + ThreeHeapOrdering.HeapArray suppliersPool; // in scaled unit + ThreeHeapOrdering.HeapArray borrowersP2P; // in scaled unit + ThreeHeapOrdering.HeapArray borrowersPool; // in scaled unit + mapping(address => uint256) collateral; // in scaled unit + } + + struct UserMarkets { + bytes32 data; + } + + struct BorrowMask { + bytes32 data; + } + + struct AssetLiquidityData { + uint256 decimals; // The number of decimals of the underlying token. + uint256 tokenUnit; // The token unit considering its decimals. + uint256 liquidationThreshold; // The liquidation threshold applied on this token (in basis point). + uint256 ltv; // The LTV applied on this token (in basis point). + uint256 underlyingPrice; // The price of the token (In base currency in wad). + } + + struct LiquidityData { + uint256 collateral; // The collateral value (In base currency in wad). + uint256 maxDebt; // The max debt value (In base currency in wad). + uint256 liquidationThresholdValue; // The liquidation threshold value (In base currency in wad). + uint256 debt; // The debt value (In base currency in wad). + } + + struct MatchVars { + address poolToken; + uint256 poolIndex; + uint256 p2pIndex; + uint256 amount; + uint256 maxLoops; + bool borrow; + bool matching; // True for match, False for unmatch + } + + struct IRMParams { + uint256 lastP2PSupplyIndex; // The peer-to-peer supply index at last update. + uint256 lastP2PBorrowIndex; // The peer-to-peer borrow index at last update. + uint256 poolSupplyIndex; // The current pool supply index. + uint256 poolBorrowIndex; // The current pool borrow index. + uint256 lastPoolSupplyIndex; // The pool supply index at last update. + uint256 lastPoolBorrowIndex; // The pool borrow index at last update. + uint256 reserveFactor; // The reserve factor percentage (10 000 = 100%). + uint256 p2pIndexCursor; // The peer-to-peer index cursor (10 000 = 100%). + Types.Delta delta; // The deltas and peer-to-peer amounts. } } diff --git a/src/libraries/aave/DataTypes.sol b/src/libraries/aave/DataTypes.sol new file mode 100644 index 000000000..8fd518090 --- /dev/null +++ b/src/libraries/aave/DataTypes.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity ^0.8.0; + +library DataTypes { + struct ReserveData { + ReserveConfigurationMap configuration; + uint128 liquidityIndex; + uint128 currentLiquidityRate; + uint128 variableBorrowIndex; + uint128 currentVariableBorrowRate; + uint128 currentStableBorrowRate; + uint40 lastUpdateTimestamp; + uint16 id; + address aTokenAddress; + address stableDebtTokenAddress; + address variableDebtTokenAddress; + address interestRateStrategyAddress; + uint128 accruedToTreasury; + uint128 unbacked; + uint128 isolationModeTotalDebt; + } + + struct ReserveConfigurationMap { + uint256 data; + } + + struct UserConfigurationMap { + uint256 data; + } +} diff --git a/src/libraries/aave/ReserveConfiguration.sol b/src/libraries/aave/ReserveConfiguration.sol new file mode 100644 index 000000000..d3eba3b3d --- /dev/null +++ b/src/libraries/aave/ReserveConfiguration.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity ^0.8.0; + +import {DataTypes} from "./DataTypes.sol"; + +library ReserveConfiguration { + uint256 internal constant LTV_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000; // prettier-ignore + uint256 internal constant LIQUIDATION_THRESHOLD_MASK = + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFF; // prettier-ignore + uint256 internal constant LIQUIDATION_BONUS_MASK = + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF; // prettier-ignore + uint256 internal constant DECIMALS_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFF; // prettier-ignore + uint256 internal constant ACTIVE_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant FROZEN_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant BORROWING_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant STABLE_BORROWING_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant PAUSED_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant BORROWABLE_IN_ISOLATION_MASK = + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant SILOED_BORROWING_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant RESERVE_FACTOR_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant BORROW_CAP_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant SUPPLY_CAP_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant LIQUIDATION_PROTOCOL_FEE_MASK = + 0xFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant EMODE_CATEGORY_MASK = 0xFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant UNBACKED_MINT_CAP_MASK = + 0xFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 internal constant DEBT_CEILING_MASK = 0xF0000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + + uint256 internal constant LIQUIDATION_THRESHOLD_START_BIT_POSITION = 16; + uint256 internal constant LIQUIDATION_BONUS_START_BIT_POSITION = 32; + uint256 internal constant RESERVE_DECIMALS_START_BIT_POSITION = 48; + uint256 internal constant RESERVE_FACTOR_START_BIT_POSITION = 64; + uint256 internal constant EMODE_CATEGORY_START_BIT_POSITION = 168; + + function getActive(DataTypes.ReserveConfigurationMap memory self) internal pure returns (bool) { + return (self.data & ~ACTIVE_MASK) != 0; + } + + function getBorrowingEnabled(DataTypes.ReserveConfigurationMap memory self) internal pure returns (bool) { + return (self.data & ~BORROWING_MASK) != 0; + } + + function getFlags(DataTypes.ReserveConfigurationMap memory self) + internal + pure + returns (bool, bool, bool, bool, bool) + { + uint256 dataLocal = self.data; + + return ( + (dataLocal & ~ACTIVE_MASK) != 0, + (dataLocal & ~FROZEN_MASK) != 0, + (dataLocal & ~BORROWING_MASK) != 0, + (dataLocal & ~STABLE_BORROWING_MASK) != 0, + (dataLocal & ~PAUSED_MASK) != 0 + ); + } + + function getParams(DataTypes.ReserveConfigurationMap memory self) + internal + pure + returns (uint256, uint256, uint256, uint256, uint256, uint256) + { + uint256 dataLocal = self.data; + + return ( + dataLocal & ~LTV_MASK, + (dataLocal & ~LIQUIDATION_THRESHOLD_MASK) >> LIQUIDATION_THRESHOLD_START_BIT_POSITION, + (dataLocal & ~LIQUIDATION_BONUS_MASK) >> LIQUIDATION_BONUS_START_BIT_POSITION, + (dataLocal & ~DECIMALS_MASK) >> RESERVE_DECIMALS_START_BIT_POSITION, + (dataLocal & ~RESERVE_FACTOR_MASK) >> RESERVE_FACTOR_START_BIT_POSITION, + (dataLocal & ~EMODE_CATEGORY_MASK) >> EMODE_CATEGORY_START_BIT_POSITION + ); + } +} diff --git a/src/libraries/aave/UserConfiguration.sol b/src/libraries/aave/UserConfiguration.sol new file mode 100644 index 000000000..931a6d6e9 --- /dev/null +++ b/src/libraries/aave/UserConfiguration.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity ^0.8.0; + +import {DataTypes} from "./DataTypes.sol"; + +library UserConfiguration { + function isUsingAsCollateral(DataTypes.UserConfigurationMap memory self, uint256 reserveIndex) + internal + pure + returns (bool) + { + unchecked { + return (self.data >> ((reserveIndex << 1) + 1)) & 1 != 0; + } + } +} diff --git a/yarn.lock b/yarn.lock index 6e5139869..9a0886998 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,11 +3,11 @@ "@openzeppelin/contracts@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" - integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== + "integrity" "sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw==" + "resolved" "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz" + "version" "4.8.0" -husky@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.2.tgz#5816a60db02650f1f22c8b69b928fd6bcd77a236" - integrity sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg== +"husky@^8.0.2": + "integrity" "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==" + "resolved" "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz" + "version" "8.0.2"