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 }} 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" } 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. 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 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/src/Morpho.sol b/src/Morpho.sol index f755904b6..3b2811c44 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 to initialize the IRM in case it is stateful. + if (marketParams.irm != address(0)) IIrm(marketParams.irm).borrowRate(marketParams, market[id]); } /* SUPPLY MANAGEMENT */ @@ -354,12 +357,11 @@ contract Morpho is IMorphoStaticTyping { _accrueInterest(marketParams, id); - uint256 collateralPrice = IOracle(marketParams.oracle).price(); + { + uint256 collateralPrice = IOracle(marketParams.oracle).price(); - require(!_isHealthy(marketParams, id, borrower, collateralPrice), ErrorsLib.HEALTHY_POSITION); + 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, @@ -367,15 +369,17 @@ contract Morpho is IMorphoStaticTyping { ); if (seizedAssets > 0) { - repaidAssets = - seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - repaidShares = repaidAssets.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares); + uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE); + + repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentiveFactor).toSharesUp( + 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); + 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(); @@ -384,23 +388,26 @@ 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; } - 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 + ); + + IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets); if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data); @@ -413,10 +420,12 @@ contract Morpho is IMorphoStaticTyping { /// @inheritdoc IMorphoBase function flashLoan(address token, uint256 assets, bytes calldata data) external { - IERC20(token).safeTransfer(msg.sender, assets); + require(assets != 0, ErrorsLib.ZERO_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); @@ -426,6 +435,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); @@ -433,11 +444,12 @@ 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); 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); @@ -470,25 +482,27 @@ contract Morpho is IMorphoStaticTyping { /// @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 (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); diff --git a/src/interfaces/IIrm.sol b/src/interfaces/IIrm.sol index db2aaf873..3de0bc1e8 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); } diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index b928e50ad..9c82524cb 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 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); @@ -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; @@ -129,12 +130,12 @@ 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 - /// 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 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 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. @@ -150,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. @@ -171,14 +172,14 @@ interface IMorphoBase { address receiver ) 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 - /// 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`. + /// @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. /// @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. @@ -200,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. @@ -226,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. @@ -242,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. 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/EventsLib.sol b/src/libraries/EventsLib.sol index 825ea1791..2ff9b829d 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -35,9 +35,10 @@ 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 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 +46,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 +62,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 +78,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 +86,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( @@ -106,7 +107,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 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, @@ -114,6 +116,7 @@ library EventsLib { uint256 repaidAssets, uint256 repaidShares, uint256 seizedAssets, + uint256 badDebtAssets, uint256 badDebtShares ); 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/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index 514760698..3ed7115b5 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -14,6 +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 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 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/src/libraries/periphery/MorphoBalancesLib.sol b/src/libraries/periphery/MorphoBalancesLib.sol index 3afabfd5f..94b3b85f9 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 of if totalBorrowAssets == 0 because interest would be null. - if (elapsed != 0 && market.totalBorrowAssets != 0) { + // 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)); market.totalBorrowAssets += interest.toUint128(); @@ -90,8 +89,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 +104,8 @@ 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 may be greater than the market's expected total borrow + /// assets. function expectedBorrowAssets(IMorpho morpho, MarketParams memory marketParams, address user) internal view diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index a26dab28e..f00917261 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(); @@ -93,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); @@ -133,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); } @@ -198,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); } @@ -382,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/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) { 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; diff --git a/test/forge/integration/AuthorizationIntegrationTest.sol b/test/forge/integration/AuthorizationIntegrationTest.sol index 263eaa550..dd9e2440d 100644 --- a/test/forge/integration/AuthorizationIntegrationTest.sol +++ b/test/forge/integration/AuthorizationIntegrationTest.sol @@ -16,11 +16,22 @@ 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 testSetAuthorizationWithSignatureDeadlineOutdated( Authorization memory authorization, uint256 privateKey, uint256 blocks ) public { + authorization.isAuthorized = true; blocks = _boundBlocks(blocks); authorization.deadline = block.timestamp - 1; @@ -40,6 +51,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. @@ -55,6 +67,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); @@ -71,6 +84,7 @@ 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. @@ -84,11 +98,12 @@ 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); } 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. @@ -102,6 +117,7 @@ contract AuthorizationIntegrationTest is BaseTest { morpho.setAuthorizationWithSig(authorization, sig); + authorization.isAuthorized = false; vm.expectRevert(bytes(ErrorsLib.INVALID_NONCE)); morpho.setAuthorizationWithSig(authorization, sig); } diff --git a/test/forge/integration/BorrowIntegrationTest.sol b/test/forge/integration/BorrowIntegrationTest.sol index 1733c7872..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); } @@ -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/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); diff --git a/test/forge/integration/CreateMarketIntegrationTest.sol b/test/forge/integration/CreateMarketIntegrationTest.sol index 1fd2eb083..db675543f 100644 --- a/test/forge/integration/CreateMarketIntegrationTest.sol +++ b/test/forge/integration/CreateMarketIntegrationTest.sol @@ -9,48 +9,46 @@ 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)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); } function testCreateMarketWithNotEnabledIrmAndEnabledLltv(MarketParams memory marketParamsFuzz) public { - vm.assume(marketParamsFuzz.irm != address(irm)); - marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); - - vm.startPrank(OWNER); - if (marketParamsFuzz.lltv != marketParams.lltv) morpho.enableLltv(marketParamsFuzz.lltv); + vm.assume(!morpho.isIrmEnabled(marketParamsFuzz.irm)); vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); } 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.stopPrank(); vm.expectRevert(bytes(ErrorsLib.LLTV_NOT_ENABLED)); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); } function testCreateMarketWithEnabledIrmAndLltv(MarketParams memory marketParamsFuzz) public { + marketParamsFuzz.irm = address(irm); marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); 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.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); 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"); @@ -61,28 +59,32 @@ contract CreateMarketIntegrationTest is BaseTest { } function testCreateMarketAlreadyCreated(MarketParams memory marketParamsFuzz) public { + marketParamsFuzz.irm = address(irm); 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.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); + + 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 { + marketParamsFuzz.irm = address(irm); marketParamsFuzz.lltv = _boundValidLltv(marketParamsFuzz.lltv); 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.isLltvEnabled(marketParamsFuzz.lltv)) morpho.enableLltv(marketParamsFuzz.lltv); + vm.stopPrank(); + vm.prank(OWNER); morpho.createMarket(marketParamsFuzz); - vm.stopPrank(); MarketParams memory params = morpho.idToMarketParams(marketParamsFuzzId); diff --git a/test/forge/integration/LiquidateIntegrationTest.sol b/test/forge/integration/LiquidateIntegrationTest.sol index 0fb9325c9..874591d6a 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; @@ -206,15 +206,15 @@ 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); 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) = @@ -353,4 +354,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"); + } } 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)); 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/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(); 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) { 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); } 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/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))) 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",