From aadab339acff0f3f96680b98e4d469dfa7ff20bb Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 09:59:35 +0100 Subject: [PATCH 01/73] docs(ifc): fix comments --- src/interfaces/IMorpho.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index b928e50ad..502bdd31b 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -132,9 +132,9 @@ interface IMorphoBase { /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller /// is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific amount /// of shares is given for full compatibility and precision. - /// @dev If the supply of a market gets depleted, the supply share price instantly resets to - /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. /// @dev Supplying a large amount can revert for overflow. + /// @dev Supplying an amount of shares may lead to supply more or fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. /// @param marketParams The market to supply assets to. /// @param assets The amount of assets to supply. /// @param shares The amount of shares to mint. @@ -175,10 +175,10 @@ interface IMorphoBase { /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller /// is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is given for /// full compatibility and precision. - /// @dev If the borrow of a market gets depleted, the borrow share price instantly resets to - /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. /// @dev Borrowing a large amount can revert for overflow. + /// @dev Borrowing an amount of shares may lead to borrow fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. /// @param marketParams The market to borrow assets from. /// @param assets The amount of assets to borrow. /// @param shares The amount of shares to mint. From a4cb34b0619a784a5f40bdba9429ee288b36edef Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 10:42:04 +0100 Subject: [PATCH 02/73] fix(safe-transfer-lib): check for code --- src/libraries/ErrorsLib.sol | 3 +++ src/libraries/SafeTransferLib.sol | 7 ++++--- test/forge/BaseTest.sol | 5 +++++ .../SupplyCollateralIntegrationTest.sol | 12 ++++++++++++ test/forge/integration/SupplyIntegrationTest.sol | 12 ++++++++++++ test/forge/libraries/SafeTransferLibTest.sol | 14 ++++++++++++++ 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 893a6b32d..02cc94423 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -27,6 +27,9 @@ library ErrorsLib { /// @notice Thrown when the market is already created. string internal constant MARKET_ALREADY_CREATED = "market already created"; + /// @notice Thrown when a token to transfer doesn't have code. + string internal constant NO_CODE = "no code"; + /// @notice Thrown when the market is not created. string internal constant MARKET_NOT_CREATED = "market not created"; diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index d31a79121..02c3c0a38 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -15,18 +15,19 @@ interface IERC20Internal { /// @custom:contact security@morpho.org /// @notice Library to manage transfers of tokens, even if calls to the transfer or transferFrom functions are not /// returning a boolean. -/// @dev It is the responsibility of the market creator to make sure that the address of the token has non-zero code. library SafeTransferLib { - /// @dev Warning: It does not revert on `token` with no code. function safeTransfer(IERC20 token, address to, uint256 value) internal { + require(address(token).code.length > 0, ErrorsLib.NO_CODE); + (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(IERC20Internal.transfer, (to, value))); require(success, ErrorsLib.TRANSFER_REVERTED); require(returndata.length == 0 || abi.decode(returndata, (bool)), ErrorsLib.TRANSFER_RETURNED_FALSE); } - /// @dev Warning: It does not revert on `token` with no code. function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + require(address(token).code.length > 0, ErrorsLib.NO_CODE); + (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(IERC20Internal.transferFrom, (from, to, value))); require(success, ErrorsLib.TRANSFER_FROM_REVERTED); diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index a26dab28e..ff9a26319 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -144,6 +144,11 @@ contract BaseTest is Test { return address(uint160(bound(uint256(uint160(input)), 1, type(uint160).max))); } + function _setValidTokens(MarketParams memory params) internal view { + params.loanToken = address(loanToken); + params.collateralToken = address(collateralToken); + } + function _supply(uint256 amount) internal { loanToken.setBalance(address(this), amount); morpho.supply(marketParams, amount, 0, address(this), hex""); diff --git a/test/forge/integration/SupplyCollateralIntegrationTest.sol b/test/forge/integration/SupplyCollateralIntegrationTest.sol index 2994f97ab..2f54ae8b2 100644 --- a/test/forge/integration/SupplyCollateralIntegrationTest.sol +++ b/test/forge/integration/SupplyCollateralIntegrationTest.sol @@ -28,6 +28,18 @@ contract SupplyCollateralIntegrationTest is BaseTest { morpho.supplyCollateral(marketParams, amount, address(0), hex""); } + function testSupplyCollateralTokenNotCreated(uint256 amount, address token) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.assume(token.code.length == 0); + + marketParams.collateralToken = token; + morpho.createMarket(marketParams); + + vm.expectRevert(bytes(ErrorsLib.NO_CODE)); + morpho.supplyCollateral(marketParams, amount, ONBEHALF, hex""); + } + function testSupplyCollateral(uint256 amount) public { amount = bound(amount, 1, MAX_COLLATERAL_ASSETS); diff --git a/test/forge/integration/SupplyIntegrationTest.sol b/test/forge/integration/SupplyIntegrationTest.sol index ceeca7eeb..21b6a6021 100644 --- a/test/forge/integration/SupplyIntegrationTest.sol +++ b/test/forge/integration/SupplyIntegrationTest.sol @@ -41,6 +41,18 @@ contract SupplyIntegrationTest is BaseTest { morpho.supply(marketParams, amount, shares, address(0), hex""); } + function testSupplyTokenNotCreated(uint256 amount, address token) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.assume(token.code.length == 0); + + marketParams.loanToken = token; + morpho.createMarket(marketParams); + + vm.expectRevert(bytes(ErrorsLib.NO_CODE)); + morpho.supply(marketParams, amount, 0, ONBEHALF, hex""); + } + function testSupplyAssets(uint256 amount) public { amount = bound(amount, 1, MAX_TEST_AMOUNT); diff --git a/test/forge/libraries/SafeTransferLibTest.sol b/test/forge/libraries/SafeTransferLibTest.sol index de019de1b..38eb46e1e 100644 --- a/test/forge/libraries/SafeTransferLibTest.sol +++ b/test/forge/libraries/SafeTransferLibTest.sol @@ -85,6 +85,20 @@ contract SafeTransferLibTest is Test { this.safeTransferFrom(address(tokenWithBooleanAlwaysFalse), from, to, amount); } + function testSafeTransferTokenNotCreated(address token, address to, uint256 amount) public { + vm.assume(token.code.length == 0); + + vm.expectRevert(bytes(ErrorsLib.NO_CODE)); + this.safeTransfer(token, to, amount); + } + + function testSafeTransferFromTokenNotCreated(address token, address from, address to, uint256 amount) public { + vm.assume(token.code.length == 0); + + vm.expectRevert(bytes(ErrorsLib.NO_CODE)); + this.safeTransferFrom(token, from, to, amount); + } + function safeTransfer(address token, address to, uint256 amount) external { IERC20(token).safeTransfer(to, amount); } From e8edd2c3f0453003255022d69b716f64be66ce27 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 5 Dec 2023 11:24:31 +0100 Subject: [PATCH 03/73] docs: on all markets --- src/interfaces/IMorpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index b928e50ad..8d250832f 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -70,7 +70,7 @@ interface IMorphoBase { /// @notice Whether the `lltv` is enabled. function isLltvEnabled(uint256 lltv) external view returns (bool); - /// @notice Whether `authorized` is authorized to modify `authorizer`'s positions. + /// @notice Whether `authorized` is authorized to modify `authorizer`'s positions on all markets. /// @dev Anyone is authorized to modify their own positions, regardless of this variable. function isAuthorized(address authorizer, address authorized) external view returns (bool); From dcb9ff2e63c15f8fbb27e7b5035f1782fcf99f17 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 5 Dec 2023 11:26:37 +0100 Subject: [PATCH 04/73] docs: minor improvement --- src/interfaces/IMorpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 8d250832f..7fd02b166 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -70,7 +70,7 @@ interface IMorphoBase { /// @notice Whether the `lltv` is enabled. function isLltvEnabled(uint256 lltv) external view returns (bool); - /// @notice Whether `authorized` is authorized to modify `authorizer`'s positions on all markets. + /// @notice Whether `authorized` is authorized to modify `authorizer`'s position on all markets. /// @dev Anyone is authorized to modify their own positions, regardless of this variable. function isAuthorized(address authorizer, address authorized) external view returns (bool); From f50c7096a91ba69cc13d77ea8cbd1afc8bc62d3c Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 5 Dec 2023 11:46:04 +0100 Subject: [PATCH 05/73] docs: cantina-23 --- src/libraries/EventsLib.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 825ea1791..1630dad56 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -37,7 +37,7 @@ library EventsLib { /// @notice Emitted on supply of assets. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address that received the supply. + /// @param onBehalf The owner of the modified position. /// @param assets The amount of assets supplied. /// @param shares The amount of shares minted. event Supply(Id indexed id, address indexed caller, address indexed onBehalf, uint256 assets, uint256 shares); @@ -45,7 +45,7 @@ library EventsLib { /// @notice Emitted on withdrawal of assets. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address from which the assets were withdrawn. + /// @param onBehalf The owner of the modified position. /// @param receiver The address that received the withdrawn assets. /// @param assets The amount of assets withdrawn. /// @param shares The amount of shares burned. @@ -61,7 +61,7 @@ library EventsLib { /// @notice Emitted on borrow of assets. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address from which the assets were borrowed. + /// @param onBehalf The owner of the modified position. /// @param receiver The address that received the borrowed assets. /// @param assets The amount of assets borrowed. /// @param shares The amount of shares minted. @@ -77,7 +77,7 @@ library EventsLib { /// @notice Emitted on repayment of assets. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address for which the assets were repaid. + /// @param onBehalf The owner of the modified position. /// @param assets The amount of assets repaid. May be 1 over the corresponding market's `totalBorrowAssets`. /// @param shares The amount of shares burned. event Repay(Id indexed id, address indexed caller, address indexed onBehalf, uint256 assets, uint256 shares); @@ -85,14 +85,14 @@ library EventsLib { /// @notice Emitted on supply of collateral. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address that received the collateral. + /// @param onBehalf The owner of the modified position. /// @param assets The amount of collateral supplied. event SupplyCollateral(Id indexed id, address indexed caller, address indexed onBehalf, uint256 assets); /// @notice Emitted on withdrawal of collateral. /// @param id The market id. /// @param caller The caller. - /// @param onBehalf The address from which the collateral was withdrawn. + /// @param onBehalf The owner of the modified position. /// @param receiver The address that received the withdrawn collateral. /// @param assets The amount of collateral withdrawn. event WithdrawCollateral( From 287fdfdf7c2a1b9c681aa993aa9484ed8afc49b2 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 14:12:26 +0100 Subject: [PATCH 06/73] docs(ifc): fix typo --- src/libraries/periphery/MorphoBalancesLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 3afabfd5f..844c891dd 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -41,7 +41,7 @@ library MorphoBalancesLib { uint256 elapsed = block.timestamp - market.lastUpdate; - // Skipped if elapsed == 0 of if totalBorrowAssets == 0 because interest would be null. + // Skipped if elapsed == 0 or if totalBorrowAssets == 0 because interest would be null. if (elapsed != 0 && market.totalBorrowAssets != 0) { uint256 borrowRate = IIrm(marketParams.irm).borrowRateView(marketParams, market); uint256 interest = market.totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); From d40529ab7c62f2c3d17eed175d6baa33994b612d Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 14:20:23 +0100 Subject: [PATCH 07/73] fix(test): remove useless util --- test/forge/BaseTest.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index ff9a26319..a26dab28e 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -144,11 +144,6 @@ contract BaseTest is Test { return address(uint160(bound(uint256(uint160(input)), 1, type(uint160).max))); } - function _setValidTokens(MarketParams memory params) internal view { - params.loanToken = address(loanToken); - params.collateralToken = address(collateralToken); - } - function _supply(uint256 amount) internal { loanToken.setBalance(address(this), amount); morpho.supply(marketParams, amount, 0, address(this), hex""); From a82219688f848463fbc899e228ffe94196c20772 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 15:16:39 +0100 Subject: [PATCH 08/73] docs(ifc): add edge case --- src/libraries/periphery/MorphoBalancesLib.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 844c891dd..21874bd15 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -90,8 +90,8 @@ library MorphoBalancesLib { /// @notice Returns the expected supply assets balance of `user` on a market after having accrued interest. /// @dev Warning: Wrong for `feeRecipient` because their supply shares increase is not taken into account. - /// @dev Warning: Withdrawing a supply position using the expected assets balance can lead to a revert due to - /// conversion roundings between shares and assets. + /// @dev Warning: Withdrawing using the expected supply assets can lead to a revert due to conversion roundings from + /// assets to shares. function expectedSupplyAssets(IMorpho morpho, MarketParams memory marketParams, address user) internal view @@ -105,8 +105,10 @@ library MorphoBalancesLib { } /// @notice Returns the expected borrow assets balance of `user` on a market after having accrued interest. - /// @dev Warning: repaying a borrow position using the expected assets balance can lead to a revert due to - /// conversion roundings between shares and assets. + /// @dev Warning: The expected balance is rounded up, so it can be greater than the market's expected total supply + /// assets. + /// @dev Warning: Repaying using the expected supply assets can lead to a revert due to conversion roundings from + /// assets to shares. function expectedBorrowAssets(IMorpho morpho, MarketParams memory marketParams, address user) internal view From c8424419d6e84c645f889abade7bd6156561b8d7 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 5 Dec 2023 15:57:25 +0100 Subject: [PATCH 09/73] docs: cantina-48 --- src/interfaces/IMorpho.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 7fd02b166..fa1ca2134 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -91,6 +91,7 @@ interface IMorphoBase { function enableLltv(uint256 lltv) external; /// @notice Sets the `newFee` for the given market `marketParams`. + /// @param newFee The new fee, scaled by wad. /// @dev Warning: The recipient can be the zero address. function setFee(MarketParams memory marketParams, uint256 newFee) external; From 4d18f5970892820119f3ddd28fa45da555457b16 Mon Sep 17 00:00:00 2001 From: Romain Milon Date: Tue, 5 Dec 2023 16:17:51 +0100 Subject: [PATCH 10/73] docs(ifc): fix supply -> borrow Co-authored-by: Quentin Garchery Signed-off-by: Romain Milon --- src/libraries/periphery/MorphoBalancesLib.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 21874bd15..47963af71 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -105,9 +105,9 @@ library MorphoBalancesLib { } /// @notice Returns the expected borrow assets balance of `user` on a market after having accrued interest. - /// @dev Warning: The expected balance is rounded up, so it can be greater than the market's expected total supply + /// @dev Warning: The expected balance is rounded up, so it can be greater than the market's expected total borrow /// assets. - /// @dev Warning: Repaying using the expected supply assets can lead to a revert due to conversion roundings from + /// @dev Warning: Repaying using the expected borrow assets can lead to a revert due to conversion roundings from /// assets to shares. function expectedBorrowAssets(IMorpho morpho, MarketParams memory marketParams, address user) internal From af5ea2778b4dc23519ea782457668f78130c6e86 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 5 Dec 2023 16:28:06 +0100 Subject: [PATCH 11/73] docs: cantina-90 --- src/interfaces/IMorpho.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index fa1ca2134..fa65408dc 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -201,6 +201,7 @@ interface IMorphoBase { /// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow. /// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion /// roundings between shares and assets. + /// @dev An attacker can front-run a repay with a small repay making the transaction revert for underflow. /// @param marketParams The market to repay assets to. /// @param assets The amount of assets to repay. /// @param shares The amount of shares to burn. @@ -243,6 +244,7 @@ interface IMorphoBase { /// @dev Either `seizedAssets` or `repaidShares` should be zero. /// @dev Seizing more than the collateral balance will underflow and revert without any error message. /// @dev Repaying more than the borrow balance will underflow and revert without any error message. + /// @dev An attacker can front-run a liquidation with a small repay making the transaction revert for underflow. /// @param marketParams The market of the position. /// @param borrower The owner of the position. /// @param seizedAssets The amount of collateral to seize. From 9772375127c484121ed3459d20067fdd7dfe1536 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 5 Dec 2023 17:24:20 +0100 Subject: [PATCH 12/73] docs(ifc): remove comment --- src/libraries/periphery/MorphoBalancesLib.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 21874bd15..8b1c0f815 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -105,10 +105,8 @@ library MorphoBalancesLib { } /// @notice Returns the expected borrow assets balance of `user` on a market after having accrued interest. - /// @dev Warning: The expected balance is rounded up, so it can be greater than the market's expected total supply + /// @dev Warning: The expected balance is rounded up, so it may be greater than the market's expected total borrow /// assets. - /// @dev Warning: Repaying using the expected supply assets can lead to a revert due to conversion roundings from - /// assets to shares. function expectedBorrowAssets(IMorpho morpho, MarketParams memory marketParams, address user) internal view From 9c87b92318c1c5aad0b1c80e2124ae5eb4b73e0f Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 6 Dec 2023 15:45:22 +0100 Subject: [PATCH 13/73] chore: minor improvements --- src/Morpho.sol | 2 +- src/libraries/UtilsLib.sol | 2 +- test/forge/helpers/SigUtils.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..d5be6b9f0 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -437,7 +437,7 @@ contract Morpho is IMorphoStaticTyping { require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE); bytes32 hashStruct = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, authorization)); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); + bytes32 digest = keccak256(bytes.concat("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); address signatory = ecrecover(digest, signature.v, signature.r, signature.s); require(signatory != address(0) && authorization.authorizer == signatory, ErrorsLib.INVALID_SIGNATURE); diff --git a/src/libraries/UtilsLib.sol b/src/libraries/UtilsLib.sol index 066043d13..f343ef769 100644 --- a/src/libraries/UtilsLib.sol +++ b/src/libraries/UtilsLib.sol @@ -29,7 +29,7 @@ library UtilsLib { return uint128(x); } - /// @dev Returns max(x - y, 0). + /// @dev Returns max(0, x - y). function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) { assembly { z := mul(gt(x, y), sub(x, y)) diff --git a/test/forge/helpers/SigUtils.sol b/test/forge/helpers/SigUtils.sol index 9a60b52f0..b8e90ea51 100644 --- a/test/forge/helpers/SigUtils.sol +++ b/test/forge/helpers/SigUtils.sol @@ -12,7 +12,7 @@ library SigUtils { pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", domainSeparator, hashStruct(authorization))); + return keccak256(bytes.concat("\x19\x01", domainSeparator, hashStruct(authorization))); } function hashStruct(Authorization memory authorization) internal pure returns (bytes32) { From 4ac87781e2a29c54fdcaca5b3ef03fb14c5668ff Mon Sep 17 00:00:00 2001 From: Jean-Grimal Date: Wed, 6 Dec 2023 16:45:09 +0100 Subject: [PATCH 14/73] fix: add call to irm at market creation --- src/Morpho.sol | 3 +++ test/forge/integration/CreateMarketIntegrationTest.sol | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..103c5239a 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -158,6 +158,9 @@ contract Morpho is IMorphoStaticTyping { idToMarketParams[id] = marketParams; emit EventsLib.CreateMarket(id, marketParams); + + // Call Irm in case it is statefull. + IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } /* SUPPLY MANAGEMENT */ diff --git a/test/forge/integration/CreateMarketIntegrationTest.sol b/test/forge/integration/CreateMarketIntegrationTest.sol index 1fd2eb083..50bfdc38a 100644 --- a/test/forge/integration/CreateMarketIntegrationTest.sol +++ b/test/forge/integration/CreateMarketIntegrationTest.sol @@ -47,6 +47,8 @@ contract CreateMarketIntegrationTest is BaseTest { if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + vm.expectEmit(true, true, true, true, address(morpho)); emit EventsLib.CreateMarket(marketParamsFuzz.id(), marketParamsFuzz); morpho.createMarket(marketParamsFuzz); @@ -66,6 +68,9 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + morpho.createMarket(marketParamsFuzz); vm.expectRevert(bytes(ErrorsLib.MARKET_ALREADY_CREATED)); @@ -81,6 +86,8 @@ contract CreateMarketIntegrationTest is BaseTest { if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + morpho.createMarket(marketParamsFuzz); vm.stopPrank(); From 01f9e2ba95fa6156c7df6563f07cb436e2a261a5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 7 Dec 2023 10:16:53 +0100 Subject: [PATCH 15/73] refactor: create market input validation rule --- certora/specs/Reverts.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 9ae422c51..059011fde 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -103,14 +103,14 @@ rule setFeeRecipientRevertCondition(env e, address newFeeRecipient) { assert lastReverted <=> e.msg.value != 0 || e.msg.sender != oldOwner || newFeeRecipient == oldFeeRecipient; } -// Check the revert condition for the createMarket function. -rule createMarketRevertCondition(env e, MorphoHarness.MarketParams marketParams) { +// Check that createMarket reverts when its input are not validated. +rule createMarketInputValidation(env e, MorphoHarness.MarketParams marketParams) { MorphoHarness.Id id = libId(marketParams); bool irmEnabled = isIrmEnabled(marketParams.irm); bool lltvEnabled = isLltvEnabled(marketParams.lltv); bool wasCreated = isCreated(id); createMarket@withrevert(e, marketParams); - assert lastReverted <=> e.msg.value != 0 || !irmEnabled || !lltvEnabled || wasCreated; + assert e.msg.value != 0 || !irmEnabled || !lltvEnabled || wasCreated => lastReverted; } // Check that supply reverts when its input are not validated. From 4b33eefe444761adda947d08fb9da86768c328c6 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 7 Dec 2023 12:19:29 +0100 Subject: [PATCH 16/73] feat: log bad debt but require via ir --- src/Morpho.sol | 11 +++++++---- src/libraries/EventsLib.sol | 1 + test/forge/integration/LiquidateIntegrationTest.sol | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..e522024ef 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -384,15 +384,16 @@ contract Morpho is IMorphoStaticTyping { position[id][borrower].collateral -= seizedAssets.toUint128(); uint256 badDebtShares; + uint256 badDebtAssets; if (position[id][borrower].collateral == 0) { badDebtShares = position[id][borrower].borrowShares; - uint256 badDebt = UtilsLib.min( + badDebtAssets = UtilsLib.min( market[id].totalBorrowAssets, badDebtShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares) ); - market[id].totalBorrowAssets -= badDebt.toUint128(); - market[id].totalSupplyAssets -= badDebt.toUint128(); + market[id].totalBorrowAssets -= badDebtAssets.toUint128(); + market[id].totalSupplyAssets -= badDebtAssets.toUint128(); market[id].totalBorrowShares -= badDebtShares.toUint128(); position[id][borrower].borrowShares = 0; } @@ -400,7 +401,9 @@ contract Morpho is IMorphoStaticTyping { IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets); // `repaidAssets` may be greater than `totalBorrowAssets` by 1. - emit EventsLib.Liquidate(id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtShares); + emit EventsLib.Liquidate( + id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtAssets, badDebtShares + ); if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data); diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 825ea1791..2ab1cdfc7 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -114,6 +114,7 @@ library EventsLib { uint256 repaidAssets, uint256 repaidShares, uint256 seizedAssets, + uint256 badDebtAssets, uint256 badDebtShares ); diff --git a/test/forge/integration/LiquidateIntegrationTest.sol b/test/forge/integration/LiquidateIntegrationTest.sol index 0fb9325c9..f59223f37 100644 --- a/test/forge/integration/LiquidateIntegrationTest.sol +++ b/test/forge/integration/LiquidateIntegrationTest.sol @@ -154,7 +154,7 @@ contract LiquidateIntegrationTest is BaseTest { vm.prank(LIQUIDATOR); vm.expectEmit(true, true, true, true, address(morpho)); - emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, expectedRepaidShares, amountSeized, 0); + emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, expectedRepaidShares, amountSeized, 0, 0); (uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(marketParams, BORROWER, amountSeized, 0, hex""); uint256 expectedCollateral = params.amountCollateral - amountSeized; @@ -214,7 +214,7 @@ contract LiquidateIntegrationTest is BaseTest { vm.prank(LIQUIDATOR); vm.expectEmit(true, true, true, true, address(morpho)); - emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, sharesRepaid, expectedSeized, 0); + emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, sharesRepaid, expectedSeized, 0, 0); (uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(marketParams, BORROWER, 0, sharesRepaid, hex""); uint256 expectedCollateral = params.amountCollateral - expectedSeized; @@ -303,6 +303,7 @@ contract LiquidateIntegrationTest is BaseTest { params.expectedRepaid, params.expectedRepaidShares, amountCollateral, + params.expectedBadDebt, params.expectedBadDebt * SharesMathLib.VIRTUAL_SHARES ); (uint256 returnSeized, uint256 returnRepaid) = From 8d81c3829a1bcf7e8c87061b203f222f4dd222e0 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 7 Dec 2023 12:24:01 +0100 Subject: [PATCH 17/73] refactor: no need for via ir anymore --- src/Morpho.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index e522024ef..787013200 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -354,12 +354,12 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); - uint256 collateralPrice = IOracle(marketParams.oracle).price(); - - require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); - uint256 repaidAssets; { + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); + // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). uint256 liquidationIncentiveFactor = UtilsLib.min( MAX_LIQUIDATION_INCENTIVE_FACTOR, From e7bfeeae9908b3e6f59ad8a42215eed685e1fa68 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 7 Dec 2023 12:25:40 +0100 Subject: [PATCH 18/73] docs: add missing param --- src/libraries/EventsLib.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 2ab1cdfc7..5b35fa9eb 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -106,6 +106,7 @@ library EventsLib { /// @param repaidAssets The amount of assets repaid. May be 1 over the corresponding market's `totalBorrowAssets`. /// @param repaidShares The amount of shares burned. /// @param seizedAssets The amount of collateral seized. + /// @param badDebtAssets The amount of assets minted as bad debt. /// @param badDebtShares The amount of shares minted as bad debt. event Liquidate( Id indexed id, From 289ad5e766c904049bfc3f85716f5bedc6441e50 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 7 Dec 2023 12:26:10 +0100 Subject: [PATCH 19/73] fix: liquidation rounding --- src/Morpho.sol | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index f755904b6..9a562577c 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -358,24 +358,19 @@ contract Morpho is IMorphoStaticTyping { require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); - uint256 repaidAssets; - { - // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). - uint256 liquidationIncentiveFactor = UtilsLib.min( - MAX_LIQUIDATION_INCENTIVE_FACTOR, - WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) - ); + // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). + uint256 liquidationIncentiveFactor = UtilsLib.min( + MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) + ); - if (seizedAssets > 0) { - repaidAssets = - seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - repaidShares = repaidAssets.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); - } else { - repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); - seizedAssets = - repaidAssets.wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); - } + if (seizedAssets > 0) { + repaidShares = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor) + .toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + } else { + seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) + .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); } + uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); position[id][borrower].borrowShares -= repaidShares.toUint128(); market[id].totalBorrowShares -= repaidShares.toUint128(); From c02daf829149990a542bd09d6272aa418a427919 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:43:00 +0100 Subject: [PATCH 20/73] Update src/Morpho.sol Co-authored-by: Romain Milon Signed-off-by: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 103c5239a..9b03acdd8 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -159,7 +159,7 @@ contract Morpho is IMorphoStaticTyping { emit EventsLib.CreateMarket(id, marketParams); - // Call Irm in case it is statefull. + // Call IRM in case it is stateful. IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } From faef77c2905a28f4b641a6d26a1f04ed7491e44a Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:12:31 +0100 Subject: [PATCH 21/73] Update src/Morpho.sol Co-authored-by: Romain Milon Signed-off-by: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 9b03acdd8..96ffb67f2 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -159,7 +159,7 @@ contract Morpho is IMorphoStaticTyping { emit EventsLib.CreateMarket(id, marketParams); - // Call IRM in case it is stateful. + // Call to initialize IRM in case it is stateful. IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } From 44281bda82f193231a15e0a9b468edd72bfb77c7 Mon Sep 17 00:00:00 2001 From: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:12:42 +0100 Subject: [PATCH 22/73] docs: apply suggestion Co-authored-by: MathisGD <74971347+MathisGD@users.noreply.github.com> Signed-off-by: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> --- src/libraries/EventsLib.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 5b35fa9eb..166f4d296 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -106,8 +106,8 @@ library EventsLib { /// @param repaidAssets The amount of assets repaid. May be 1 over the corresponding market's `totalBorrowAssets`. /// @param repaidShares The amount of shares burned. /// @param seizedAssets The amount of collateral seized. - /// @param badDebtAssets The amount of assets minted as bad debt. - /// @param badDebtShares The amount of shares minted as bad debt. + /// @param badDebtAssets The amount of assets of bad debt realized. + /// @param badDebtShares The amount of borrow shares of bad debt realized. event Liquidate( Id indexed id, address indexed caller, From 4d68d40cc25930fa8d07cbee883974b949434589 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:18:04 +0100 Subject: [PATCH 23/73] Update src/Morpho.sol Co-authored-by: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> Signed-off-by: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 96ffb67f2..16fd75c80 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -159,7 +159,7 @@ contract Morpho is IMorphoStaticTyping { emit EventsLib.CreateMarket(id, marketParams); - // Call to initialize IRM in case it is stateful. + // Call to initialize the IRM in case it is stateful. IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } From d3e785a34a652bf6ab9005721397a90a20aec1dd Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 10:21:33 +0100 Subject: [PATCH 24/73] docs(shares-math): document borrow share price --- src/libraries/SharesMathLib.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index 514760698..224c5f179 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -18,6 +18,8 @@ library SharesMathLib { /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is /// empty. + /// @dev Warning: virtual borrow assets behave like bad debt, but it is assumed the borrow share price stays low + /// enough to not inflate the virtual borrow assets too much. uint256 internal constant VIRTUAL_ASSETS = 1; /// @dev Calculates the value of `assets` quoted in shares, rounding down. From 38070e3179acbaa36fb3a07b1c0e07be86ff7d52 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 10:22:37 +0100 Subject: [PATCH 25/73] docs(shares-math): fix wording --- src/libraries/SharesMathLib.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index 224c5f179..cf6176e31 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -18,8 +18,8 @@ library SharesMathLib { /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is /// empty. - /// @dev Warning: virtual borrow assets behave like bad debt, but it is assumed the borrow share price stays low - /// enough to not inflate the virtual borrow assets too much. + /// @dev Warning: virtual borrow assets behave like unrealizable bad debt, but it is assumed the borrow share price + /// stays low enough to not inflate the virtual borrow assets too much. uint256 internal constant VIRTUAL_ASSETS = 1; /// @dev Calculates the value of `assets` quoted in shares, rounding down. From f4272f935fc185d4bdcbb41a537f72589ded2225 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 7 Dec 2023 15:03:16 +0100 Subject: [PATCH 26/73] fix: compilation without via-ir --- src/Morpho.sol | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 9a562577c..b4d93c089 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -358,17 +358,21 @@ contract Morpho is IMorphoStaticTyping { require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); - // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). - uint256 liquidationIncentiveFactor = UtilsLib.min( - MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) - ); + { + // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). + uint256 liquidationIncentiveFactor = UtilsLib.min( + MAX_LIQUIDATION_INCENTIVE_FACTOR, + WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) + ); - if (seizedAssets > 0) { - repaidShares = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor) - .toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); - } else { - seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) - .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); + if (seizedAssets > 0) { + repaidShares = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp( + liquidationIncentiveFactor + ).toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + } else { + seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) + .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); + } } uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); From e14da6e1dc7a9df24ae2d52a555dc69fc93a0d99 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 7 Dec 2023 15:03:57 +0100 Subject: [PATCH 27/73] test: test liquidation fix --- .../integration/LiquidateIntegrationTest.sol | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/forge/integration/LiquidateIntegrationTest.sol b/test/forge/integration/LiquidateIntegrationTest.sol index 0fb9325c9..4b05a589f 100644 --- a/test/forge/integration/LiquidateIntegrationTest.sol +++ b/test/forge/integration/LiquidateIntegrationTest.sol @@ -206,8 +206,8 @@ contract LiquidateIntegrationTest is BaseTest { sharesRepaid = bound(sharesRepaid, 1, Math.min(borrowShares, maxSharesRepaid)); uint256 expectedRepaid = sharesRepaid.toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); - uint256 expectedSeized = - expectedRepaid.wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, params.priceCollateral); + uint256 expectedSeized = sharesRepaid.toAssetsDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)) + .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, params.priceCollateral); loanToken.setBalance(LIQUIDATOR, params.amountBorrowed); @@ -353,4 +353,28 @@ contract LiquidateIntegrationTest is BaseTest { vm.prank(LIQUIDATOR); morpho.liquidate(marketParams, BORROWER, collateralAmount, 0, hex""); } + + function testSeizedAssetsRoundUp() public { + _setLltv(0.75e18); + _supply(100e18); + + uint256 amountCollateral = 400; + uint256 amountBorrowed = 300; + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(marketParams, amountCollateral, BORROWER, hex""); + morpho.borrow(marketParams, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + oracle.setPrice(ORACLE_PRICE_SCALE - 0.01e18); + + loanToken.setBalance(LIQUIDATOR, amountBorrowed); + + vm.prank(LIQUIDATOR); + (uint256 seizedAssets, uint256 repaidAssets) = morpho.liquidate(marketParams, BORROWER, 0, 1, hex""); + + assertEq(seizedAssets, 0, "seizedAssets"); + assertEq(repaidAssets, 1, "repaidAssets"); + } } From c5f6c94fee665135c47d49fe34034efecc19c458 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 16:12:14 +0100 Subject: [PATCH 28/73] docs(shares-math): change wording --- src/libraries/SharesMathLib.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index cf6176e31..0a87fe774 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -14,12 +14,12 @@ library SharesMathLib { /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure /// high precision computations. + /// @dev Warning: the assets virtual borrow shares are entitled to behave like unrealizable bad debt, but it is + /// assumed the borrow share price stays low enough to not inflate these assets. uint256 internal constant VIRTUAL_SHARES = 1e6; /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is /// empty. - /// @dev Warning: virtual borrow assets behave like unrealizable bad debt, but it is assumed the borrow share price - /// stays low enough to not inflate the virtual borrow assets too much. uint256 internal constant VIRTUAL_ASSETS = 1; /// @dev Calculates the value of `assets` quoted in shares, rounding down. From 7ab8eb523565801f2de090ad1422d0c9b2c94f19 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 17:33:08 +0100 Subject: [PATCH 29/73] docs(shares-math): capitalize natspec --- src/libraries/SharesMathLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index 0a87fe774..cbbe908ef 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -14,7 +14,7 @@ library SharesMathLib { /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure /// high precision computations. - /// @dev Warning: the assets virtual borrow shares are entitled to behave like unrealizable bad debt, but it is + /// @dev Warning: The assets virtual borrow shares are entitled to behave like unrealizable bad debt, but it is /// assumed the borrow share price stays low enough to not inflate these assets. uint256 internal constant VIRTUAL_SHARES = 1e6; From 53938ce89f37488886666f2f9f24ebb8e027813b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 17:34:36 +0100 Subject: [PATCH 30/73] ci(foundry): use test profile --- .github/workflows/foundry.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 4334bdbf6..99b493082 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -39,7 +39,7 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run Forge tests in ${{ matrix.type }} mode - run: forge test -vvv + run: yarn test:forge -vvv env: FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }} FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }} From f5ecb61430c7a30258b343091c64e5c13d6ea02b Mon Sep 17 00:00:00 2001 From: MathisGD Date: Fri, 8 Dec 2023 13:14:31 +0100 Subject: [PATCH 31/73] refactor: fix stack too deep --- src/Morpho.sol | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index b4d93c089..707674cf0 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -340,6 +340,11 @@ contract Morpho is IMorphoStaticTyping { /* LIQUIDATION */ + struct Vars { + uint256 collateralPrice; + uint256 liquidationIncentiveFactor; + } + /// @inheritdoc IMorphoBase function liquidate( MarketParams memory marketParams, @@ -354,25 +359,23 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); - uint256 collateralPrice = IOracle(marketParams.oracle).price(); + Vars memory vars; + vars.collateralPrice = IOracle(marketParams.oracle).price(); - require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); + require(!_isHealthy(marketParams, id, borrower, vars.collateralPrice), ErrorsLib.HEALTHY_POSITION); - { - // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). - uint256 liquidationIncentiveFactor = UtilsLib.min( - MAX_LIQUIDATION_INCENTIVE_FACTOR, - WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) - ); + // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). + vars.liquidationIncentiveFactor = UtilsLib.min( + MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) + ); - if (seizedAssets > 0) { - repaidShares = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp( - liquidationIncentiveFactor - ).toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); - } else { - seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) - .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); - } + if (seizedAssets > 0) { + repaidShares = seizedAssets.mulDivUp(vars.collateralPrice, ORACLE_PRICE_SCALE).wDivUp( + vars.liquidationIncentiveFactor + ).toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + } else { + seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) + .wMulDown(vars.liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, vars.collateralPrice); } uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); From bab5dea3b3d9f7517de0497b256615c35fe355a2 Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Fri, 8 Dec 2023 13:38:14 +0100 Subject: [PATCH 32/73] no memory vars --- src/Morpho.sol | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 707674cf0..1ed573294 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -340,11 +340,6 @@ contract Morpho is IMorphoStaticTyping { /* LIQUIDATION */ - struct Vars { - uint256 collateralPrice; - uint256 liquidationIncentiveFactor; - } - /// @inheritdoc IMorphoBase function liquidate( MarketParams memory marketParams, @@ -359,23 +354,25 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); - Vars memory vars; - vars.collateralPrice = IOracle(marketParams.oracle).price(); + uint256 collateralPrice = IOracle(marketParams.oracle).price(); - require(!_isHealthy(marketParams, id, borrower, vars.collateralPrice), ErrorsLib.HEALTHY_POSITION); + require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); - // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). - vars.liquidationIncentiveFactor = UtilsLib.min( - MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) - ); + { + // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). + uint liquidationIncentiveFactor = UtilsLib.min( + MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) + ); - if (seizedAssets > 0) { - repaidShares = seizedAssets.mulDivUp(vars.collateralPrice, ORACLE_PRICE_SCALE).wDivUp( - vars.liquidationIncentiveFactor - ).toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); - } else { - seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) - .wMulDown(vars.liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, vars.collateralPrice); + if (seizedAssets > 0) { + uint repaidIntermediate = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp( + liquidationIncentiveFactor + ); + repaidShares = repaidIntermediate.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + } else { + seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) + .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); + } } uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); From d7bc725520ad05102723849e8ce5cf225915677c Mon Sep 17 00:00:00 2001 From: MathisGD Date: Fri, 8 Dec 2023 15:22:54 +0100 Subject: [PATCH 33/73] chore: fmt --- src/Morpho.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 1ed573294..1587280bc 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -360,15 +360,15 @@ contract Morpho is IMorphoStaticTyping { { // The liquidation incentive factor is min(maxLiquidationIncentiveFactor, 1/(1 - cursor*(1 - lltv))). - uint liquidationIncentiveFactor = UtilsLib.min( - MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) + uint256 liquidationIncentiveFactor = UtilsLib.min( + MAX_LIQUIDATION_INCENTIVE_FACTOR, + WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv)) ); if (seizedAssets > 0) { - uint repaidIntermediate = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp( - liquidationIncentiveFactor - ); - repaidShares = repaidIntermediate.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + uint256 intermediateVar = + seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); + repaidShares = intermediateVar.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); } else { seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); From 867ca89089f0af975dcc7a2060f7bcb83198a4ca Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 8 Dec 2023 17:14:31 +0100 Subject: [PATCH 34/73] docs(shares-math): fix wording --- src/libraries/SharesMathLib.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index cbbe908ef..ff6bc94b4 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -14,8 +14,8 @@ library SharesMathLib { /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure /// high precision computations. - /// @dev Warning: The assets virtual borrow shares are entitled to behave like unrealizable bad debt, but it is - /// assumed the borrow share price stays low enough to not inflate these assets. + /// @dev Warning: The assets to which virtual borrow shares are entitled behave like unrealizable bad debt, but it + /// is assumed the borrow share price stays low enough to not inflate these assets. uint256 internal constant VIRTUAL_SHARES = 1e6; /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is From d9792e28188cd4d88c6c5ea1f58f6a8da542ccfa Mon Sep 17 00:00:00 2001 From: MathisGD Date: Sun, 10 Dec 2023 15:48:04 +0100 Subject: [PATCH 35/73] fix: issue-431 --- src/Morpho.sol | 2 ++ test/forge/integration/AuthorizationIntegrationTest.sol | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/Morpho.sol b/src/Morpho.sol index d5be6b9f0..e64155fb2 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -426,6 +426,8 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorization(address authorized, bool newIsAuthorized) external { + require(newIsAuthorized != isAuthorized[msg.sender][authorized], ErrorsLib.ALREADY_SET); + isAuthorized[msg.sender][authorized] = newIsAuthorized; emit EventsLib.SetAuthorization(msg.sender, msg.sender, authorized, newIsAuthorized); diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 263eaa550..69ef8f8de 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -16,6 +16,12 @@ contract AuthorizationIntegrationTest is BaseTest { assertFalse(morpho.isAuthorized(address(this), addressFuzz)); } + function testAlreadySet(address addressFuzz, bool status) public { + morpho.setAuthorization(addressFuzz, status); + vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); + morpho.setAuthorization(addressFuzz, status); + } + function testSetAuthorizationWithSignatureDeadlineOutdated( Authorization memory authorization, uint256 privateKey, From 32ba44979a7b9f892bf5d24cef36a7b80a54d575 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Sun, 10 Dec 2023 16:34:46 +0100 Subject: [PATCH 36/73] fix: issue-503 --- src/Morpho.sol | 8 ++++---- src/interfaces/IMorpho.sol | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index e64155fb2..830fd421d 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -397,11 +397,11 @@ contract Morpho is IMorphoStaticTyping { position[id][borrower].borrowShares = 0; } - IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets); - // `repaidAssets` may be greater than `totalBorrowAssets` by 1. emit EventsLib.Liquidate(id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtShares); + IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets); + if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data); IERC20(marketParams.loanToken).safeTransferFrom(msg.sender, address(this), repaidAssets); @@ -413,10 +413,10 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function flashLoan(address token, uint256 assets, bytes calldata data) external { - IERC20(token).safeTransfer(msg.sender, assets); - emit EventsLib.FlashLoan(msg.sender, token, assets); + IERC20(token).safeTransfer(msg.sender, assets); + IMorphoFlashLoanCallback(msg.sender).onMorphoFlashLoan(assets, data); IERC20(token).safeTransferFrom(msg.sender, address(this), assets); diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index fa65408dc..9e7564f6d 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -130,7 +130,8 @@ interface IMorphoBase { /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's /// `onMorphoSupply` function with the given `data`. - /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller /// is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific amount /// of shares is given for full compatibility and precision. /// @dev If the supply of a market gets depleted, the supply share price instantly resets to @@ -173,7 +174,8 @@ interface IMorphoBase { ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn); /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` to `receiver`. - /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller /// is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is given for /// full compatibility and precision. /// @dev If the borrow of a market gets depleted, the borrow share price instantly resets to From 1176e03547429181f778a46938f65ef3430ba552 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 14:28:37 +0100 Subject: [PATCH 37/73] feat(irm): skip address(0) --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 7871519af..3bc606a1b 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -477,7 +477,7 @@ contract Morpho is IMorphoStaticTyping { function _accrueInterest(MarketParams memory marketParams, Id id) internal { uint256 elapsed = block.timestamp - market[id].lastUpdate; - if (elapsed == 0) return; + if (elapsed == 0 || marketParams.irm == address(0)) return; uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]); uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); From fc91741337cb4d46fb6dc6f157a8196e6b4f37d9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 15:39:39 +0100 Subject: [PATCH 38/73] fix(irm): skip before elapsed --- src/Morpho.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 3bc606a1b..90bb57837 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -475,9 +475,10 @@ contract Morpho is IMorphoStaticTyping { /// @dev Accrues interest for the given market `marketParams`. /// @dev Assumes that the inputs `marketParams` and `id` match. function _accrueInterest(MarketParams memory marketParams, Id id) internal { - uint256 elapsed = block.timestamp - market[id].lastUpdate; + if (marketParams.irm == address(0)) return; - if (elapsed == 0 || marketParams.irm == address(0)) return; + uint256 elapsed = block.timestamp - market[id].lastUpdate; + if (elapsed == 0) return; uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]); uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); From 0e0ebdbeaee6bff0641557b8c9afb942535a1b0d Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 7 Dec 2023 17:31:17 +0100 Subject: [PATCH 39/73] test(irm): add irm zero test --- lib/forge-std | 2 +- test/forge/BaseTest.sol | 2 ++ .../integration/AccrueInterestIntegrationTest.sol | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index e8a047e3f..2f1126975 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e +Subproject commit 2f112697506eab12d433a65fdc31a639548fe365 diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index a26dab28e..ef8c19836 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -82,7 +82,9 @@ contract BaseTest is Test { irm = new IrmMock(); vm.startPrank(OWNER); + morpho.enableIrm(address(0)); morpho.enableIrm(address(irm)); + morpho.enableLltv(0); morpho.setFeeRecipient(FEE_RECIPIENT); vm.stopPrank(); diff --git a/test/forge/integration/AccrueInterestIntegrationTest.sol b/test/forge/integration/AccrueInterestIntegrationTest.sol index 3b91564f7..c935f8f58 100644 --- a/test/forge/integration/AccrueInterestIntegrationTest.sol +++ b/test/forge/integration/AccrueInterestIntegrationTest.sol @@ -15,6 +15,18 @@ contract AccrueInterestIntegrationTest is BaseTest { morpho.accrueInterest(marketParamsFuzz); } + function testAccrueInterestIrmZero(MarketParams memory marketParamsFuzz, uint256 blocks) public { + marketParamsFuzz.irm = address(0); + marketParamsFuzz.lltv = 0; + blocks = _boundBlocks(blocks); + + morpho.createMarket(marketParamsFuzz); + + _forward(blocks); + + morpho.accrueInterest(marketParamsFuzz); + } + function testAccrueInterestNoTimeElapsed(uint256 amountSupplied, uint256 amountBorrowed) public { uint256 collateralPrice = oracle.price(); uint256 amountCollateral; From 598c43e41303ec8fc7ec4bd2f3d518a6bf818453 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 8 Dec 2023 17:17:15 +0100 Subject: [PATCH 40/73] fix(lib): skip irm in external lib --- src/libraries/periphery/MorphoBalancesLib.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 8b1c0f815..649da92cf 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -36,13 +36,12 @@ library MorphoBalancesLib { returns (uint256, uint256, uint256, uint256) { Id id = marketParams.id(); - Market memory market = morpho.market(id); uint256 elapsed = block.timestamp - market.lastUpdate; - // Skipped if elapsed == 0 or if totalBorrowAssets == 0 because interest would be null. - if (elapsed != 0 && market.totalBorrowAssets != 0) { + // Skipped if elapsed == 0 or totalBorrowAssets == 0 or irm == address(0) because interest would be null. + if (elapsed != 0 && market.totalBorrowAssets != 0 && marketParams.irm != address(0)) { uint256 borrowRate = IIrm(marketParams.irm).borrowRateView(marketParams, market); uint256 interest = market.totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); market.totalBorrowAssets += interest.toUint128(); From b9fe01c2223191f1ca8e9144dd5e2cde620bdea4 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 09:56:53 +0100 Subject: [PATCH 41/73] fix(accrue-interest): skip address(0) --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 90bb57837..68b819324 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -160,7 +160,7 @@ contract Morpho is IMorphoStaticTyping { emit EventsLib.CreateMarket(id, marketParams); // Call to initialize the IRM in case it is stateful. - IIrm(marketParams.irm).borrowRate(marketParams, market[id]); + if (marketParams.irm != address(0)) IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } /* SUPPLY MANAGEMENT */ From 962b66eba09abc6a119dab098d9ad4e05124a204 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 10:43:30 +0100 Subject: [PATCH 42/73] test(irm): fix irm tests --- test/forge/BaseTest.sol | 6 ++--- .../CreateMarketIntegrationTest.sol | 27 ++++++++++--------- .../integration/OnlyOwnerIntegrationTest.sol | 5 ++-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index ef8c19836..33a8bfa81 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -135,7 +135,7 @@ contract BaseTest is Test { } /// @dev Bounds the fuzzing input to a realistic number of blocks. - function _boundBlocks(uint256 blocks) internal view returns (uint256) { + function _boundBlocks(uint256 blocks) internal pure returns (uint256) { return bound(blocks, 1, type(uint32).max); } @@ -200,7 +200,7 @@ contract BaseTest is Test { return (amountCollateral, amountBorrowed, priceCollateral); } - function _boundTestLltv(uint256 lltv) internal view returns (uint256) { + function _boundTestLltv(uint256 lltv) internal pure returns (uint256) { return bound(lltv, MIN_TEST_LLTV, MAX_TEST_LLTV); } @@ -384,7 +384,7 @@ contract BaseTest is Test { return Math.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv))); } - function _boundValidLltv(uint256 lltv) internal view returns (uint256) { + function _boundValidLltv(uint256 lltv) internal pure returns (uint256) { return bound(lltv, 0, WAD - 1); } diff --git a/test/forge/integration/CreateMarketIntegrationTest.sol b/test/forge/integration/CreateMarketIntegrationTest.sol index 50bfdc38a..352397480 100644 --- a/test/forge/integration/CreateMarketIntegrationTest.sol +++ b/test/forge/integration/CreateMarketIntegrationTest.sol @@ -9,7 +9,7 @@ contract CreateMarketIntegrationTest is BaseTest { using MarketParamsLib for MarketParams; function testCreateMarketWithNotEnabledIrmAndNotEnabledLltv(MarketParams memory marketParamsFuzz) public { - vm.assume(marketParamsFuzz.irm != address(irm) && marketParamsFuzz.lltv != marketParams.lltv); + vm.assume(!morpho.isIrmEnabled(marketParamsFuzz.irm) && !morpho.isLltvEnabled(marketParamsFuzz.lltv)); vm.prank(OWNER); vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); @@ -17,11 +17,12 @@ contract CreateMarketIntegrationTest is BaseTest { } function testCreateMarketWithNotEnabledIrmAndEnabledLltv(MarketParams memory marketParamsFuzz) public { - vm.assume(marketParamsFuzz.irm != address(irm)); + vm.assume(!morpho.isIrmEnabled(marketParamsFuzz.irm)); + marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); vm.startPrank(OWNER); - if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); morpho.createMarket(marketParamsFuzz); @@ -29,10 +30,10 @@ contract CreateMarketIntegrationTest is BaseTest { } function testCreateMarketWithEnabledIrmAndNotEnabledLltv(MarketParams memory marketParamsFuzz) public { - vm.assume(marketParamsFuzz.lltv != marketParams.lltv); + vm.assume(!morpho.isLltvEnabled(marketParamsFuzz.lltv)); vm.startPrank(OWNER); - if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); + if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); vm.expectRevert(bytes(ErrorsLib.LLTV_NOT_ENABLED)); morpho.createMarket(marketParamsFuzz); @@ -44,8 +45,8 @@ contract CreateMarketIntegrationTest is BaseTest { Id marketParamsFuzzId = marketParamsFuzz.id(); vm.startPrank(OWNER); - if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); - if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); + if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); @@ -66,10 +67,12 @@ contract CreateMarketIntegrationTest is BaseTest { marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); vm.startPrank(OWNER); - if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); - if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); + if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + if (marketParamsFuzz.irm != address(0)) { + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + } morpho.createMarket(marketParamsFuzz); @@ -83,8 +86,8 @@ contract CreateMarketIntegrationTest is BaseTest { Id marketParamsFuzzId = marketParamsFuzz.id(); vm.startPrank(OWNER); - if (marketParamsFuzz.irm != marketParams.irm) morpho.enableIrm(marketParamsFuzz.irm); - if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); + if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); diff --git a/test/forge/integration/OnlyOwnerIntegrationTest.sol b/test/forge/integration/OnlyOwnerIntegrationTest.sol index fb7f8c016..0b5ba9817 100644 --- a/test/forge/integration/OnlyOwnerIntegrationTest.sol +++ b/test/forge/integration/OnlyOwnerIntegrationTest.sol @@ -59,7 +59,7 @@ contract OnlyOwnerIntegrationTest is BaseTest { } function testEnableIrm(address irmFuzz) public { - vm.assume(irmFuzz != address(irm)); + vm.assume(!morpho.isIrmEnabled(irmFuzz)); vm.prank(OWNER); vm.expectEmit(true, true, true, true, address(morpho)); @@ -94,7 +94,8 @@ contract OnlyOwnerIntegrationTest is BaseTest { function testEnableLltv(uint256 lltvFuzz) public { lltvFuzz = _boundValidLltv(lltvFuzz); - vm.assume(lltvFuzz != marketParams.lltv); + + vm.assume(!morpho.isLltvEnabled(lltvFuzz)); vm.prank(OWNER); vm.expectEmit(true, true, true, true, address(morpho)); From d10c4e5fb4508c81b67cfe1479d4cf9c92173cbf Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 10:45:55 +0100 Subject: [PATCH 43/73] docs(shares-math): complete warning --- src/libraries/SharesMathLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index ff6bc94b4..34da853d8 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -15,7 +15,7 @@ library SharesMathLib { /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure /// high precision computations. /// @dev Warning: The assets to which virtual borrow shares are entitled behave like unrealizable bad debt, but it - /// is assumed the borrow share price stays low enough to not inflate these assets. + /// is assumed the borrow share price stays low enough not to inflate these assets to a significant value. uint256 internal constant VIRTUAL_SHARES = 1e6; /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is From dfe479e38668c1a05b0af55d90a7dfbb0ef0ab21 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 11:32:31 +0100 Subject: [PATCH 44/73] docs(shares-math): generalize doc --- src/libraries/SharesMathLib.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index 34da853d8..3ed7115b5 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -14,8 +14,9 @@ library SharesMathLib { /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure /// high precision computations. - /// @dev Warning: The assets to which virtual borrow shares are entitled behave like unrealizable bad debt, but it - /// is assumed the borrow share price stays low enough not to inflate these assets to a significant value. + /// @dev Virtual shares can never be redeemed for the assets they are entitled to, but it is assumed the share price + /// stays low enough not to inflate these assets to a significant value. + /// @dev Warning: The assets to which virtual borrow shares are entitled behave like unrealizable bad debt. uint256 internal constant VIRTUAL_SHARES = 1e6; /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is From c1ed84b4510c12703f08565d34a3cac1d1fb5f56 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 11:45:00 +0100 Subject: [PATCH 45/73] test(hardhat): add idle market tests --- test/hardhat/Morpho.spec.ts | 32 +++++++++++++++++++++++++++++++- tsconfig.json | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/test/hardhat/Morpho.spec.ts b/test/hardhat/Morpho.spec.ts index f43d02010..c095924ba 100644 --- a/test/hardhat/Morpho.spec.ts +++ b/test/hardhat/Morpho.spec.ts @@ -1,7 +1,7 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { setNextBlockTimestamp } from "@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time"; import { expect } from "chai"; -import { AbiCoder, MaxUint256, keccak256, toBigInt } from "ethers"; +import { AbiCoder, MaxUint256, ZeroAddress, keccak256, toBigInt } from "ethers"; import hre from "hardhat"; import { Morpho, OracleMock, ERC20Mock, IrmMock } from "types"; import { MarketParamsStruct } from "types/src/Morpho"; @@ -161,6 +161,36 @@ describe("Morpho", () => { } }); + it("should simulate gas cost [idle]", async () => { + updateMarket({ + loanToken: await loanToken.getAddress(), + collateralToken: ZeroAddress, + oracle: ZeroAddress, + irm: ZeroAddress, + lltv: 0, + }); + + await morpho.enableLltv(0); + await morpho.enableIrm(ZeroAddress); + await morpho.createMarket(marketParams); + + for (let i = 0; i < suppliers.length; ++i) { + logProgress("idle", i, suppliers.length); + + const supplier = suppliers[i]; + + let assets = BigInt.WAD * toBigInt(1 + Math.floor(random() * 100)); + + await randomForwardTimestamp(); + + await morpho.connect(supplier).supply(marketParams, assets, 0, supplier.address, "0x"); + + await randomForwardTimestamp(); + + await morpho.connect(supplier).withdraw(marketParams, assets / 2n, 0, supplier.address, supplier.address); + } + }); + it("should simulate gas cost [liquidations]", async () => { for (let i = 0; i < suppliers.length; ++i) { logProgress("liquidations", i, suppliers.length); diff --git a/tsconfig.json b/tsconfig.json index 7d4c1978e..e538e2c1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2020", "module": "nodenext", "moduleResolution": "nodenext", "outDir": "dist", From 70e2636aba706186de8b097e195e5e0e3853f869 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 11:51:12 +0100 Subject: [PATCH 46/73] fix: cantina-670 --- src/Morpho.sol | 2 ++ test/forge/integration/CallbacksIntegrationTest.sol | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/Morpho.sol b/src/Morpho.sol index 830fd421d..b153e8481 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -413,6 +413,8 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function flashLoan(address token, uint256 assets, bytes calldata data) external { + require(assets != 0, ErrorsLib.ZERO_ASSETS); + emit EventsLib.FlashLoan(msg.sender, token, assets); IERC20(token).safeTransfer(msg.sender, assets); diff --git a/test/forge/integration/CallbacksIntegrationTest.sol b/test/forge/integration/CallbacksIntegrationTest.sol index 62578aa19..512522e5e 100644 --- a/test/forge/integration/CallbacksIntegrationTest.sol +++ b/test/forge/integration/CallbacksIntegrationTest.sol @@ -83,6 +83,11 @@ contract CallbacksIntegrationTest is assertEq(loanToken.balanceOf(address(morpho)), amount, "balanceOf"); } + function testFlashLoanZero() public { + vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); + morpho.flashLoan(address(loanToken), 0, abi.encode(this.testFlashLoan.selector, hex"")); + } + function testFlashLoanShouldRevertIfNotReimbursed(uint256 amount) public { amount = bound(amount, 1, MAX_TEST_AMOUNT); From 915cd56c449f99eb197c8ee0bbacc5e162597ea2 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 11:53:44 +0100 Subject: [PATCH 47/73] fix: cantina-686 --- src/interfaces/IIrm.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/interfaces/IIrm.sol b/src/interfaces/IIrm.sol index db2aaf873..bab747b57 100644 --- a/src/interfaces/IIrm.sol +++ b/src/interfaces/IIrm.sol @@ -8,11 +8,12 @@ import {MarketParams, Market} from "./IMorpho.sol"; /// @custom:contact security@morpho.org /// @notice Interface that Interest Rate Models (IRMs) used by Morpho must implement. interface IIrm { - /// @notice Returns the borrow rate of the market `marketParams`. + /// @notice Returns the borrow rate per second (scaled by wad) of the market `marketParams`. /// @dev Assumes that `market` corresponds to `marketParams`. function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256); - /// @notice Returns the borrow rate of the market `marketParams` without modifying any storage. + /// @notice Returns the borrow rate per second (scaled by wad) of the market `marketParams` without modifying any + /// storage. /// @dev Assumes that `market` corresponds to `marketParams`. function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256); } From 429912c66cd7489dc55db14e139b6aae203249d1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 11:57:03 +0100 Subject: [PATCH 48/73] fix: cantina-699 --- src/libraries/EventsLib.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 1630dad56..bc57b511a 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -35,6 +35,7 @@ library EventsLib { event CreateMarket(Id indexed id, MarketParams marketParams); /// @notice Emitted on supply of assets. + /// @dev Warning: `feeRecipient` receives some shares during interest accrual without any supply event emitted. /// @param id The market id. /// @param caller The caller. /// @param onBehalf The owner of the modified position. From 936971d033dcd3f3ca0fefbce2463f25fb66307a Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 11 Dec 2023 11:59:47 +0100 Subject: [PATCH 49/73] test(forge): fix create market tests --- .../CreateMarketIntegrationTest.sol | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/forge/integration/CreateMarketIntegrationTest.sol b/test/forge/integration/CreateMarketIntegrationTest.sol index 352397480..cbf23393a 100644 --- a/test/forge/integration/CreateMarketIntegrationTest.sol +++ b/test/forge/integration/CreateMarketIntegrationTest.sol @@ -11,8 +11,8 @@ contract CreateMarketIntegrationTest is BaseTest { function testCreateMarketWithNotEnabledIrmAndNotEnabledLltv(MarketParams memory marketParamsFuzz) public { vm.assume(!morpho.isIrmEnabled(marketParamsFuzz.irm) && !morpho.isLltvEnabled(marketParamsFuzz.lltv)); - vm.prank(OWNER); vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); } @@ -23,10 +23,11 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); } function testCreateMarketWithEnabledIrmAndNotEnabledLltv(MarketParams memory marketParamsFuzz) public { @@ -34,10 +35,11 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); + vm.stopPrank(); vm.expectRevert(bytes(ErrorsLib.LLTV_NOT_ENABLED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); } function testCreateMarketWithEnabledIrmAndLltv(MarketParams memory marketParamsFuzz) public { @@ -47,13 +49,16 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + if (marketParamsFuzz.irm != address(0)) { + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + } vm.expectEmit(true, true, true, true, address(morpho)); emit EventsLib.CreateMarket(marketParamsFuzz.id(), marketParamsFuzz); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); assertEq(morpho.lastUpdate(marketParamsFuzzId), block.timestamp, "lastUpdate != block.timestamp"); assertEq(morpho.totalSupplyAssets(marketParamsFuzzId), 0, "totalSupplyAssets != 0"); @@ -69,16 +74,18 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); if (marketParamsFuzz.irm != address(0)) { vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); } + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); vm.expectRevert(bytes(ErrorsLib.MARKET_ALREADY_CREATED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); } function testIdToMarketParams(MarketParams memory marketParamsFuzz) public { @@ -88,11 +95,14 @@ contract CreateMarketIntegrationTest is BaseTest { vm.startPrank(OWNER); if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + if (marketParamsFuzz.irm != address(0)) { + vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); + } + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); MarketParams memory params = morpho.idToMarketParams(marketParamsFuzzId); From 94c9f5746866de20b0c2b2d16e81433b4ff8b5c4 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 12:14:19 +0100 Subject: [PATCH 50/73] fix: auth already set --- src/Morpho.sol | 4 ++++ .../AuthorizationIntegrationTest.sol | 18 ++++++++++++++---- .../integration/BorrowIntegrationTest.sol | 4 ++-- .../WithdrawCollateralIntegrationTest.sol | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index b153e8481..863d619b9 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -437,6 +437,10 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external { + require( + authorization.isAuthorized != isAuthorized[authorization.authorizer][authorization.authorized], + ErrorsLib.ALREADY_SET + ); require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED); require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE); diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 69ef8f8de..7c4c25506 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -16,10 +16,19 @@ contract AuthorizationIntegrationTest is BaseTest { assertFalse(morpho.isAuthorized(address(this), addressFuzz)); } - function testAlreadySet(address addressFuzz, bool status) public { - morpho.setAuthorization(addressFuzz, status); + function testAlreadySet(address addressFuzz) public { vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); - morpho.setAuthorization(addressFuzz, status); + morpho.setAuthorization(addressFuzz, false); + morpho.setAuthorization(addressFuzz, true); + vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); + morpho.setAuthorization(addressFuzz, true); + } + + function testAlreadySetWithSig(Authorization memory authorization, Signature memory sig) public { + authorization.isAuthorized = false; + authorization.authorizer = address(this); + morpho.setAuthorization(authorization.authorized, true); + morpho.setAuthorizationWithSig(authorization, sig); } function testSetAuthorizationWithSignatureDeadlineOutdated( @@ -83,6 +92,7 @@ contract AuthorizationIntegrationTest is BaseTest { privateKey = bound(privateKey, 1, type(uint32).max); authorization.nonce = 0; authorization.authorizer = vm.addr(privateKey); + authorization.isAuthorized = true; Signature memory sig; bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); @@ -90,7 +100,7 @@ contract AuthorizationIntegrationTest is BaseTest { morpho.setAuthorizationWithSig(authorization, sig); - assertEq(morpho.isAuthorized(authorization.authorizer, authorization.authorized), authorization.isAuthorized); + assertEq(morpho.isAuthorized(authorization.authorizer, authorization.authorized), true); assertEq(morpho.nonce(authorization.authorizer), 1); } diff --git a/test/forge/integration/BorrowIntegrationTest.sol b/test/forge/integration/BorrowIntegrationTest.sol index 1733c7872..d02a83efb 100644 --- a/test/forge/integration/BorrowIntegrationTest.sol +++ b/test/forge/integration/BorrowIntegrationTest.sol @@ -202,7 +202,7 @@ contract BorrowIntegrationTest is BaseTest { vm.startPrank(ONBEHALF); collateralToken.approve(address(morpho), amountCollateral); morpho.supplyCollateral(marketParams, amountCollateral, ONBEHALF, hex""); - morpho.setAuthorization(BORROWER, true); + // BORROWER is already authorized. vm.stopPrank(); uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0); @@ -248,7 +248,7 @@ contract BorrowIntegrationTest is BaseTest { vm.startPrank(ONBEHALF); collateralToken.approve(address(morpho), amountCollateral); morpho.supplyCollateral(marketParams, amountCollateral, ONBEHALF, hex""); - morpho.setAuthorization(BORROWER, true); + // BORROWER is already authorized. vm.stopPrank(); vm.prank(BORROWER); diff --git a/test/forge/integration/WithdrawCollateralIntegrationTest.sol b/test/forge/integration/WithdrawCollateralIntegrationTest.sol index 8a5b0ce58..c9798d0ad 100644 --- a/test/forge/integration/WithdrawCollateralIntegrationTest.sol +++ b/test/forge/integration/WithdrawCollateralIntegrationTest.sol @@ -145,7 +145,7 @@ contract WithdrawCollateralIntegrationTest is BaseTest { vm.startPrank(ONBEHALF); morpho.supplyCollateral(marketParams, amountCollateral + amountCollateralExcess, ONBEHALF, hex""); - morpho.setAuthorization(BORROWER, true); + // BORROWER is already authorized. morpho.borrow(marketParams, amountBorrowed, 0, ONBEHALF, ONBEHALF); vm.stopPrank(); From 691d3ffdf50b1c5597230518f857223c9658e723 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 12:23:56 +0100 Subject: [PATCH 51/73] test: fix auth already set --- test/forge/integration/AuthorizationIntegrationTest.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 7c4c25506..8047ec32c 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -36,6 +36,7 @@ contract AuthorizationIntegrationTest is BaseTest { uint256 privateKey, uint256 blocks ) public { + authorization.isAuthorized = true; blocks = _boundBlocks(blocks); authorization.deadline = block.timestamp - 1; @@ -55,6 +56,7 @@ contract AuthorizationIntegrationTest is BaseTest { } function testAuthorizationWithSigWrongPK(Authorization memory authorization, uint256 privateKey) public { + authorization.isAuthorized = true; authorization.deadline = bound(authorization.deadline, block.timestamp, type(uint256).max); // Private key must be less than the secp256k1 curve order. @@ -70,6 +72,7 @@ contract AuthorizationIntegrationTest is BaseTest { } function testAuthorizationWithSigWrongNonce(Authorization memory authorization, uint256 privateKey) public { + authorization.isAuthorized = true; authorization.deadline = bound(authorization.deadline, block.timestamp, type(uint256).max); authorization.nonce = bound(authorization.nonce, 1, type(uint256).max); @@ -86,13 +89,13 @@ contract AuthorizationIntegrationTest is BaseTest { } function testAuthorizationWithSig(Authorization memory authorization, uint256 privateKey) public { + authorization.isAuthorized = true; authorization.deadline = bound(authorization.deadline, block.timestamp, type(uint256).max); // Private key must be less than the secp256k1 curve order. privateKey = bound(privateKey, 1, type(uint32).max); authorization.nonce = 0; authorization.authorizer = vm.addr(privateKey); - authorization.isAuthorized = true; Signature memory sig; bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); @@ -105,6 +108,7 @@ contract AuthorizationIntegrationTest is BaseTest { } function testAuthorizationFailsWithReusedSig(Authorization memory authorization, uint256 privateKey) public { + authorization.isAuthorized = true; authorization.deadline = bound(authorization.deadline, block.timestamp, type(uint256).max); // Private key must be less than the secp256k1 curve order. From 91036d494a970084e432ea10ee978d826f234fe1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 13:51:47 +0100 Subject: [PATCH 52/73] test: fix auth already set --- test/forge/integration/AuthorizationIntegrationTest.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 8047ec32c..f5a45183f 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -25,9 +25,12 @@ contract AuthorizationIntegrationTest is BaseTest { } function testAlreadySetWithSig(Authorization memory authorization, Signature memory sig) public { + morpho.setAuthorization(authorization.authorized, true); + authorization.isAuthorized = false; authorization.authorizer = address(this); - morpho.setAuthorization(authorization.authorized, true); + authorization.deadline = block.timestamp - 1; + authorization.nonce = 0; morpho.setAuthorizationWithSig(authorization, sig); } @@ -122,6 +125,7 @@ contract AuthorizationIntegrationTest is BaseTest { morpho.setAuthorizationWithSig(authorization, sig); + authorization.isAuthorized = false; vm.expectRevert(bytes(ErrorsLib.INVALID_NONCE)); morpho.setAuthorizationWithSig(authorization, sig); } From 7dd997a1bb52ab9a63d8606815bd49a807df3dca Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 14:00:08 +0100 Subject: [PATCH 53/73] test: fix auth already set --- test/forge/integration/AuthorizationIntegrationTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index f5a45183f..547ffb24f 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -29,7 +29,7 @@ contract AuthorizationIntegrationTest is BaseTest { authorization.isAuthorized = false; authorization.authorizer = address(this); - authorization.deadline = block.timestamp - 1; + authorization.deadline = block.timestamp; authorization.nonce = 0; morpho.setAuthorizationWithSig(authorization, sig); } From dd43449267a3accc669cf716a74381d6e10d3849 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 14:20:27 +0100 Subject: [PATCH 54/73] test: fix auth already set --- .../integration/AuthorizationIntegrationTest.sol | 12 ++++++++++-- test/forge/invariant/MorphoInvariantTest.sol | 8 +++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 547ffb24f..e7933d19f 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -19,18 +19,26 @@ contract AuthorizationIntegrationTest is BaseTest { function testAlreadySet(address addressFuzz) public { vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); morpho.setAuthorization(addressFuzz, false); + morpho.setAuthorization(addressFuzz, true); + vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); morpho.setAuthorization(addressFuzz, true); } function testAlreadySetWithSig(Authorization memory authorization, Signature memory sig) public { - morpho.setAuthorization(authorization.authorized, true); - authorization.isAuthorized = false; authorization.authorizer = address(this); authorization.deadline = block.timestamp; authorization.nonce = 0; + + vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); + morpho.setAuthorizationWithSig(authorization, sig); + + morpho.setAuthorization(authorization.authorized, true); + + authorization.isAuthorized = true; + vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); morpho.setAuthorizationWithSig(authorization, sig); } diff --git a/test/forge/invariant/MorphoInvariantTest.sol b/test/forge/invariant/MorphoInvariantTest.sol index edb423473..122d5f515 100644 --- a/test/forge/invariant/MorphoInvariantTest.sol +++ b/test/forge/invariant/MorphoInvariantTest.sol @@ -62,15 +62,17 @@ contract MorphoInvariantTest is InvariantTest { } modifier authorized(address onBehalf) { - if (onBehalf != msg.sender) { + if (onBehalf != msg.sender && !morpho.isAuthorized(onBehalf, msg.sender)) { vm.prank(onBehalf); morpho.setAuthorization(msg.sender, true); } _; - vm.prank(onBehalf); - morpho.setAuthorization(msg.sender, false); + if (morpho.isAuthorized(onBehalf, msg.sender)) { + vm.prank(onBehalf); + morpho.setAuthorization(msg.sender, false); + } } function _randomMarket(uint256 marketSeed) internal view returns (MarketParams memory _marketParams) { From 0a7c4602a734697806d8603d5ec65edc6ff2cedc Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 14:36:25 +0100 Subject: [PATCH 55/73] chore: fmt --- src/interfaces/IMorpho.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 9e7564f6d..8cedc984d 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -131,9 +131,8 @@ interface IMorphoBase { /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's /// `onMorphoSupply` function with the given `data`. /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the - /// caller - /// is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific amount - /// of shares is given for full compatibility and precision. + /// caller is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific + /// amount of shares is given for full compatibility and precision. /// @dev If the supply of a market gets depleted, the supply share price instantly resets to /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. /// @dev Supplying a large amount can revert for overflow. @@ -175,9 +174,8 @@ interface IMorphoBase { /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` to `receiver`. /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the - /// caller - /// is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is given for - /// full compatibility and precision. + /// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is + /// given for full compatibility and precision. /// @dev If the borrow of a market gets depleted, the borrow share price instantly resets to /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. From 9bf3dd3f2310ed36f5078e106c896fc1ca6cdb0d Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:56:13 +0100 Subject: [PATCH 56/73] docs: minor improvement Co-authored-by: Romain Milon Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/interfaces/IIrm.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IIrm.sol b/src/interfaces/IIrm.sol index bab747b57..58fb5bf21 100644 --- a/src/interfaces/IIrm.sol +++ b/src/interfaces/IIrm.sol @@ -8,7 +8,7 @@ import {MarketParams, Market} from "./IMorpho.sol"; /// @custom:contact security@morpho.org /// @notice Interface that Interest Rate Models (IRMs) used by Morpho must implement. interface IIrm { - /// @notice Returns the borrow rate per second (scaled by wad) of the market `marketParams`. + /// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams`. /// @dev Assumes that `market` corresponds to `marketParams`. function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256); From 2b052d11140584d39645bd7e4d569930c9894e3b Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 11 Dec 2023 15:04:20 +0100 Subject: [PATCH 57/73] docs: minor improvements --- src/interfaces/IIrm.sol | 2 +- src/interfaces/IMorpho.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interfaces/IIrm.sol b/src/interfaces/IIrm.sol index 58fb5bf21..3de0bc1e8 100644 --- a/src/interfaces/IIrm.sol +++ b/src/interfaces/IIrm.sol @@ -12,7 +12,7 @@ interface IIrm { /// @dev Assumes that `market` corresponds to `marketParams`. function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256); - /// @notice Returns the borrow rate per second (scaled by wad) of the market `marketParams` without modifying any + /// @notice Returns the borrow rate per second (scaled by WAD) of the market `marketParams` without modifying any /// storage. /// @dev Assumes that `market` corresponds to `marketParams`. function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256); diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 71cd58e28..83b12302b 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -91,7 +91,7 @@ interface IMorphoBase { function enableLltv(uint256 lltv) external; /// @notice Sets the `newFee` for the given market `marketParams`. - /// @param newFee The new fee, scaled by wad. + /// @param newFee The new fee, scaled by WAD. /// @dev Warning: The recipient can be the zero address. function setFee(MarketParams memory marketParams, uint256 newFee) external; From cc70d7793a1cdcdaa43ba2ef91153ec38cab9a21 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 12 Dec 2023 09:42:53 +0100 Subject: [PATCH 58/73] style: naming temp --- src/Morpho.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 606552c19..89d6db7d4 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -369,9 +369,9 @@ contract Morpho is IMorphoStaticTyping { ); if (seizedAssets > 0) { - uint256 intermediateVar = + uint256 temp = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - repaidShares = intermediateVar.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + repaidShares = temp.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); } else { seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); From d61c9da5334c9043e9abaf9f77939dfba9ab8ab0 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 12 Dec 2023 10:30:30 +0100 Subject: [PATCH 59/73] refactor(liquidate): rename intermediary var --- src/Morpho.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 606552c19..60baa13e6 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -369,9 +369,11 @@ contract Morpho is IMorphoStaticTyping { ); if (seizedAssets > 0) { - uint256 intermediateVar = - seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - repaidShares = intermediateVar.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE); + + repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesDown( + market[id].totalBorrowAssets, market[id].totalBorrowShares + ); } else { seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); From a7d3ffd86100e86c6bc44fd8fe32c78140313900 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 12 Dec 2023 10:34:02 +0100 Subject: [PATCH 60/73] fix(accrue-interest): update lastUpdate --- src/Morpho.sol | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index deea0b218..5f6d24a4d 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -483,27 +483,28 @@ contract Morpho is IMorphoStaticTyping { /// @dev Accrues interest for the given market `marketParams`. /// @dev Assumes that the inputs `marketParams` and `id` match. function _accrueInterest(MarketParams memory marketParams, Id id) internal { - if (marketParams.irm == address(0)) return; - uint256 elapsed = block.timestamp - market[id].lastUpdate; if (elapsed == 0) return; - uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]); - uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); - market[id].totalBorrowAssets += interest.toUint128(); - market[id].totalSupplyAssets += interest.toUint128(); - - uint256 feeShares; - if (market[id].fee != 0) { - uint256 feeAmount = interest.wMulDown(market[id].fee); - // The fee amount is subtracted from the total supply in this calculation to compensate for the fact - // that total supply is already increased by the full interest (including the fee amount). - feeShares = feeAmount.toSharesDown(market[id].totalSupplyAssets - feeAmount, market[id].totalSupplyShares); - position[id][feeRecipient].supplyShares += feeShares; - market[id].totalSupplyShares += feeShares.toUint128(); - } + if (marketParams.irm != address(0)) { + uint256 borrowRate = IIrm(marketParams.irm).borrowRate(marketParams, market[id]); + uint256 interest = market[id].totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); + market[id].totalBorrowAssets += interest.toUint128(); + market[id].totalSupplyAssets += interest.toUint128(); + + uint256 feeShares; + if (market[id].fee != 0) { + uint256 feeAmount = interest.wMulDown(market[id].fee); + // The fee amount is subtracted from the total supply in this calculation to compensate for the fact + // that total supply is already increased by the full interest (including the fee amount). + feeShares = + feeAmount.toSharesDown(market[id].totalSupplyAssets - feeAmount, market[id].totalSupplyShares); + position[id][feeRecipient].supplyShares += feeShares; + market[id].totalSupplyShares += feeShares.toUint128(); + } - emit EventsLib.AccrueInterest(id, borrowRate, interest, feeShares); + emit EventsLib.AccrueInterest(id, borrowRate, interest, feeShares); + } // Safe "unchecked" cast. market[id].lastUpdate = uint128(block.timestamp); From 5ae9ff2df79cfba38cabc5bfc784b3b01ee3a0e8 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 12 Dec 2023 11:41:51 +0100 Subject: [PATCH 61/73] docs(balances-lib): update comment --- src/libraries/periphery/MorphoBalancesLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 649da92cf..94b3b85f9 100644 --- a/src/libraries/periphery/MorphoBalancesLib.sol +++ b/src/libraries/periphery/MorphoBalancesLib.sol @@ -40,7 +40,7 @@ library MorphoBalancesLib { uint256 elapsed = block.timestamp - market.lastUpdate; - // Skipped if elapsed == 0 or totalBorrowAssets == 0 or irm == address(0) because interest would be null. + // Skipped if elapsed == 0 or totalBorrowAssets == 0 because interest would be null, or if irm == address(0). if (elapsed != 0 && market.totalBorrowAssets != 0 && marketParams.irm != address(0)) { uint256 borrowRate = IIrm(marketParams.irm).borrowRateView(marketParams, market); uint256 interest = market.totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); From 75c07c48313c991b4899468e918b605435df12f4 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 13 Dec 2023 18:16:14 +0100 Subject: [PATCH 62/73] fix(foundry.toml): decrease optimization runs --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index d641b9154..4b0da7556 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ names = true sizes = true via-ir = true -optimizer_runs = 4294967295 +optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs. [profile.default.invariant] runs = 8 From 54d8af2bb466e04cfc5ab3b943bddcbc80f7a797 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 18 Dec 2023 11:49:34 +0100 Subject: [PATCH 63/73] fix(test): remove outdated tech --- test/forge/BaseTest.sol | 8 +- .../integration/BorrowIntegrationTest.sol | 2 +- test/morpho_tests.tree | 288 ------------------ 3 files changed, 5 insertions(+), 293 deletions(-) delete mode 100644 test/morpho_tests.tree diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index 33a8bfa81..f00917261 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -95,19 +95,19 @@ contract BaseTest is Test { loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); - changePrank(BORROWER); + vm.startPrank(BORROWER); loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); - changePrank(REPAYER); + vm.startPrank(REPAYER); loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); - changePrank(LIQUIDATOR); + vm.startPrank(LIQUIDATOR); loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); - changePrank(ONBEHALF); + vm.startPrank(ONBEHALF); loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); morpho.setAuthorization(BORROWER, true); diff --git a/test/forge/integration/BorrowIntegrationTest.sol b/test/forge/integration/BorrowIntegrationTest.sol index d02a83efb..7b44ef931 100644 --- a/test/forge/integration/BorrowIntegrationTest.sol +++ b/test/forge/integration/BorrowIntegrationTest.sol @@ -55,7 +55,7 @@ contract BorrowIntegrationTest is BaseTest { collateralToken.approve(address(morpho), amountCollateral); morpho.supplyCollateral(marketParams, amountCollateral, supplier, hex""); - changePrank(attacker); + vm.startPrank(attacker); vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); morpho.borrow(marketParams, amountBorrowed, 0, supplier, RECEIVER); } diff --git a/test/morpho_tests.tree b/test/morpho_tests.tree deleted file mode 100644 index a2faa9950..000000000 --- a/test/morpho_tests.tree +++ /dev/null @@ -1,288 +0,0 @@ -. -└── setOwner(address newOwner) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── it should set owner to newOwner - └── it should emit SetOwner(newOwner) -. -└── enableIrm(address irm) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── it should set isIrmEnabled[irm] to true - └── it should emit EnableIrm(irm) -. -└── enableLltv(uint256 lltv) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when lltv >= WAD - │ └── revert with MAX_LLTV_EXCEEDED - └── when lltv < WAD - ├── it should set isLltvEnabled[lltv] to true - └── it should emit EnableLltv(lltv) -. -└── setFee(MarketParams memory marketParams, uint256 newFee) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when newFee > MAX_FEE - │ └── revert with MAX_FEE_EXCEEDED - └── when newFee <= MAX_FEE - ├── it should accrue the interest - ├── it should set fee[marketParams.id] to newFee - └── it should emit SetFee(marketParams.id, newFee) -. -└── setFeeRecipient(address recipient) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── it should set feeRecipient to recipient - └── it should emit SetFeeRecipient(recipient) -. -└── createMarket(MarketParams memory marketParams) external - ├── when irm is not enabled - │ └── revert with IRM_NOT_ENABLED - └── when irm is enabled - ├── when marketParams.lltv is not enabled - │ └── revert with LLTV_NOT_ENABLED - └── when marketParams.lltv is enabled - ├── when market is already created - │ └── revert with MARKET_ALREADY_CREATED - └── when market is not already created - ├── it should set lastUpdate[marketParams.id] to block.timestamp - ├── it should set idToMarket[id] to marketParams - └── it should emit CreateMarket(marketParams.id, marketParams) -. -└── supply(MarketParams memory marketParams, uint256 assets, uint256 shares, address onBehalf, bytes calldata data) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when both assets and shares are null or both assets and shares are not null - │ └─ revert with INCONSISTENT_INPUT - └── when one of assets or shares is null and one of assets or shares is not null - ├── when onBehalf is the zero address - │ └── revert with ZERO_ADDRESS - └── when onBehalf is not the zero address - ├── it should accrue the interest - ├── when assets is not zero - │ └── it should set shares to assets.toSharesUp(totalSupplyAssets[marketParams.id], totalSupplyShares[marketParams.id]) - ├── when assets is zero - │ └── it should set assets to shares.toAssetsDown(totalSupplyAssets[id], totalSupplyShares[id]) - ├── it should add shares to supplyShares[marketParams.id][onBehalf] - ├── it should add shares to totalSupplyShares[marketParams.id] - ├── it should add assets to totalSupplyAssets[marketParams.id] - ├── it should emit Supply(marketParams.id, msg.sender, onBehalf, assets, shares) - ├── if data.length > 0 - │ └── it should call sender's onMorphoSupply callback - ├── it should transfer assets of the loan asset from the sender to Morpho - └── it should return the assets and the shares supplied -. -└── withdraw(MarketParams memory marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when both assets and shares are null or both assets and shares are not null - │ └─ revert with INCONSISTENT_INPUT - └── when one of assets or shares is null and one of assets or shares is not null - ├── when receiver is the zero address - │ └── revert with ZERO_ADDRESS - └── when receiver is not the zero address - ├── when not sender and not approved - │ └── revert with UNAUTHORIZED - └── when sender or approved - ├── it should accrue the interest - ├── when assets is not zero - │ └── it should set shares to assets.toSharesUp(totalSupplyAssets[marketParams.id], totalSupplyShares[marketParams.id]) - ├── when assets is zero - │ └── it should set assets to shares.toAssetsDown(totalSupplyAssets[id], totalSupplyShares[id]) - ├── it should remove shares from supplyShares[marketParams.id][onBehalf] - ├── it should remove shares from totalSupplyShares[marketParams.id] - ├── it should remove assets from totalSupplyAssets[marketParams.id] - ├── it should emit Withdraw(marketParams.id, msg.sender, onBehalf, receiver, assets, shares) - ├── it should transfer assets of the loan asset to the receiver - ├── when totalBorrowAssets[marketParams.id] > totalSupplyAssets[marketParams.id] - │ └── revert with INSUFFICIENT_LIQUIDITY - └── when totalBorrowAssets[marketParams.id] <= totalSupplyAssets[marketParams.id] - └── it should return the assets and the shares withdrawn -. -└── borrow(MarketParams memory marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when both assets and shares are null or both assets and shares are not null - │ └─ revert with INCONSISTENT_INPUT - └── when one of assets or shares is null and one of assets or shares is not null - ├── when receiver is the zero address - │ └── revert with ZERO_ADDRESS - └── when receiver is not the zero address - ├── when not sender and not approved - │ └── revert with UNAUTHORIZED - └── when sender or approved - ├── it should accrue the interest - ├── when assets is not zero - │ └── it should set shares to assets.toSharesUp(totalSupplyAssets[marketParams.id], totalSupplyShares[marketParams.id]) - ├── when assets is zero - │ └── it should set assets to shares.toAssetsDown(totalSupplyAssets[id], totalSupplyShares[id]) - ├── it should add shares to borrowShares[marketParams.id][onBehalf] - ├── it should add shares to totalBorrowShares[marketParams.id] - ├── it should add assets to totalBorrowAssets[marketParams.id] - ├── it should emit Borrow(marketParams.id, msg.sender, onBehalf, receiver, assets, shares) - ├── it should transfer assets of the loan asset to the receiver - ├── when position is not healthy - │ └── revert with INSUFFICIENT_COLLATERAL - └── when position is healthy - ├── when totalBorrowAssets[marketParams.id] > totalSupplyAssets[marketParams.id] - │ └── revert with INSUFFICIENT_LIQUIDITY - └── when totalBorrowAssets[marketParams.id] <= totalSupplyAssets[marketParams.id] - └── it should return the assets and the shares borrowed - -. -└── repay(MarketParams memory marketParams, uint256 assets, uint256 shares, address onBehalf, bytes calldata data) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when both assets and shares are null or both assets and shares are not null - │ └─ revert with INCONSISTENT_INPUT - └── when one of assets or shares is null and one of assets or shares is not null - ├── when onBehalf is the zero address - │ └── revert with ZERO_ADDRESS - └── when onBehalf is not the zero address - ├── it should accrue the interest - ├── when assets is not zero - │ └── it should set shares to assets.toSharesUp(totalSupplyAssets[marketParams.id], totalSupplyShares[marketParams.id]) - ├── when assets is zero - │ └── it should set assets to shares.toAssetsDown(totalSupplyAssets[id], totalSupplyShares[id]) - ├── it should remove shares from borrowShares[marketParams.id][onBehalf] - ├── it should remove shares from totalBorrowShares[marketParams.id] - ├── it should remove assets from totalBorrowAssets[marketParams.id] - ├── it should emit Repay(marketParams.id, msg.sender, onBehalf, assets, shares) - ├── if data.length > 0 - │ └── it should call sender's onMorphoRepay callback - ├── it should transfer assets of the loan asset from the sender to Morpho - └── it should return the assets and the shares repaid -. -└── supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes calldata data) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when the assets to supply is zero - │ └── revert with ZERO_ASSETS - └── when the assets to supply is not zero - ├── when onBehalf is the zero address - │ └── revert with ZERO_ADDRESS - └── when onBehalf is not the zero address - ├── it should add assets to collateral[marketParams.id][onBehalf] - ├── it should emit SupplyCollateral(marketParams.id, msg.sender, onBehalf, assets) - ├── if data.length > 0 - │ └── it should call sender's onMorphoSupplyCollateral callback - └── it should transfer assets of the collateral asset from the sender to Morpho -. -└── withdrawCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, address receiver) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when the assets to withdraw is zero - │ └── revert with ZERO_ASSETS - └── when the assets to withdraw is not zero - ├── when receiver is the zero address - │ └── revert with ZERO_ADDRESS - └── when receiver is not the zero address - ├── when not sender and not approved - │ └── revert with MANAGER_NOT_APPROVED - └── when sender or approved - ├── it should accrue the interest - ├── it should remove assets from collateral[marketParams.id][onBehalf] - ├── it should emit WithdrawCollateral(marketParams.id, msg.sender, onBehalf, receiver, assets) - ├── it should transfer assets of the collateral asset to the receiver - └── when position is not healthy - └── revert with INSUFFICIENT_COLLATERAL -. -└── liquidate(MarketParams memory marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes calldata data) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - ├── when both seizedAssets and repaidShares are null or both seizedAssets and repaidShares are not null - │ └─ revert with INCONSISTENT_INPUT - └── when one of seizedAssets or repaidShares is null and one of seizedAssets or repaidShares is not null - ├── it should accrue the interest - ├── when position is healthy - │ └── revert with HEALTHY_POSITION - └── when the position is not healthy - ├── when seizedAssets is not zero - │ ├── it should compute assetsRepaid = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor(marketParams.lltv)) - │ └── it should compute repaidShares = assetsRepaid.toSharesDown(totalBorrow[marketParams.id], totalBorrowShares[market.id]) - ├── when repaidShares is not zero - │ ├── it should compute assetsRepaid = repaidShares.toAssetsUp(totalBorrow[marketParams.id], totalBorrowShares[marketParams.id]) - │ └── it should compute seizedAssets = assetsRepaid.wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice) - ├── it should remove repaidShares from totalBorrowShares[marketParams.id] - ├── it should remove assetsRepaid from totalBorrow[marketParams.id] - ├── it should remove repaidShares from collateral[marketParams.id][borrower] - ├── if after the liquidation the borrower's collateral is 0 - │ └── it should realize bad debt - │ ├── it should compute badDebt = borrowShares[marketParams.id][borrower].toAssetsUp(totalBorrow[marketParams.id], totalBorrowShares[marketParams.id]) - │ ├── it should remove badDebt from totalSupplyAssets[marketParams.id] - │ ├── it should remove badDebt from totalBorrowAssets[marketParams.id] - │ ├── it should remove borrowShares[marketParams.id][borrower] from totalBorrowShares[marketParams.id] - │ └── it should set borrowShares[marketParams.id][borrower] to 0 - ├── it should transfer repaidShares of collateral asset to the sender - ├── it should emit Liquidate(marketParams.id, msg.sender, borrower, assetsRepaid, repaidShares, seizedAssets, badDebtShares) - ├── if data.length > 0 - │ └── it should call sender's onMorphoLiquidate callback - └── it should transfer assetsRepaid of loan asset from the sender to Morpho -. -└── flashLoan(address token, uint256 assets, bytes calldata data) external - ├── it should transfer assets of token from Morpho to the sender - ├── it should call sender's onMorphoFlashLoan callback - ├── it should emit FlashLoan(msg.sender, token, assets) - └── it should transfer assets of token from the sender to Morpho -. -└── setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external - ├── when block.timestamp > authorization.deadline - │ └── revert with SIGNATURE_EXPIRED - └── when block.timestamp <= deadline - ├── when authorization.nonce != nonce[authorization.authorizer] - │ └── revert with INVALID_NONCE - └── when authorization.nonce == nonce[authorization.authorizer] - ├── when the signature is invalid or not signed by authorization.authorizer - │ └── revert with INVALID_SIGNATURE - └── when the signature is valid and signed by authorization.authorizer - ├── it should increment authorization.authorizer's nonce - ├── it should emit IncrementNonce(msg.sender, authorization.authorizer, authorization.nonce) - ├── it should set isAuthorized[authorization.authorizer][authorization.authorized] to authorization.isAuthorized - └── it should emit SetAuthorization(msg.sender, authorization.authorizer, authorization.authorized, authorization.isAuthorized) -. -└── setAuthorization(address authorized, bool newIsAuthorized) external - ├── should set isApproved[msg.sender][authorized] to newIsAuthorized - └── it should emit SetAuthorization(msg.sender, msg.sender, authorized, newIsAuthorized) -. -└── accrueInterest(MarketParams memory marketParams) external - ├── when market is not created - │ └── revert with MARKET_NOT_CREATED - └── when market is created - └── it should accrue the interest -. -└── _accrueInterest(MarketParams memory marketParams, Id id) internal - └── when interest not already accrued in the block - ├── it should set lastUpdate to block.timestamp - └── when marketTotalBorrow is not 0 - ├── it should compute accruedInterest = marketTotalBorrow.wMulDown(borrowRate.wTaylorCompounded(elapsed)) - ├── it should add accruedInterest to totalBorrowAssets - ├── it should add accruedInterest to totalSupplyAssets - └── when fee[id] != 0 - │ ├── it should add accruedInterest.wMulDown(fee[id]) to feeAmount - │ ├── it should add feeAmount.mulDivDown(totalSupplyShares[id], totalSupplyAssets[id] - feeAmount) to supplyShares[id][feeRecipient] - │ └── it should add feeAmount.mulDivDown(totalSupplyShares[id], totalSupplyAssets[id] - feeAmount) to totalSupplyShares[id] - └── it should emit AccrueInterest(id, borrowRate, accruedInterest, feeShares) -. -└── _isHealthy(MarketParams memory marketParams, Id id, address user, uint256 collateralPrice) internal - ├── it should compute borrowed = borrowShares[id][user].toAssetsUp(totalBorrowAssets[id], totalBorrowShares[id]) - ├── it should compute maxBorrow = collateral[id][user].mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv) - └── it should return maxBorrow >= borrowed -. -└── liquidationIncentiveFactor(uint256 lltv) internal - └── it should return min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv))) From acf96da5bfdcb0a2ce55debc9a2fc01d99ac8ce8 Mon Sep 17 00:00:00 2001 From: Jean-Grimal Date: Mon, 18 Dec 2023 17:26:33 +0100 Subject: [PATCH 64/73] fix: natspec on borrow and withdraw --- src/interfaces/IMorpho.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index 83b12302b..9c82524cb 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -151,7 +151,7 @@ interface IMorphoBase { bytes memory data ) external returns (uint256 assetsSupplied, uint256 sharesSupplied); - /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` to `receiver`. + /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. /// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`. /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. /// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow. @@ -172,7 +172,7 @@ interface IMorphoBase { address receiver ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn); - /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` to `receiver`. + /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the /// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is /// given for full compatibility and precision. @@ -228,7 +228,7 @@ interface IMorphoBase { function supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes memory data) external; - /// @notice Withdraws `assets` of collateral on behalf of `onBehalf` to `receiver`. + /// @notice Withdraws `assets` of collateral on behalf of `onBehalf` and sends the assets to `receiver`. /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. /// @dev Withdrawing an amount corresponding to more collateral than supplied will revert for underflow. /// @param marketParams The market to withdraw collateral from. From 1d1a4163d3e92d1d82e0f7aa0082d7baf620857f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 18 Dec 2023 18:41:22 +0100 Subject: [PATCH 65/73] fix(authorization): revert already set error --- src/Morpho.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 72ae6b74c..01b430bc4 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -444,10 +444,6 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external { - require( - authorization.isAuthorized != isAuthorized[authorization.authorizer][authorization.authorized], - ErrorsLib.ALREADY_SET - ); require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED); require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE); From 7b49523f08dc0c7efd53af2fe2dd879e2a1a9025 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 18 Dec 2023 18:53:39 +0100 Subject: [PATCH 66/73] refactor: clear rounding through incentive --- src/Morpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 72ae6b74c..1ef72cc45 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -371,7 +371,7 @@ contract Morpho is IMorphoStaticTyping { if (seizedAssets > 0) { uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE); - repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesDown( + repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesUp( market[id].totalBorrowAssets, market[id].totalBorrowShares ); } else { From 94981de053c980b4d9fc553918f59444ab431b00 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Mon, 18 Dec 2023 18:54:03 +0100 Subject: [PATCH 67/73] refactor: repay incentive temp variable --- src/Morpho.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index 1ef72cc45..b085a5fab 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -375,8 +375,11 @@ contract Morpho is IMorphoStaticTyping { market[id].totalBorrowAssets, market[id].totalBorrowShares ); } else { - seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) - .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); + uint256 repayIncentive = repaidShares.toAssetsDown( + market[id].totalBorrowAssets, market[id].totalBorrowShares + ).wMulDown(liquidationIncentiveFactor); + + seizedAssets = repayIncentive.mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); } } uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); From c34a8016c090859dcd7c4aba62ac95475d77f35d Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 19 Dec 2023 09:59:13 +0100 Subject: [PATCH 68/73] test(irm): dont fuzz irm --- .../CreateMarketIntegrationTest.sol | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/test/forge/integration/CreateMarketIntegrationTest.sol b/test/forge/integration/CreateMarketIntegrationTest.sol index cbf23393a..db675543f 100644 --- a/test/forge/integration/CreateMarketIntegrationTest.sol +++ b/test/forge/integration/CreateMarketIntegrationTest.sol @@ -19,12 +19,6 @@ contract CreateMarketIntegrationTest is BaseTest { function testCreateMarketWithNotEnabledIrmAndEnabledLltv(MarketParams memory marketParamsFuzz) public { vm.assume(!morpho.isIrmEnabled(marketParamsFuzz.irm)); - marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); - - vm.startPrank(OWNER); - if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); - vm.stopPrank(); - vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); @@ -43,18 +37,14 @@ contract CreateMarketIntegrationTest is BaseTest { } function testCreateMarketWithEnabledIrmAndLltv(MarketParams memory marketParamsFuzz) public { + marketParamsFuzz.irm = address(irm); marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); Id marketParamsFuzzId = marketParamsFuzz.id(); vm.startPrank(OWNER); - if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.stopPrank(); - if (marketParamsFuzz.irm != address(0)) { - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); - } - vm.expectEmit(true, true, true, true, address(morpho)); emit EventsLib.CreateMarket(marketParamsFuzz.id(), marketParamsFuzz); vm.prank(OWNER); @@ -69,17 +59,13 @@ contract CreateMarketIntegrationTest is BaseTest { } function testCreateMarketAlreadyCreated(MarketParams memory marketParamsFuzz) public { + marketParamsFuzz.irm = address(irm); marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); vm.startPrank(OWNER); - if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.stopPrank(); - if (marketParamsFuzz.irm != address(0)) { - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); - } - vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); @@ -89,18 +75,14 @@ contract CreateMarketIntegrationTest is BaseTest { } function testIdToMarketParams(MarketParams memory marketParamsFuzz) public { + marketParamsFuzz.irm = address(irm); marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); Id marketParamsFuzzId = marketParamsFuzz.id(); vm.startPrank(OWNER); - if (!morpho.isIrmEnabled(marketParamsFuzz.irm)) morpho.enableIrm(marketParamsFuzz.irm); if (!morpho.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); vm.stopPrank(); - if (marketParamsFuzz.irm != address(0)) { - vm.mockCall(marketParamsFuzz.irm, abi.encodeWithSelector(IIrm.borrowRate.selector), abi.encode(0)); - } - vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); From 47aaecf4185787dc589a85157ff9f651f3985da7 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 20 Dec 2023 10:03:31 +0100 Subject: [PATCH 69/73] fix: depth 3 for AccrueInterest --- certora/confs/AccrueInterest.conf | 2 ++ certora/confs/AssetsAccounting.conf | 1 + certora/confs/ConsistentState.conf | 1 + certora/confs/ExactMath.conf | 1 + certora/confs/Health.conf | 1 + certora/confs/LibSummary.conf | 1 + certora/confs/Liveness.conf | 1 + certora/confs/RatioMath.conf | 1 + certora/confs/Reentrancy.conf | 1 + certora/confs/Reverts.conf | 1 + certora/confs/Transfer.conf | 1 + 11 files changed, 12 insertions(+) diff --git a/certora/confs/AccrueInterest.conf b/certora/confs/AccrueInterest.conf index a923c628e..8f8c26d72 100644 --- a/certora/confs/AccrueInterest.conf +++ b/certora/confs/AccrueInterest.conf @@ -4,9 +4,11 @@ ], "verify": "MorphoHarness:certora/specs/AccrueInterest.spec", "prover_args": [ + "-depth 3", "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" ], "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Accrue Interest" } diff --git a/certora/confs/AssetsAccounting.conf b/certora/confs/AssetsAccounting.conf index 8bdc141ec..6dcd57dc6 100644 --- a/certora/confs/AssetsAccounting.conf +++ b/certora/confs/AssetsAccounting.conf @@ -4,5 +4,6 @@ ], "verify": "MorphoHarness:certora/specs/AssetsAccounting.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Assets Accounting" } diff --git a/certora/confs/ConsistentState.conf b/certora/confs/ConsistentState.conf index 9691cfcb1..fdfa6e17a 100644 --- a/certora/confs/ConsistentState.conf +++ b/certora/confs/ConsistentState.conf @@ -4,5 +4,6 @@ ], "verify": "MorphoHarness:certora/specs/ConsistentState.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Consistent State" } diff --git a/certora/confs/ExactMath.conf b/certora/confs/ExactMath.conf index 26a0d6d2b..947e98bb4 100644 --- a/certora/confs/ExactMath.conf +++ b/certora/confs/ExactMath.conf @@ -8,5 +8,6 @@ "-smt_hashingScheme plaininjectivity", "-mediumTimeout 30" ], + "server": "production", "msg": "Morpho Blue Exact Math" } diff --git a/certora/confs/Health.conf b/certora/confs/Health.conf index a75fa1bb6..f361bf43d 100644 --- a/certora/confs/Health.conf +++ b/certora/confs/Health.conf @@ -8,5 +8,6 @@ "prover_args": [ "-smt_hashingScheme plaininjectivity" ], + "server": "production", "msg": "Morpho Blue Health" } diff --git a/certora/confs/LibSummary.conf b/certora/confs/LibSummary.conf index 4aebed846..f931810eb 100644 --- a/certora/confs/LibSummary.conf +++ b/certora/confs/LibSummary.conf @@ -4,5 +4,6 @@ ], "verify": "MorphoHarness:certora/specs/LibSummary.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Lib Summary" } diff --git a/certora/confs/Liveness.conf b/certora/confs/Liveness.conf index 2659b17b1..211a0897a 100644 --- a/certora/confs/Liveness.conf +++ b/certora/confs/Liveness.conf @@ -4,5 +4,6 @@ ], "verify": "MorphoInternalAccess:certora/specs/Liveness.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Liveness" } diff --git a/certora/confs/RatioMath.conf b/certora/confs/RatioMath.conf index 700c436e2..9062f1b14 100644 --- a/certora/confs/RatioMath.conf +++ b/certora/confs/RatioMath.conf @@ -9,5 +9,6 @@ "-mediumTimeout 30", "-timeout 3600" ], + "server": "production", "msg": "Morpho Blue Ratio Math" } diff --git a/certora/confs/Reentrancy.conf b/certora/confs/Reentrancy.conf index 0fe902cfb..84019dceb 100644 --- a/certora/confs/Reentrancy.conf +++ b/certora/confs/Reentrancy.conf @@ -7,5 +7,6 @@ "prover_args": [ "-enableStorageSplitting false" ], + "server": "production", "msg": "Morpho Blue Reentrancy" } diff --git a/certora/confs/Reverts.conf b/certora/confs/Reverts.conf index 92ce70bff..6992b6b4b 100644 --- a/certora/confs/Reverts.conf +++ b/certora/confs/Reverts.conf @@ -4,5 +4,6 @@ ], "verify": "MorphoHarness:certora/specs/Reverts.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Reverts" } diff --git a/certora/confs/Transfer.conf b/certora/confs/Transfer.conf index ac1bdeee1..6425f56a5 100644 --- a/certora/confs/Transfer.conf +++ b/certora/confs/Transfer.conf @@ -7,5 +7,6 @@ ], "verify": "TransferHarness:certora/specs/Transfer.spec", "rule_sanity": "basic", + "server": "production", "msg": "Morpho Blue Transfer" } From 65cf2c0e1dc001c02d1bfbf489ec43cf208ed34f Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 20 Dec 2023 10:29:08 +0100 Subject: [PATCH 70/73] test: remove useless test --- .../forge/integration/AuthorizationIntegrationTest.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index e7933d19f..8115c7a32 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -16,16 +16,6 @@ contract AuthorizationIntegrationTest is BaseTest { assertFalse(morpho.isAuthorized(address(this), addressFuzz)); } - function testAlreadySet(address addressFuzz) public { - vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); - morpho.setAuthorization(addressFuzz, false); - - morpho.setAuthorization(addressFuzz, true); - - vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); - morpho.setAuthorization(addressFuzz, true); - } - function testAlreadySetWithSig(Authorization memory authorization, Signature memory sig) public { authorization.isAuthorized = false; authorization.authorizer = address(this); From 3d55307d51897fa2690b4c427bad5b96b65f7b3a Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 20 Dec 2023 10:37:43 +0100 Subject: [PATCH 71/73] test: remove useless test --- .../integration/AuthorizationIntegrationTest.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 8115c7a32..dd9e2440d 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -16,20 +16,14 @@ contract AuthorizationIntegrationTest is BaseTest { assertFalse(morpho.isAuthorized(address(this), addressFuzz)); } - function testAlreadySetWithSig(Authorization memory authorization, Signature memory sig) public { - authorization.isAuthorized = false; - authorization.authorizer = address(this); - authorization.deadline = block.timestamp; - authorization.nonce = 0; - + function testAlreadySet(address addressFuzz) public { vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); - morpho.setAuthorizationWithSig(authorization, sig); + morpho.setAuthorization(addressFuzz, false); - morpho.setAuthorization(authorization.authorized, true); + morpho.setAuthorization(addressFuzz, true); - authorization.isAuthorized = true; vm.expectRevert(bytes(ErrorsLib.ALREADY_SET)); - morpho.setAuthorizationWithSig(authorization, sig); + morpho.setAuthorization(addressFuzz, true); } function testSetAuthorizationWithSignatureDeadlineOutdated( From 90fb045df81a093659780e0e7d2b00d8c6d659b4 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Wed, 20 Dec 2023 10:42:53 +0100 Subject: [PATCH 72/73] refactor: revert repayIncentive temp variable --- src/Morpho.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Morpho.sol b/src/Morpho.sol index b085a5fab..1ef72cc45 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -375,11 +375,8 @@ contract Morpho is IMorphoStaticTyping { market[id].totalBorrowAssets, market[id].totalBorrowShares ); } else { - uint256 repayIncentive = repaidShares.toAssetsDown( - market[id].totalBorrowAssets, market[id].totalBorrowShares - ).wMulDown(liquidationIncentiveFactor); - - seizedAssets = repayIncentive.mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); + seizedAssets = repaidShares.toAssetsDown(market[id].totalBorrowAssets, market[id].totalBorrowShares) + .wMulDown(liquidationIncentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice); } } uint256 repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares); From b2279f2cbd55baa5a19892541e44b466b2801127 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 20 Dec 2023 11:45:11 +0100 Subject: [PATCH 73/73] docs(authorization): comment already set --- src/Morpho.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Morpho.sol b/src/Morpho.sol index 01b430bc4..4ef3996a9 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -444,6 +444,7 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function setAuthorizationWithSig(Authorization memory authorization, Signature calldata signature) external { + /// Do not check whether authorization is already set because the nonce increment is a desired side effect. require(block.timestamp <= authorization.deadline, ErrorsLib.SIGNATURE_EXPIRED); require(authorization.nonce == nonce[authorization.authorizer]++, ErrorsLib.INVALID_NONCE);