From ffc14a26703d7b2197879e8096d4c4c057203e9d Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 5 Dec 2022 19:34:56 -0500 Subject: [PATCH 001/105] Remove compound math from contracts --- contracts/compound/InterestRatesManager.sol | 7 ++++--- contracts/compound/MorphoUtils.sol | 4 ++-- contracts/compound/PositionsManager.sol | 19 ++++++++++--------- contracts/compound/RewardsManager.sol | 8 +++++--- test-foundry/compound/TestRepay.t.sol | 4 ++-- test-foundry/compound/setup/TestSetup.sol | 2 +- test-foundry/compound/setup/Utils.sol | 2 +- .../prod/compound/setup/TestSetup.sol | 2 +- 8 files changed, 26 insertions(+), 22 deletions(-) diff --git a/contracts/compound/InterestRatesManager.sol b/contracts/compound/InterestRatesManager.sol index bc991702d..de117bdec 100644 --- a/contracts/compound/InterestRatesManager.sol +++ b/contracts/compound/InterestRatesManager.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.13; import "./interfaces/IInterestRatesManager.sol"; import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; -import "./libraries/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/Math.sol"; import "./MorphoStorage.sol"; @@ -136,7 +137,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { if (_params.delta.p2pSupplyAmount == 0 || _params.delta.p2pSupplyDelta == 0) { newP2PSupplyIndex = _params.lastP2PSupplyIndex.mul(p2pSupplyGrowthFactor); } else { - uint256 shareOfTheDelta = CompoundMath.min( + uint256 shareOfTheDelta = Math.min( (_params.delta.p2pSupplyDelta.mul(_params.lastPoolSupplyIndex)).div( (_params.delta.p2pSupplyAmount).mul(_params.lastP2PSupplyIndex) ), @@ -154,7 +155,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { if (_params.delta.p2pBorrowAmount == 0 || _params.delta.p2pBorrowDelta == 0) { newP2PBorrowIndex = _params.lastP2PBorrowIndex.mul(p2pBorrowGrowthFactor); } else { - uint256 shareOfTheDelta = CompoundMath.min( + uint256 shareOfTheDelta = Math.min( (_params.delta.p2pBorrowDelta.mul(_params.lastPoolBorrowIndex)).div( (_params.delta.p2pBorrowAmount).mul(_params.lastP2PBorrowIndex) ), diff --git a/contracts/compound/MorphoUtils.sol b/contracts/compound/MorphoUtils.sol index de401fab9..394cee9b5 100644 --- a/contracts/compound/MorphoUtils.sol +++ b/contracts/compound/MorphoUtils.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.13; import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; -import "./libraries/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/Math.sol"; +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; import "@morpho-dao/morpho-utils/DelegateCall.sol"; import "./MorphoStorage.sol"; diff --git a/contracts/compound/PositionsManager.sol b/contracts/compound/PositionsManager.sol index 2d2086617..ad1c9312c 100644 --- a/contracts/compound/PositionsManager.sol +++ b/contracts/compound/PositionsManager.sol @@ -14,6 +14,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { using DoubleLinkedList for DoubleLinkedList.List; using SafeTransferLib for ERC20; using CompoundMath for uint256; + using Math for uint256; /// EVENTS /// @@ -557,10 +558,10 @@ contract PositionsManager is IPositionsManager, MatchingEngine { _amount = Math.min( _amount, Math.min( - deltas.p2pSupplyAmount.mul(p2pSupplyIndex[_poolToken]).safeSub( + deltas.p2pSupplyAmount.mul(p2pSupplyIndex[_poolToken]).zeroFloorSub( deltas.p2pSupplyDelta.mul(poolSupplyIndex) ), - deltas.p2pBorrowAmount.mul(p2pBorrowIndex[_poolToken]).safeSub( + deltas.p2pBorrowAmount.mul(p2pBorrowIndex[_poolToken]).zeroFloorSub( deltas.p2pBorrowDelta.mul(lastPoolIndexes.lastBorrowPoolIndex) ) ) @@ -647,7 +648,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { Types.Delta storage delta = deltas[_poolToken]; vars.p2pSupplyIndex = p2pSupplyIndex[_poolToken]; - supplierSupplyBalance.inP2P -= CompoundMath.min( + supplierSupplyBalance.inP2P -= Math.min( supplierSupplyBalance.inP2P, vars.remainingToWithdraw.div(vars.p2pSupplyIndex) ); // In peer-to-peer supply unit. @@ -775,7 +776,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { if (vars.maxToRepayOnPool > vars.remainingToRepay) { vars.toRepay = vars.remainingToRepay; - borrowerBorrowBalance.onPool -= CompoundMath.min( + borrowerBorrowBalance.onPool -= Math.min( vars.borrowedOnPool, vars.toRepay.div(vars.poolBorrowIndex) ); // In cdUnit. @@ -806,7 +807,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { vars.p2pSupplyIndex = p2pSupplyIndex[_poolToken]; vars.p2pBorrowIndex = p2pBorrowIndex[_poolToken]; - borrowerBorrowBalance.inP2P -= CompoundMath.min( + borrowerBorrowBalance.inP2P -= Math.min( borrowerBorrowBalance.inP2P, vars.remainingToRepay.div(vars.p2pBorrowIndex) ); // In peer-to-peer borrow unit. @@ -835,15 +836,15 @@ contract PositionsManager is IPositionsManager, MatchingEngine { if (vars.remainingToRepay > 0) { // Fee = (p2pBorrowAmount - p2pBorrowDelta) - (p2pSupplyAmount - p2pSupplyDelta). // No need to subtract p2pBorrowDelta as it is zero. - vars.feeToRepay = CompoundMath.safeSub( + vars.feeToRepay = Math.zeroFloorSub( delta.p2pBorrowAmount.mul(vars.p2pBorrowIndex), - delta.p2pSupplyAmount.mul(vars.p2pSupplyIndex).safeSub( + delta.p2pSupplyAmount.mul(vars.p2pSupplyIndex).zeroFloorSub( delta.p2pSupplyDelta.mul(ICToken(_poolToken).exchangeRateStored()) ) ); if (vars.feeToRepay > 0) { - uint256 feeRepaid = CompoundMath.min(vars.feeToRepay, vars.remainingToRepay); + uint256 feeRepaid = Math.min(vars.feeToRepay, vars.remainingToRepay); vars.remainingToRepay -= feeRepaid; delta.p2pBorrowAmount -= feeRepaid.div(vars.p2pBorrowIndex); emit P2PAmountsUpdated(_poolToken, delta.p2pSupplyAmount, delta.p2pBorrowAmount); @@ -935,7 +936,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _amount The amount of token (in underlying). function _withdrawFromPool(address _poolToken, uint256 _amount) internal { // Withdraw only what is possible. The remaining dust is taken from the contract balance. - _amount = CompoundMath.min(ICToken(_poolToken).balanceOfUnderlying(address(this)), _amount); + _amount = Math.min(ICToken(_poolToken).balanceOfUnderlying(address(this)), _amount); if (ICToken(_poolToken).redeemUnderlying(_amount) != 0) revert RedeemOnCompoundFailed(); if (_poolToken == cEth) IWETH(address(wEth)).deposit{value: _amount}(); // Turn the ETH received in wETH. } diff --git a/contracts/compound/RewardsManager.sol b/contracts/compound/RewardsManager.sol index 9d8990612..9d295e8bb 100644 --- a/contracts/compound/RewardsManager.sol +++ b/contracts/compound/RewardsManager.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.13; import "./interfaces/IRewardsManager.sol"; import "./interfaces/IMorpho.sol"; -import "./libraries/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -14,6 +15,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /// @notice This contract is used to manage the COMP rewards from the Compound protocol. contract RewardsManager is IRewardsManager, Initializable { using CompoundMath for uint256; + using SafeCast for uint256; /// STORAGE /// @@ -224,7 +226,7 @@ contract RewardsManager is IRewardsManager, Initializable { localCompSupplyState[_cTokenAddress] = IComptroller.CompMarketState({ index: newCompSupplyIndex, - block: CompoundMath.safe32(block.number) + block: block.number.toUint32() }); } } @@ -258,7 +260,7 @@ contract RewardsManager is IRewardsManager, Initializable { localCompBorrowState[_cTokenAddress] = IComptroller.CompMarketState({ index: newCompBorrowIndex, - block: CompoundMath.safe32(block.number) + block: block.number.toUint32() }); } } diff --git a/test-foundry/compound/TestRepay.t.sol b/test-foundry/compound/TestRepay.t.sol index 06602432a..e9ed56e89 100644 --- a/test-foundry/compound/TestRepay.t.sol +++ b/test-foundry/compound/TestRepay.t.sol @@ -418,7 +418,7 @@ contract TestRepay is TestSetup { function testDeltaRepay() public { // Allows only 10 unmatch suppliers. - setDefaultMaxGasForMatchingHelper(3e6, 3e6, 3e6, 1e6); + setDefaultMaxGasForMatchingHelper(3e6, 3e6, 3e6, 0.9e6); uint256 suppliedAmount = 1 ether; uint256 borrowedAmount = 20 * suppliedAmount; @@ -607,7 +607,7 @@ contract TestRepay is TestSetup { function testDeltaRepayAll() public { // Allows only 10 unmatch suppliers. - setDefaultMaxGasForMatchingHelper(3e6, 3e6, 3e6, 1e6); + setDefaultMaxGasForMatchingHelper(3e6, 3e6, 3e6, 0.9e6); uint256 suppliedAmount = 1 ether; uint256 borrowedAmount = 20 * suppliedAmount + 1e12; diff --git a/test-foundry/compound/setup/TestSetup.sol b/test-foundry/compound/setup/TestSetup.sol index 73b0e1cd2..89e997390 100644 --- a/test-foundry/compound/setup/TestSetup.sol +++ b/test-foundry/compound/setup/TestSetup.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "@contracts/compound/interfaces/IMorpho.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; -import "@contracts/compound/libraries/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; import {IncentivesVault} from "@contracts/compound/IncentivesVault.sol"; diff --git a/test-foundry/compound/setup/Utils.sol b/test-foundry/compound/setup/Utils.sol index d5570d565..6cc94dd7a 100644 --- a/test-foundry/compound/setup/Utils.sol +++ b/test-foundry/compound/setup/Utils.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity ^0.8.0; -import "@contracts/compound/libraries/CompoundMath.sol"; +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; import "@forge-std/Test.sol"; diff --git a/test-foundry/prod/compound/setup/TestSetup.sol b/test-foundry/prod/compound/setup/TestSetup.sol index e73fb282d..b4ee8ff27 100644 --- a/test-foundry/prod/compound/setup/TestSetup.sol +++ b/test-foundry/prod/compound/setup/TestSetup.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity ^0.8.0; -import {CompoundMath} from "@contracts/compound/libraries/CompoundMath.sol"; +import {CompoundMath} from "@morpho-dao/morpho-utils/math/CompoundMath.sol"; import {PercentageMath} from "@morpho-dao/morpho-utils/math/PercentageMath.sol"; import {SafeTransferLib, ERC20} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; import {Math} from "@morpho-dao/morpho-utils/math/Math.sol"; From 2e63774795b11d3c0ded8c95fda2833ac1927b15 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 7 Dec 2022 17:27:34 -0500 Subject: [PATCH 002/105] Reduce coverage for compound lib --- .github/actions/ci-foundry/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci-foundry/action.yml b/.github/actions/ci-foundry/action.yml index f8c06ad52..13c2e3700 100644 --- a/.github/actions/ci-foundry/action.yml +++ b/.github/actions/ci-foundry/action.yml @@ -70,7 +70,7 @@ runs: - name: Check coverage threshold if: ${{ inputs.codecovToken != '' }} - run: npx lcov-total lcov.info --gte=90.3 + run: npx lcov-total lcov.info --gte=90 shell: bash - name: Upload coverage to Codecov From 14d758a453094653ac93c29809d6576f912d5ecd Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 8 Dec 2022 12:46:49 +0100 Subject: [PATCH 003/105] feat: cache storage pointers in enter/leave market if needed --- src/compound/PositionsManager.sol | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 2d2086617..7016ef037 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -978,8 +978,9 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _user The address of the user to update. /// @param _poolToken The address of the market to check. function _enterMarketIfNeeded(address _poolToken, address _user) internal { - if (!userMembership[_poolToken][_user]) { - userMembership[_poolToken][_user] = true; + mapping(address => bool) storage userMembership = userMembership[_poolToken]; + if (!userMembership[_user]) { + userMembership[_user] = true; enteredMarkets[_user].push(_poolToken); } } @@ -988,25 +989,29 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _user The address of the user to update. /// @param _poolToken The address of the market to check. function _leaveMarketIfNeeded(address _poolToken, address _user) internal { + Types.SupplyBalance memory supplyBalance = supplyBalanceInOf[_poolToken][_user]; + Types.BorrowBalance memory borrowBalance = borrowBalanceInOf[_poolToken][_user]; + mapping(address => bool) storage userMembership = userMembership[_poolToken]; if ( - userMembership[_poolToken][_user] && - supplyBalanceInOf[_poolToken][_user].inP2P == 0 && - supplyBalanceInOf[_poolToken][_user].onPool == 0 && - borrowBalanceInOf[_poolToken][_user].inP2P == 0 && - borrowBalanceInOf[_poolToken][_user].onPool == 0 + userMembership[_user] && + supplyBalance.inP2P == 0 && + supplyBalance.onPool == 0 && + borrowBalance.inP2P == 0 && + borrowBalance.onPool == 0 ) { + address[] storage enteredMarkets = enteredMarkets[_user]; uint256 index; - while (enteredMarkets[_user][index] != _poolToken) { + while (enteredMarkets[index] != _poolToken) { unchecked { ++index; } } - userMembership[_poolToken][_user] = false; - uint256 length = enteredMarkets[_user].length; - if (index != length - 1) - enteredMarkets[_user][index] = enteredMarkets[_user][length - 1]; - enteredMarkets[_user].pop(); + userMembership[_user] = false; + + uint256 length = enteredMarkets.length; + if (index != length - 1) enteredMarkets[index] = enteredMarkets[length - 1]; + enteredMarkets.pop(); } } From ad0be5708f34293898423e331a2ec4fccd107b93 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 8 Dec 2022 13:09:50 +0100 Subject: [PATCH 004/105] refactor: memory to storage --- src/compound/PositionsManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 7016ef037..e69a46f8a 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -989,8 +989,8 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _user The address of the user to update. /// @param _poolToken The address of the market to check. function _leaveMarketIfNeeded(address _poolToken, address _user) internal { - Types.SupplyBalance memory supplyBalance = supplyBalanceInOf[_poolToken][_user]; - Types.BorrowBalance memory borrowBalance = borrowBalanceInOf[_poolToken][_user]; + Types.SupplyBalance storage supplyBalance = supplyBalanceInOf[_poolToken][_user]; + Types.BorrowBalance storage borrowBalance = borrowBalanceInOf[_poolToken][_user]; mapping(address => bool) storage userMembership = userMembership[_poolToken]; if ( userMembership[_user] && From b418a7d2c5ac6c9a1638573e3b56839a5f5452ca Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 8 Dec 2022 18:34:47 +0100 Subject: [PATCH 005/105] docs: unmote -> demote --- src/compound/PositionsManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 2d2086617..fbb5eecbb 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -877,7 +877,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// Breaking repay /// - // Unmote peer-to-peer suppliers. + // Demote peer-to-peer suppliers. if (vars.remainingToRepay > 0) { uint256 unmatched = _unmatchSuppliers( _poolToken, From f81700e8035ad9c5f898b80d01b61ef0e8d090b2 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 8 Dec 2022 18:39:56 +0100 Subject: [PATCH 006/105] test: add failing test for interface --- test/compound/TestMorphoGetters.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/compound/TestMorphoGetters.t.sol b/test/compound/TestMorphoGetters.t.sol index 213f42c96..8de655f1d 100644 --- a/test/compound/TestMorphoGetters.t.sol +++ b/test/compound/TestMorphoGetters.t.sol @@ -113,6 +113,7 @@ contract TestMorphoGetters is TestSetup { borrower1.supply(cUsdc, to6Decimals(10 ether)); assertEq(morpho.enteredMarkets(address(borrower1), 0), cDai); + assertEq(IMorpho(address(morpho)).enteredMarkets(address(borrower1), 0), cDai); // test the interface assertEq(morpho.enteredMarkets(address(borrower1), 1), cUsdc); // Borrower1 withdraw, USDC should be the first in enteredMarkets. From a7820f60cdb2d5c9cc8adb94cbd76213bac20b55 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 8 Dec 2022 18:41:01 +0100 Subject: [PATCH 007/105] fix: fix enteredMarkets interface --- src/compound/interfaces/IMorpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compound/interfaces/IMorpho.sol b/src/compound/interfaces/IMorpho.sol index 44a0bfb46..69c514772 100644 --- a/src/compound/interfaces/IMorpho.sol +++ b/src/compound/interfaces/IMorpho.sol @@ -22,7 +22,7 @@ interface IMorpho { function dustThreshold() external view returns (uint256); function supplyBalanceInOf(address, address) external view returns (Types.SupplyBalance memory); function borrowBalanceInOf(address, address) external view returns (Types.BorrowBalance memory); - function enteredMarkets(address) external view returns (address); + function enteredMarkets(address, uint256) external view returns (address); function deltas(address) external view returns (Types.Delta memory); function marketParameters(address) external view returns (Types.MarketParameters memory); function marketPauseStatus(address) external view returns (Types.MarketPauseStatus memory); From c953e8151d5943ad1012b6be7ffa087383235df5 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 8 Dec 2022 18:57:54 +0100 Subject: [PATCH 008/105] Refactor Morpho-AaveV2 IRM --- src/aave-v2/InterestRatesManager.sol | 161 ++++++++----------- src/aave-v2/libraries/InterestRatesModel.sol | 5 +- test/aave-v2/TestInterestRates.t.sol | 30 ++-- 3 files changed, 78 insertions(+), 118 deletions(-) diff --git a/src/aave-v2/InterestRatesManager.sol b/src/aave-v2/InterestRatesManager.sol index 65ab57fd8..25b5409b6 100644 --- a/src/aave-v2/InterestRatesManager.sol +++ b/src/aave-v2/InterestRatesManager.sol @@ -4,9 +4,8 @@ pragma solidity 0.8.13; import "./interfaces/aave/IAToken.sol"; import "./interfaces/lido/ILido.sol"; -import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; +import "./libraries/InterestRatesModel.sol"; import "@morpho-dao/morpho-utils/math/WadRayMath.sol"; -import "@morpho-dao/morpho-utils/math/Math.sol"; import "./MorphoStorage.sol"; @@ -16,7 +15,6 @@ import "./MorphoStorage.sol"; /// @notice Smart contract handling the computation of indexes used for peer-to-peer interactions. /// @dev This contract inherits from MorphoStorage so that Morpho can delegate calls to this contract. contract InterestRatesManager is IInterestRatesManager, MorphoStorage { - using PercentageMath for uint256; using WadRayMath for uint256; /// STRUCTS /// @@ -26,8 +24,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { uint256 lastP2PBorrowIndex; // The peer-to-peer borrow index at last update. uint256 poolSupplyIndex; // The current pool supply index. uint256 poolBorrowIndex; // The current pool borrow index. - uint256 lastPoolSupplyIndex; // The pool supply index at last update. - uint256 lastPoolBorrowIndex; // The pool borrow index at last update. + Types.PoolIndexes lastPoolIndexes; // The pool indexes at last update. uint256 reserveFactor; // The reserve factor percentage (10 000 = 100%). uint256 p2pIndexCursor; // The peer-to-peer index cursor (10 000 = 100%). Types.Delta delta; // The deltas and peer-to-peer amounts. @@ -58,35 +55,23 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { if (block.timestamp == marketPoolIndexes.lastUpdateTimestamp) return; - Types.Market storage market = market[_poolToken]; - - address underlyingToken = market.underlyingToken; - uint256 newPoolSupplyIndex = pool.getReserveNormalizedIncome(underlyingToken); - uint256 newPoolBorrowIndex = pool.getReserveNormalizedVariableDebt(underlyingToken); - - if (underlyingToken == ST_ETH) { - uint256 stEthRebaseIndex = ILido(ST_ETH).getPooledEthByShares(WadRayMath.RAY); - newPoolSupplyIndex = newPoolSupplyIndex.rayMul(stEthRebaseIndex).rayDiv( - ST_ETH_BASE_REBASE_INDEX - ); - newPoolBorrowIndex = newPoolBorrowIndex.rayMul(stEthRebaseIndex).rayDiv( - ST_ETH_BASE_REBASE_INDEX - ); - } - - Params memory params = Params( - p2pSupplyIndex[_poolToken], - p2pBorrowIndex[_poolToken], - newPoolSupplyIndex, - newPoolBorrowIndex, - marketPoolIndexes.poolSupplyIndex, - marketPoolIndexes.poolBorrowIndex, - market.reserveFactor, - market.p2pIndexCursor, - deltas[_poolToken] + Types.Market memory market = market[_poolToken]; + (uint256 newPoolSupplyIndex, uint256 newPoolBorrowIndex) = _getPoolIndexes( + market.underlyingToken ); - (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes(params); + (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes( + Params( + p2pSupplyIndex[_poolToken], + p2pBorrowIndex[_poolToken], + newPoolSupplyIndex, + newPoolBorrowIndex, + marketPoolIndexes, + market.reserveFactor, + market.p2pIndexCursor, + deltas[_poolToken] + ) + ); p2pSupplyIndex[_poolToken] = newP2PSupplyIndex; p2pBorrowIndex[_poolToken] = newP2PBorrowIndex; @@ -106,6 +91,26 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { /// INTERNAL /// + /// @notice Returns the current pool indexes. + /// @param _underlyingToken The address of the underlying token. + /// @return poolSupplyIndex The pool supply index. + /// @return poolBorrowIndex The pool borrow index. + function _getPoolIndexes(address _underlyingToken) + internal + view + returns (uint256 poolSupplyIndex, uint256 poolBorrowIndex) + { + poolSupplyIndex = pool.getReserveNormalizedIncome(_underlyingToken); + poolBorrowIndex = pool.getReserveNormalizedVariableDebt(_underlyingToken); + + if (_underlyingToken == ST_ETH) { + uint256 rebaseIndex = ILido(ST_ETH).getPooledEthByShares(WadRayMath.RAY); + + poolSupplyIndex = poolSupplyIndex.rayMul(rebaseIndex).rayDiv(ST_ETH_BASE_REBASE_INDEX); + poolBorrowIndex = poolBorrowIndex.rayMul(rebaseIndex).rayDiv(ST_ETH_BASE_REBASE_INDEX); + } + } + /// @notice Computes and returns new peer-to-peer indexes. /// @param _params Computation parameters. /// @return newP2PSupplyIndex The updated p2pSupplyIndex. @@ -115,74 +120,34 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { pure returns (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) { - // Compute pool growth factors + InterestRatesModel.GrowthFactors memory growthFactors = InterestRatesModel + .computeGrowthFactors( + _params.poolSupplyIndex, + _params.poolBorrowIndex, + _params.lastPoolIndexes, + _params.p2pIndexCursor, + _params.reserveFactor + ); - uint256 poolSupplyGrowthFactor = _params.poolSupplyIndex.rayDiv( - _params.lastPoolSupplyIndex + newP2PSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, + p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, + lastPoolIndex: _params.lastPoolIndexes.poolSupplyIndex, + lastP2PIndex: _params.lastP2PSupplyIndex, + p2pDelta: _params.delta.p2pSupplyDelta, + p2pAmount: _params.delta.p2pSupplyAmount + }) ); - uint256 poolBorrowGrowthFactor = _params.poolBorrowIndex.rayDiv( - _params.lastPoolBorrowIndex + newP2PBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, + p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, + lastPoolIndex: _params.lastPoolIndexes.poolBorrowIndex, + lastP2PIndex: _params.lastP2PBorrowIndex, + p2pDelta: _params.delta.p2pBorrowDelta, + p2pAmount: _params.delta.p2pBorrowAmount + }) ); - - // Compute peer-to-peer growth factors. - - uint256 p2pSupplyGrowthFactor; - uint256 p2pBorrowGrowthFactor; - if (poolSupplyGrowthFactor <= poolBorrowGrowthFactor) { - uint256 p2pGrowthFactor = PercentageMath.weightedAvg( - poolSupplyGrowthFactor, - poolBorrowGrowthFactor, - _params.p2pIndexCursor - ); - - p2pSupplyGrowthFactor = - p2pGrowthFactor - - (p2pGrowthFactor - poolSupplyGrowthFactor).percentMul(_params.reserveFactor); - p2pBorrowGrowthFactor = - p2pGrowthFactor + - (poolBorrowGrowthFactor - p2pGrowthFactor).percentMul(_params.reserveFactor); - } else { - // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone has done a flashloan on Aave, or because the interests - // generated by the stable rate borrowing are high (making the supply rate higher than the variable borrow rate). In this case the peer-to-peer - // growth factors are set to the pool borrow growth factor. - p2pSupplyGrowthFactor = poolBorrowGrowthFactor; - p2pBorrowGrowthFactor = poolBorrowGrowthFactor; - } - - // Compute new peer-to-peer supply index. - - if (_params.delta.p2pSupplyAmount == 0 || _params.delta.p2pSupplyDelta == 0) { - newP2PSupplyIndex = _params.lastP2PSupplyIndex.rayMul(p2pSupplyGrowthFactor); - } else { - uint256 shareOfTheDelta = Math.min( - (_params.delta.p2pSupplyDelta.rayMul(_params.lastPoolSupplyIndex)).rayDiv( - _params.delta.p2pSupplyAmount.rayMul(_params.lastP2PSupplyIndex) - ), // Using ray division of an amount in underlying decimals by an amount in underlying decimals yields a value in ray. - WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. - ); // In ray. - - newP2PSupplyIndex = _params.lastP2PSupplyIndex.rayMul( - (WadRayMath.RAY - shareOfTheDelta).rayMul(p2pSupplyGrowthFactor) + - shareOfTheDelta.rayMul(poolSupplyGrowthFactor) - ); - } - - // Compute new peer-to-peer borrow index. - - if (_params.delta.p2pBorrowAmount == 0 || _params.delta.p2pBorrowDelta == 0) { - newP2PBorrowIndex = _params.lastP2PBorrowIndex.rayMul(p2pBorrowGrowthFactor); - } else { - uint256 shareOfTheDelta = Math.min( - (_params.delta.p2pBorrowDelta.rayMul(_params.lastPoolBorrowIndex)).rayDiv( - _params.delta.p2pBorrowAmount.rayMul(_params.lastP2PBorrowIndex) - ), // Using ray division of an amount in underlying decimals by an amount in underlying decimals yields a value in ray. - WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. - ); // In ray. - - newP2PBorrowIndex = _params.lastP2PBorrowIndex.rayMul( - (WadRayMath.RAY - shareOfTheDelta).rayMul(p2pBorrowGrowthFactor) + - shareOfTheDelta.rayMul(poolBorrowGrowthFactor) - ); - } } } diff --git a/src/aave-v2/libraries/InterestRatesModel.sol b/src/aave-v2/libraries/InterestRatesModel.sol index e2740c018..a170fe559 100644 --- a/src/aave-v2/libraries/InterestRatesModel.sol +++ b/src/aave-v2/libraries/InterestRatesModel.sol @@ -75,8 +75,9 @@ library InterestRatesModel { p2pGrowthFactor + (growthFactors.poolBorrowGrowthFactor - p2pGrowthFactor).percentMul(_reserveFactor); } else { - // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone has done a flashloan on Aave: - // the peer-to-peer growth factors are set to the pool borrow growth factor. + // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone has done a flashloan on Aave, or because the interests + // generated by the stable rate borrowing are high (making the supply rate higher than the variable borrow rate). In this case the peer-to-peer + // growth factors are set to the pool borrow growth factor. growthFactors.p2pSupplyGrowthFactor = growthFactors.poolBorrowGrowthFactor; growthFactors.p2pBorrowGrowthFactor = growthFactors.poolBorrowGrowthFactor; } diff --git a/test/aave-v2/TestInterestRates.t.sol b/test/aave-v2/TestInterestRates.t.sol index 0003eacb4..0e0f6d9b0 100644 --- a/test/aave-v2/TestInterestRates.t.sol +++ b/test/aave-v2/TestInterestRates.t.sol @@ -16,8 +16,8 @@ contract TestInterestRates is InterestRatesManager, Test { uint256 public p2pBorrowIndexTest = 1 * RAY; uint256 public poolSupplyIndexTest = 2 * RAY; uint256 public poolBorrowIndexTest = 3 * RAY; - uint256 public lastPoolSupplyIndexTest = 1 * RAY; - uint256 public lastPoolBorrowIndexTest = 1 * RAY; + Types.PoolIndexes public lastPoolIndexesTest = + Types.PoolIndexes(uint32(block.timestamp), uint112(1 * RAY), uint112(1 * RAY)); uint256 public reserveFactor0PerCentTest = 0; uint256 public reserveFactor50PerCentTest = 5_000; uint256 public p2pIndexCursorTest = 3_333; @@ -28,15 +28,15 @@ contract TestInterestRates is InterestRatesManager, Test { pure returns (uint256 p2pSupplyIndex_, uint256 p2pBorrowIndex_) { - uint256 poolSupplyGrowthFactor = _params.poolSupplyIndex.rayDiv(_params.lastPoolSupplyIndex); - uint256 poolBorrowGrowthFactor = _params.poolBorrowIndex.rayDiv(_params.lastPoolBorrowIndex); + uint256 poolSupplyGrowthFactor = _params.poolSupplyIndex.rayDiv(_params.lastPoolIndexes.poolSupplyIndex); + uint256 poolBorrowGrowthFactor = _params.poolBorrowIndex.rayDiv(_params.lastPoolIndexes.poolBorrowIndex); uint256 p2pGrowthFactor = (poolSupplyGrowthFactor.percentMul(PercentageMath.PERCENTAGE_FACTOR - _params.p2pIndexCursor) + poolBorrowGrowthFactor.percentMul(_params.p2pIndexCursor)); uint256 shareOfTheSupplyDelta = _params.delta.p2pBorrowAmount > 0 - ? (_params.delta.p2pSupplyDelta.rayMul(_params.lastPoolSupplyIndex)).rayDiv( + ? (_params.delta.p2pSupplyDelta.rayMul(_params.lastPoolIndexes.poolSupplyIndex)).rayDiv( _params.delta.p2pSupplyAmount.rayMul(_params.lastP2PSupplyIndex)) : 0; uint256 shareOfTheBorrowDelta = _params.delta.p2pSupplyAmount > 0 - ? (_params.delta.p2pBorrowDelta.rayMul(_params.lastPoolBorrowIndex)).rayDiv( + ? (_params.delta.p2pBorrowDelta.rayMul(_params.lastPoolIndexes.poolBorrowIndex)).rayDiv( _params.delta.p2pBorrowAmount.rayMul(_params.lastP2PBorrowIndex)) : 0; if (poolSupplyGrowthFactor <= poolBorrowGrowthFactor) { @@ -69,8 +69,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor0PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -88,8 +87,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -107,8 +105,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor0PerCentTest, p2pIndexCursorTest, Types.Delta(1 * RAY, 1 * RAY, 4 * RAY, 6 * RAY) @@ -126,8 +123,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(1 * RAY, 1 * RAY, 4 * RAY, 6 * RAY) @@ -145,8 +141,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolBorrowIndexTest * 2, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -164,8 +159,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolBorrowIndexTest * 2, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(1 * RAY, 1 * RAY, 4 * RAY, 6 * RAY) From 57bd7530ad5da05bd8fb0563913b290a45cc2329 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 8 Dec 2022 19:19:37 +0100 Subject: [PATCH 009/105] Refactor Morpho-Compound IRM --- src/compound/InterestRatesManager.sol | 150 +++++++----------- src/compound/lens/IndexesLens.sol | 28 ++-- src/compound/libraries/InterestRatesModel.sol | 120 ++++++-------- test/compound/TestInterestRates.t.sol | 30 ++-- test/compound/helpers/User.sol | 3 - 5 files changed, 133 insertions(+), 198 deletions(-) diff --git a/src/compound/InterestRatesManager.sol b/src/compound/InterestRatesManager.sol index bc991702d..9b2dfe8ef 100644 --- a/src/compound/InterestRatesManager.sol +++ b/src/compound/InterestRatesManager.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.13; import "./interfaces/IInterestRatesManager.sol"; +import "./libraries/InterestRatesModel.sol"; import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; -import "./libraries/CompoundMath.sol"; import "./MorphoStorage.sol"; @@ -15,7 +15,6 @@ import "./MorphoStorage.sol"; /// @dev This contract inherits from MorphoStorage so that Morpho can delegate calls to this contract. contract InterestRatesManager is IInterestRatesManager, MorphoStorage { using PercentageMath for uint256; - using CompoundMath for uint256; /// STRUCTS /// @@ -24,8 +23,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { uint256 lastP2PBorrowIndex; // The peer-to-peer borrow index at last update. uint256 poolSupplyIndex; // The current pool supply index. uint256 poolBorrowIndex; // The current pool borrow index. - uint256 lastPoolSupplyIndex; // The pool supply index at last update. - uint256 lastPoolBorrowIndex; // The pool borrow index at last update. + Types.LastPoolIndexes lastPoolIndexes; // The pool indexes at last update. uint256 reserveFactor; // The reserve factor percentage (10 000 = 100%). uint256 p2pIndexCursor; // The peer-to-peer index cursor (10 000 = 100%). Types.Delta delta; // The deltas and peer-to-peer amounts. @@ -54,41 +52,40 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { function updateP2PIndexes(address _poolToken) external { Types.LastPoolIndexes storage poolIndexes = lastPoolIndexes[_poolToken]; - if (block.number > poolIndexes.lastUpdateBlockNumber) { - Types.MarketParameters storage marketParams = marketParameters[_poolToken]; + if (block.number <= poolIndexes.lastUpdateBlockNumber) return; - uint256 poolSupplyIndex = ICToken(_poolToken).exchangeRateCurrent(); - uint256 poolBorrowIndex = ICToken(_poolToken).borrowIndex(); + Types.MarketParameters memory marketParams = marketParameters[_poolToken]; - Params memory params = Params( + uint256 poolSupplyIndex = ICToken(_poolToken).exchangeRateCurrent(); + uint256 poolBorrowIndex = ICToken(_poolToken).borrowIndex(); + + (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes( + Params( p2pSupplyIndex[_poolToken], p2pBorrowIndex[_poolToken], poolSupplyIndex, poolBorrowIndex, - poolIndexes.lastSupplyPoolIndex, - poolIndexes.lastBorrowPoolIndex, + poolIndexes, marketParams.reserveFactor, marketParams.p2pIndexCursor, deltas[_poolToken] - ); - - (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes(params); - - p2pSupplyIndex[_poolToken] = newP2PSupplyIndex; - p2pBorrowIndex[_poolToken] = newP2PBorrowIndex; - - poolIndexes.lastUpdateBlockNumber = uint32(block.number); - poolIndexes.lastSupplyPoolIndex = uint112(poolSupplyIndex); - poolIndexes.lastBorrowPoolIndex = uint112(poolBorrowIndex); - - emit P2PIndexesUpdated( - _poolToken, - newP2PSupplyIndex, - newP2PBorrowIndex, - poolSupplyIndex, - poolBorrowIndex - ); - } + ) + ); + + p2pSupplyIndex[_poolToken] = newP2PSupplyIndex; + p2pBorrowIndex[_poolToken] = newP2PBorrowIndex; + + poolIndexes.lastUpdateBlockNumber = uint32(block.number); + poolIndexes.lastSupplyPoolIndex = uint112(poolSupplyIndex); + poolIndexes.lastBorrowPoolIndex = uint112(poolBorrowIndex); + + emit P2PIndexesUpdated( + _poolToken, + newP2PSupplyIndex, + newP2PBorrowIndex, + poolSupplyIndex, + poolBorrowIndex + ); } /// INTERNAL /// @@ -102,69 +99,34 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { pure returns (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) { - // Compute pool growth factors - - uint256 poolSupplyGrowthFactor = _params.poolSupplyIndex.div(_params.lastPoolSupplyIndex); - uint256 poolBorrowGrowthFactor = _params.poolBorrowIndex.div(_params.lastPoolBorrowIndex); - - // Compute peer-to-peer growth factors. - - uint256 p2pSupplyGrowthFactor; - uint256 p2pBorrowGrowthFactor; - if (poolSupplyGrowthFactor <= poolBorrowGrowthFactor) { - uint256 p2pGrowthFactor = PercentageMath.weightedAvg( - poolSupplyGrowthFactor, - poolBorrowGrowthFactor, - _params.p2pIndexCursor - ); - - p2pSupplyGrowthFactor = - p2pGrowthFactor - - (p2pGrowthFactor - poolSupplyGrowthFactor).percentMul(_params.reserveFactor); - p2pBorrowGrowthFactor = - p2pGrowthFactor + - (poolBorrowGrowthFactor - p2pGrowthFactor).percentMul(_params.reserveFactor); - } else { - // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone sent underlying tokens to the - // cToken contract: the peer-to-peer growth factors are set to the pool borrow growth factor. - p2pSupplyGrowthFactor = poolBorrowGrowthFactor; - p2pBorrowGrowthFactor = poolBorrowGrowthFactor; - } - - // Compute new peer-to-peer supply index - - if (_params.delta.p2pSupplyAmount == 0 || _params.delta.p2pSupplyDelta == 0) { - newP2PSupplyIndex = _params.lastP2PSupplyIndex.mul(p2pSupplyGrowthFactor); - } else { - uint256 shareOfTheDelta = CompoundMath.min( - (_params.delta.p2pSupplyDelta.mul(_params.lastPoolSupplyIndex)).div( - (_params.delta.p2pSupplyAmount).mul(_params.lastP2PSupplyIndex) - ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. - ); - - newP2PSupplyIndex = _params.lastP2PSupplyIndex.mul( - (WAD - shareOfTheDelta).mul(p2pSupplyGrowthFactor) + - shareOfTheDelta.mul(poolSupplyGrowthFactor) - ); - } - - // Compute new peer-to-peer borrow index - - if (_params.delta.p2pBorrowAmount == 0 || _params.delta.p2pBorrowDelta == 0) { - newP2PBorrowIndex = _params.lastP2PBorrowIndex.mul(p2pBorrowGrowthFactor); - } else { - uint256 shareOfTheDelta = CompoundMath.min( - (_params.delta.p2pBorrowDelta.mul(_params.lastPoolBorrowIndex)).div( - (_params.delta.p2pBorrowAmount).mul(_params.lastP2PBorrowIndex) - ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. - ); - - newP2PBorrowIndex = _params.lastP2PBorrowIndex.mul( - (WAD - shareOfTheDelta).mul(p2pBorrowGrowthFactor) + - shareOfTheDelta.mul(poolBorrowGrowthFactor) - ); - } + InterestRatesModel.GrowthFactors memory growthFactors = InterestRatesModel + .computeGrowthFactors( + _params.poolSupplyIndex, + _params.poolBorrowIndex, + _params.lastPoolIndexes, + _params.p2pIndexCursor, + _params.reserveFactor + ); + + newP2PSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, + p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, + lastPoolIndex: _params.lastPoolIndexes.lastSupplyPoolIndex, + lastP2PIndex: _params.lastP2PSupplyIndex, + p2pDelta: _params.delta.p2pSupplyDelta, + p2pAmount: _params.delta.p2pSupplyAmount + }) + ); + newP2PBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, + p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, + lastPoolIndex: _params.lastPoolIndexes.lastBorrowPoolIndex, + lastP2PIndex: _params.lastP2PBorrowIndex, + p2pDelta: _params.delta.p2pBorrowDelta, + p2pAmount: _params.delta.p2pBorrowAmount + }) + ); } } diff --git a/src/compound/lens/IndexesLens.sol b/src/compound/lens/IndexesLens.sol index 335125ec8..ca048e3e8 100644 --- a/src/compound/lens/IndexesLens.sol +++ b/src/compound/lens/IndexesLens.sol @@ -133,23 +133,23 @@ abstract contract IndexesLens is LensStorage { ); indexes.p2pSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( - InterestRatesModel.P2PSupplyIndexComputeParams({ - poolSupplyGrowthFactor: growthFactors.poolSupplyGrowthFactor, - p2pSupplyGrowthFactor: growthFactors.p2pSupplyGrowthFactor, - lastPoolSupplyIndex: lastPoolIndexes.lastSupplyPoolIndex, - lastP2PSupplyIndex: morpho.p2pSupplyIndex(_poolToken), - p2pSupplyDelta: delta.p2pSupplyDelta, - p2pSupplyAmount: delta.p2pSupplyAmount + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, + p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, + lastPoolIndex: lastPoolIndexes.lastSupplyPoolIndex, + lastP2PIndex: morpho.p2pSupplyIndex(_poolToken), + p2pDelta: delta.p2pSupplyDelta, + p2pAmount: delta.p2pSupplyAmount }) ); indexes.p2pBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( - InterestRatesModel.P2PBorrowIndexComputeParams({ - poolBorrowGrowthFactor: growthFactors.poolBorrowGrowthFactor, - p2pBorrowGrowthFactor: growthFactors.p2pBorrowGrowthFactor, - lastPoolBorrowIndex: lastPoolIndexes.lastBorrowPoolIndex, - lastP2PBorrowIndex: morpho.p2pBorrowIndex(_poolToken), - p2pBorrowDelta: delta.p2pBorrowDelta, - p2pBorrowAmount: delta.p2pBorrowAmount + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, + p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, + lastPoolIndex: lastPoolIndexes.lastBorrowPoolIndex, + lastP2PIndex: morpho.p2pBorrowIndex(_poolToken), + p2pDelta: delta.p2pBorrowDelta, + p2pAmount: delta.p2pBorrowAmount }) ); } diff --git a/src/compound/libraries/InterestRatesModel.sol b/src/compound/libraries/InterestRatesModel.sol index 5a7370cf6..518fadb07 100644 --- a/src/compound/libraries/InterestRatesModel.sol +++ b/src/compound/libraries/InterestRatesModel.sol @@ -10,9 +10,6 @@ library InterestRatesModel { using PercentageMath for uint256; using CompoundMath for uint256; - uint256 public constant MAX_BASIS_POINTS = 100_00; // 100% (in basis points). - uint256 public constant WAD = 1e18; - /// STRUCTS /// struct GrowthFactors { @@ -22,22 +19,13 @@ library InterestRatesModel { uint256 p2pBorrowGrowthFactor; // Peer-to-peer borrow index growth factor (in wad). } - struct P2PSupplyIndexComputeParams { - uint256 poolSupplyGrowthFactor; // The pool supply index growth factor (in wad). - uint256 p2pSupplyGrowthFactor; // The peer-to-peer supply index growth factor (in wad). - uint256 lastP2PSupplyIndex; // The last stored peer-to-peer supply index (in wad). - uint256 lastPoolSupplyIndex; // The last stored pool supply index (in wad). - uint256 p2pSupplyDelta; // The peer-to-peer delta for the given market (in pool unit). - uint256 p2pSupplyAmount; // The peer-to-peer amount for the given market (in peer-to-peer unit). - } - - struct P2PBorrowIndexComputeParams { - uint256 poolBorrowGrowthFactor; // Pool borrow index growth factor (in wad). - uint256 p2pBorrowGrowthFactor; // Peer-to-peer borrow index growth factor (in wad). - uint256 lastP2PBorrowIndex; // Last stored peer-to-peer borrow index (in wad). - uint256 lastPoolBorrowIndex; // Last stored pool borrow index (in wad). - uint256 p2pBorrowDelta; // The peer-to-peer delta for the given market (in pool unit). - uint256 p2pBorrowAmount; // The peer-to-peer amount for the given market (in peer-to-peer unit). + struct P2PIndexComputeParams { + uint256 poolGrowthFactor; // The pool index growth factor (in wad). + uint256 p2pGrowthFactor; // Morpho's peer-to-peer median index growth factor (in wad). + uint256 lastPoolIndex; // The last stored pool index (in wad). + uint256 lastP2PIndex; // The last stored peer-to-peer index (in wad). + uint256 p2pDelta; // The peer-to-peer delta for the given market (in pool unit). + uint256 p2pAmount; // The peer-to-peer amount for the given market (in peer-to-peer unit). } struct P2PRateComputeParams { @@ -56,99 +44,95 @@ library InterestRatesModel { /// @param _lastPoolIndexes The last stored pool indexes. /// @param _p2pIndexCursor The peer-to-peer index cursor for the given market. /// @param _reserveFactor The reserve factor of the given market. - /// @return growthFactors_ The market's indexes growth factors (in wad). + /// @return growthFactors The market's indexes growth factors (in wad). function computeGrowthFactors( uint256 _newPoolSupplyIndex, uint256 _newPoolBorrowIndex, Types.LastPoolIndexes memory _lastPoolIndexes, - uint16 _p2pIndexCursor, + uint256 _p2pIndexCursor, uint256 _reserveFactor - ) internal pure returns (GrowthFactors memory growthFactors_) { - growthFactors_.poolSupplyGrowthFactor = _newPoolSupplyIndex.div( + ) internal pure returns (GrowthFactors memory growthFactors) { + growthFactors.poolSupplyGrowthFactor = _newPoolSupplyIndex.div( _lastPoolIndexes.lastSupplyPoolIndex ); - growthFactors_.poolBorrowGrowthFactor = _newPoolBorrowIndex.div( + growthFactors.poolBorrowGrowthFactor = _newPoolBorrowIndex.div( _lastPoolIndexes.lastBorrowPoolIndex ); - if (growthFactors_.poolSupplyGrowthFactor <= growthFactors_.poolBorrowGrowthFactor) { + if (growthFactors.poolSupplyGrowthFactor <= growthFactors.poolBorrowGrowthFactor) { uint256 p2pGrowthFactor = PercentageMath.weightedAvg( - growthFactors_.poolSupplyGrowthFactor, - growthFactors_.poolBorrowGrowthFactor, + growthFactors.poolSupplyGrowthFactor, + growthFactors.poolBorrowGrowthFactor, _p2pIndexCursor ); - growthFactors_.p2pSupplyGrowthFactor = + growthFactors.p2pSupplyGrowthFactor = p2pGrowthFactor - - (p2pGrowthFactor - growthFactors_.poolSupplyGrowthFactor).percentMul( - _reserveFactor - ); - growthFactors_.p2pBorrowGrowthFactor = + (p2pGrowthFactor - growthFactors.poolSupplyGrowthFactor).percentMul(_reserveFactor); + growthFactors.p2pBorrowGrowthFactor = p2pGrowthFactor + - (growthFactors_.poolBorrowGrowthFactor - p2pGrowthFactor).percentMul( - _reserveFactor - ); + (growthFactors.poolBorrowGrowthFactor - p2pGrowthFactor).percentMul(_reserveFactor); } else { // The case poolSupplyGrowthFactor > poolBorrowGrowthFactor happens because someone sent underlying tokens to the // cToken contract: the peer-to-peer growth factors are set to the pool borrow growth factor. - growthFactors_.p2pSupplyGrowthFactor = growthFactors_.poolBorrowGrowthFactor; - growthFactors_.p2pBorrowGrowthFactor = growthFactors_.poolBorrowGrowthFactor; + growthFactors.p2pSupplyGrowthFactor = growthFactors.poolBorrowGrowthFactor; + growthFactors.p2pBorrowGrowthFactor = growthFactors.poolBorrowGrowthFactor; } } /// @notice Computes and returns the new peer-to-peer supply index of a market given its parameters. /// @param _params The computation parameters. - /// @return newP2PSupplyIndex_ The updated peer-to-peer index. - function computeP2PSupplyIndex(P2PSupplyIndexComputeParams memory _params) + /// @return newP2PSupplyIndex The updated peer-to-peer index (in wad). + function computeP2PSupplyIndex(P2PIndexComputeParams memory _params) internal pure - returns (uint256 newP2PSupplyIndex_) + returns (uint256 newP2PSupplyIndex) { - if (_params.p2pSupplyAmount == 0 || _params.p2pSupplyDelta == 0) { - newP2PSupplyIndex_ = _params.lastP2PSupplyIndex.mul(_params.p2pSupplyGrowthFactor); + if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { + newP2PSupplyIndex = _params.lastP2PIndex.mul(_params.p2pGrowthFactor); } else { uint256 shareOfTheDelta = Math.min( - (_params.p2pSupplyDelta.mul(_params.lastPoolSupplyIndex)).div( - (_params.p2pSupplyAmount).mul(_params.lastP2PSupplyIndex) + (_params.p2pDelta.mul(_params.lastPoolIndex)).div( + (_params.p2pAmount).mul(_params.lastP2PIndex) ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. + CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. ); - newP2PSupplyIndex_ = _params.lastP2PSupplyIndex.mul( - (WAD - shareOfTheDelta).mul(_params.p2pSupplyGrowthFactor) + - shareOfTheDelta.mul(_params.poolSupplyGrowthFactor) + newP2PSupplyIndex = _params.lastP2PIndex.mul( + (CompoundMath.WAD - shareOfTheDelta).mul(_params.p2pGrowthFactor) + + shareOfTheDelta.mul(_params.poolGrowthFactor) ); } } /// @notice Computes and returns the new peer-to-peer borrow index of a market given its parameters. /// @param _params The computation parameters. - /// @return newP2PBorrowIndex_ The updated peer-to-peer index. - function computeP2PBorrowIndex(P2PBorrowIndexComputeParams memory _params) + /// @return newP2PBorrowIndex The updated peer-to-peer index (in wad). + function computeP2PBorrowIndex(P2PIndexComputeParams memory _params) internal pure - returns (uint256 newP2PBorrowIndex_) + returns (uint256 newP2PBorrowIndex) { - if (_params.p2pBorrowAmount == 0 || _params.p2pBorrowDelta == 0) { - newP2PBorrowIndex_ = _params.lastP2PBorrowIndex.mul(_params.p2pBorrowGrowthFactor); + if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { + newP2PBorrowIndex = _params.lastP2PIndex.mul(_params.p2pGrowthFactor); } else { uint256 shareOfTheDelta = Math.min( - (_params.p2pBorrowDelta.mul(_params.lastPoolBorrowIndex)).div( - (_params.p2pBorrowAmount).mul(_params.lastP2PBorrowIndex) + (_params.p2pDelta.mul(_params.lastPoolIndex)).div( + (_params.p2pAmount).mul(_params.lastP2PIndex) ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. + CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. ); - newP2PBorrowIndex_ = _params.lastP2PBorrowIndex.mul( - (WAD - shareOfTheDelta).mul(_params.p2pBorrowGrowthFactor) + - shareOfTheDelta.mul(_params.poolBorrowGrowthFactor) + newP2PBorrowIndex = _params.lastP2PIndex.mul( + (CompoundMath.WAD - shareOfTheDelta).mul(_params.p2pGrowthFactor) + + shareOfTheDelta.mul(_params.poolGrowthFactor) ); } } /// @notice Computes and returns the peer-to-peer supply rate per block of a market given its parameters. /// @param _params The computation parameters. - /// @return p2pSupplyRate The peer-to-peer supply rate per block. + /// @return p2pSupplyRate The peer-to-peer supply rate per block (in wad). function computeP2PSupplyRatePerBlock(P2PRateComputeParams memory _params) internal pure @@ -156,26 +140,25 @@ library InterestRatesModel { { p2pSupplyRate = _params.p2pRate - - ((_params.p2pRate - _params.poolRate) * _params.reserveFactor) / - MAX_BASIS_POINTS; + (_params.p2pRate - _params.poolRate).percentMul(_params.reserveFactor); if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { uint256 shareOfTheDelta = Math.min( _params.p2pDelta.mul(_params.poolIndex).div( _params.p2pAmount.mul(_params.p2pIndex) ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. + CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. ); p2pSupplyRate = - p2pSupplyRate.mul(WAD - shareOfTheDelta) + + p2pSupplyRate.mul(CompoundMath.WAD - shareOfTheDelta) + _params.poolRate.mul(shareOfTheDelta); } } /// @notice Computes and returns the peer-to-peer borrow rate per block of a market given its parameters. /// @param _params The computation parameters. - /// @return p2pBorrowRate The peer-to-peer borrow rate per block. + /// @return p2pBorrowRate The peer-to-peer borrow rate per block (in wad). function computeP2PBorrowRatePerBlock(P2PRateComputeParams memory _params) internal pure @@ -183,19 +166,18 @@ library InterestRatesModel { { p2pBorrowRate = _params.p2pRate + - ((_params.poolRate - _params.p2pRate) * _params.reserveFactor) / - MAX_BASIS_POINTS; + (_params.poolRate - _params.p2pRate).percentMul(_params.reserveFactor); if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { uint256 shareOfTheDelta = Math.min( _params.p2pDelta.mul(_params.poolIndex).div( _params.p2pAmount.mul(_params.p2pIndex) ), - WAD // To avoid shareOfTheDelta > 1 with rounding errors. + CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. ); p2pBorrowRate = - p2pBorrowRate.mul(WAD - shareOfTheDelta) + + p2pBorrowRate.mul(CompoundMath.WAD - shareOfTheDelta) + _params.poolRate.mul(shareOfTheDelta); } } diff --git a/test/compound/TestInterestRates.t.sol b/test/compound/TestInterestRates.t.sol index 5812ec3e0..0e212cebb 100644 --- a/test/compound/TestInterestRates.t.sol +++ b/test/compound/TestInterestRates.t.sol @@ -11,8 +11,8 @@ contract TestInterestRates is InterestRatesManager, Test { uint256 public p2pBorrowIndexTest = 1 * WAD; uint256 public poolSupplyIndexTest = 2 * WAD; uint256 public poolBorrowIndexTest = 3 * WAD; - uint256 public lastPoolSupplyIndexTest = 1 * WAD; - uint256 public lastPoolBorrowIndexTest = 1 * WAD; + Types.LastPoolIndexes public lastPoolIndexesTest = + Types.LastPoolIndexes(uint32(block.number), uint112(1 * WAD), uint112(1 * WAD)); uint256 public reserveFactor0PerCentTest = 0; uint256 public reserveFactor50PerCentTest = 5_000; uint256 public p2pIndexCursorTest = 3_333; @@ -23,15 +23,15 @@ contract TestInterestRates is InterestRatesManager, Test { pure returns (uint256 p2pSupplyIndex_, uint256 p2pBorrowIndex_) { - uint256 poolSupplyGrowthFactor = ((_params.poolSupplyIndex * WAD) / _params.lastPoolSupplyIndex); - uint256 poolBorrowGrowthFactor = ((_params.poolBorrowIndex * WAD) / _params.lastPoolBorrowIndex); + uint256 poolSupplyGrowthFactor = ((_params.poolSupplyIndex * WAD) / _params.lastPoolIndexes.lastSupplyPoolIndex); + uint256 poolBorrowGrowthFactor = ((_params.poolBorrowIndex * WAD) / _params.lastPoolIndexes.lastBorrowPoolIndex); uint256 p2pGrowthFactor = ((PercentageMath.PERCENTAGE_FACTOR - _params.p2pIndexCursor) * poolSupplyGrowthFactor + _params.p2pIndexCursor * poolBorrowGrowthFactor) / PercentageMath.PERCENTAGE_FACTOR; uint256 shareOfTheSupplyDelta = _params.delta.p2pBorrowAmount > 0 - ? (((_params.delta.p2pSupplyDelta * _params.lastPoolSupplyIndex) / WAD) * WAD) / + ? (((_params.delta.p2pSupplyDelta * _params.lastPoolIndexes.lastSupplyPoolIndex) / WAD) * WAD) / ((_params.delta.p2pSupplyAmount * _params.lastP2PSupplyIndex) / WAD) : 0; uint256 shareOfTheBorrowDelta = _params.delta.p2pSupplyAmount > 0 - ? (((_params.delta.p2pBorrowDelta * _params.lastPoolBorrowIndex) / WAD) * WAD) / + ? (((_params.delta.p2pBorrowDelta * _params.lastPoolIndexes.lastBorrowPoolIndex) / WAD) * WAD) / ((_params.delta.p2pBorrowAmount * _params.lastP2PBorrowIndex) / WAD) : 0; if (poolSupplyGrowthFactor <= poolBorrowGrowthFactor) { @@ -63,8 +63,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor0PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -82,8 +81,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -101,8 +99,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor0PerCentTest, p2pIndexCursorTest, Types.Delta(1 * WAD, 1 * WAD, 4 * WAD, 6 * WAD) @@ -120,8 +117,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolSupplyIndexTest, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(1 * WAD, 1 * WAD, 4 * WAD, 6 * WAD) @@ -139,8 +135,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolBorrowIndexTest * 2, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(0, 0, 0, 0) @@ -158,8 +153,7 @@ contract TestInterestRates is InterestRatesManager, Test { p2pBorrowIndexTest, poolBorrowIndexTest * 2, poolBorrowIndexTest, - lastPoolSupplyIndexTest, - lastPoolBorrowIndexTest, + lastPoolIndexesTest, reserveFactor50PerCentTest, p2pIndexCursorTest, Types.Delta(1 * WAD, 1 * WAD, 4 * WAD, 6 * WAD) diff --git a/test/compound/helpers/User.sol b/test/compound/helpers/User.sol index d2a522c28..0150710b8 100644 --- a/test/compound/helpers/User.sol +++ b/test/compound/helpers/User.sol @@ -4,19 +4,16 @@ pragma solidity ^0.8.0; import "src/compound/interfaces/IRewardsManager.sol"; import "src/compound/Morpho.sol"; -import "src/compound/InterestRatesManager.sol"; contract User { using SafeTransferLib for ERC20; Morpho internal morpho; - InterestRatesManager internal interestRatesManager; IRewardsManager internal rewardsManager; IComptroller internal comptroller; constructor(Morpho _morpho) { morpho = _morpho; - interestRatesManager = InterestRatesManager(address(_morpho.interestRatesManager())); rewardsManager = _morpho.rewardsManager(); comptroller = morpho.comptroller(); } From c9398db0a15dcf20b690034885421ef4b3685b60 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 13:28:59 -0500 Subject: [PATCH 010/105] Remove rewards manager setter --- src/aave-v2/MorphoGovernance.sol | 11 ----------- src/aave-v2/interfaces/IMorpho.sol | 1 - test/aave-v2/TestGovernance.t.sol | 9 --------- 3 files changed, 21 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 2015ac2fa..e23a09b45 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -42,10 +42,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @param _exitPositionsManager The new address of the `exitPositionsManager`. event ExitPositionsManagerSet(address indexed _exitPositionsManager); - /// @notice Emitted when the `rewardsManager` is set. - /// @param _newRewardsManagerAddress The new address of the `rewardsManager`. - event RewardsManagerSet(address indexed _newRewardsManagerAddress); - /// @notice Emitted when the `interestRatesManager` is set. /// @param _interestRatesManager The new address of the `interestRatesManager`. event InterestRatesSet(address indexed _interestRatesManager); @@ -224,13 +220,6 @@ abstract contract MorphoGovernance is MorphoUtils { emit InterestRatesSet(address(_interestRatesManager)); } - /// @notice Sets the `rewardsManager`. - /// @param _rewardsManager The new `rewardsManager`. - function setRewardsManager(IRewardsManager _rewardsManager) external onlyOwner { - rewardsManager = _rewardsManager; - emit RewardsManagerSet(address(_rewardsManager)); - } - /// @notice Sets the `treasuryVault`. /// @param _treasuryVault The address of the new `treasuryVault`. function setTreasuryVault(address _treasuryVault) external onlyOwner { diff --git a/src/aave-v2/interfaces/IMorpho.sol b/src/aave-v2/interfaces/IMorpho.sol index e2d2cb89d..fb420b06f 100644 --- a/src/aave-v2/interfaces/IMorpho.sol +++ b/src/aave-v2/interfaces/IMorpho.sol @@ -66,7 +66,6 @@ interface IMorpho { function setMaxSortedUsers(uint256 _newMaxSortedUsers) external; function setDefaultMaxGasForMatching(Types.MaxGasForMatching memory _maxGasForMatching) external; function setIncentivesVault(address _newIncentivesVault) external; - function setRewardsManager(address _rewardsManagerAddress) external; function setExitPositionsManager(IExitPositionsManager _exitPositionsManager) external; function setEntryPositionsManager(IEntryPositionsManager _entryPositionsManager) external; function setInterestRatesManager(IInterestRatesManager _interestRatesManager) external; diff --git a/test/aave-v2/TestGovernance.t.sol b/test/aave-v2/TestGovernance.t.sol index ba4e308cc..3fb283078 100644 --- a/test/aave-v2/TestGovernance.t.sol +++ b/test/aave-v2/TestGovernance.t.sol @@ -137,15 +137,6 @@ contract TestGovernance is TestSetup { assertEq(address(morpho.entryPositionsManager()), address(entryPositionsManagerV2)); } - function testOnlyOwnerShouldSetRewardsManager() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - morpho.setRewardsManager(IRewardsManager(address(1))); - - morpho.setRewardsManager(IRewardsManager(address(1))); - assertEq(address(morpho.rewardsManager()), address(1)); - } - function testOnlyOwnerShouldSetInterestRatesManager() public { IInterestRatesManager interestRatesV2 = new InterestRatesManager(); From 6882710056fbb28b9875680503c4c93045bfdadf Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 13:35:11 -0500 Subject: [PATCH 011/105] Remove set as collateral --- src/aave-v2/MorphoGovernance.sol | 11 ----------- test/aave-v2/TestBorrow.t.sol | 3 ++- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 2015ac2fa..7d3e0f2b3 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -401,17 +401,6 @@ abstract contract MorphoGovernance is MorphoUtils { emit IsDeprecatedSet(_poolToken, _isDeprecated); } - /// @notice Sets a market's asset as collateral. - /// @param _poolToken The address of the market to (un)set as collateral. - /// @param _assetAsCollateral True to set the asset as collateral (True by default). - function setAssetAsCollateral(address _poolToken, bool _assetAsCollateral) - external - onlyOwner - isMarketCreated(_poolToken) - { - pool.setUserUseReserveAsCollateral(market[_poolToken].underlyingToken, _assetAsCollateral); - } - /// @notice Increases peer-to-peer deltas, to put some liquidity back on the pool. /// @dev The current Morpho supply on the pool might not be enough to borrow `_amount` before resuppling it. /// In this case, consider calling multiple times this function. diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index 6dea58d11..5efe02eae 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -258,7 +258,8 @@ contract TestBorrow is TestSetup { supplier1.approve(usdc, to6Decimals(amount)); supplier1.supply(aUsdc, to6Decimals(amount)); - morpho.setAssetAsCollateral(aDai, false); + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, false); hevm.expectRevert(EntryPositionsManager.UnauthorisedBorrow.selector); borrower1.borrow(aUsdc, to6Decimals(amount)); From 31216a3e9ab65b0d9716f5b817b67b2e087aca1c Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 14:00:12 -0500 Subject: [PATCH 012/105] Move liquidation computation --- src/aave-v2/MorphoUtils.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index ffb4a6b16..9e3cf5ea5 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -310,18 +310,18 @@ abstract contract MorphoUtils is MorphoStorage { values.collateral += assetCollateralValue; // Calculate LTV for borrow. values.maxDebt += assetCollateralValue.percentMul(assetData.ltv); + + // Update LT variable for withdraw. + if (assetCollateralValue > 0) + values.liquidationThreshold += assetCollateralValue.percentMul( + assetData.liquidationThreshold + ); } // Update debt variable for borrowed token. if (_poolToken == vars.poolToken && _amountBorrowed > 0) values.debt += (_amountBorrowed * vars.underlyingPrice).divUp(assetData.tokenUnit); - // Update LT variable for withdraw. - if (assetCollateralValue > 0) - values.liquidationThreshold += assetCollateralValue.percentMul( - assetData.liquidationThreshold - ); - // Subtract withdrawn amount from liquidation threshold and collateral. if (_poolToken == vars.poolToken && _amountWithdrawn > 0) { uint256 withdrawn = (_amountWithdrawn * vars.underlyingPrice) / assetData.tokenUnit; From 4a77662fec093c14830aa2e9c4a12bcf22267525 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 8 Dec 2022 20:12:39 +0100 Subject: [PATCH 013/105] Remove everything rewards-related --- config/eth-mainnet/aave-v2/Config.sol | 11 -- src/aave-v2/IncentivesVault.sol | 150 ----------------- src/aave-v2/MatchingEngine.sol | 22 --- src/aave-v2/Morpho.sol | 39 ----- src/aave-v2/MorphoGovernance.sol | 33 ---- src/aave-v2/MorphoStorage.sol | 10 +- src/aave-v2/interfaces/IIncentivesVault.sol | 28 ---- src/aave-v2/interfaces/IMorpho.sol | 10 +- src/aave-v2/interfaces/IRewardsManager.sol | 26 --- .../aave/IAaveIncentivesController.sol | 153 ------------------ test/aave-v2/TestGovernance.t.sol | 26 --- test/aave-v2/TestIncentivesVault.t.sol | 113 ------------- test/aave-v2/helpers/User.sol | 19 --- test/aave-v2/setup/TestSetup.sol | 16 +- test/prod/aave-v2/TestLifecycle.t.sol | 47 ------ test/prod/aave-v2/setup/TestSetup.sol | 5 - 16 files changed, 8 insertions(+), 700 deletions(-) delete mode 100644 src/aave-v2/IncentivesVault.sol delete mode 100644 src/aave-v2/interfaces/IIncentivesVault.sol delete mode 100644 src/aave-v2/interfaces/IRewardsManager.sol delete mode 100644 src/aave-v2/interfaces/aave/IAaveIncentivesController.sol delete mode 100644 test/aave-v2/TestIncentivesVault.t.sol diff --git a/config/eth-mainnet/aave-v2/Config.sol b/config/eth-mainnet/aave-v2/Config.sol index 7b4192e77..a3587c1b8 100644 --- a/config/eth-mainnet/aave-v2/Config.sol +++ b/config/eth-mainnet/aave-v2/Config.sol @@ -3,10 +3,7 @@ pragma solidity >=0.8.0; import {ILendingPool} from "src/aave-v2/interfaces/aave/ILendingPool.sol"; import {IPriceOracleGetter} from "src/aave-v2/interfaces/aave/IPriceOracleGetter.sol"; -import {IAaveIncentivesController} from "src/aave-v2/interfaces/aave/IAaveIncentivesController.sol"; import {ILendingPoolAddressesProvider} from "src/aave-v2/interfaces/aave/ILendingPoolAddressesProvider.sol"; -import {IRewardsManager} from "src/aave-v2/interfaces/IRewardsManager.sol"; -import {IIncentivesVault} from "src/aave-v2/interfaces/IIncentivesVault.sol"; import {IEntryPositionsManager} from "src/aave-v2/interfaces/IEntryPositionsManager.sol"; import {IExitPositionsManager} from "src/aave-v2/interfaces/IExitPositionsManager.sol"; import {IInterestRatesManager} from "src/aave-v2/interfaces/IInterestRatesManager.sol"; @@ -44,29 +41,21 @@ contract Config is BaseConfig { ILendingPoolAddressesProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); ILendingPoolConfigurator public lendingPoolConfigurator = ILendingPoolConfigurator(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); - IAaveIncentivesController public aaveIncentivesController = - IAaveIncentivesController(0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5); IPriceOracleGetter public oracle = IPriceOracleGetter(poolAddressesProvider.getPriceOracle()); ILendingPool public pool = ILendingPool(poolAddressesProvider.getLendingPool()); - address public REWARD_TOKEN = aaveIncentivesController.REWARD_TOKEN(); - ProxyAdmin public proxyAdmin = ProxyAdmin(0x99917ca0426fbC677e84f873Fb0b726Bb4799cD8); TransparentUpgradeableProxy public lensProxy = TransparentUpgradeableProxy(payable(0x507fA343d0A90786d86C7cd885f5C49263A91FF4)); TransparentUpgradeableProxy public morphoProxy = TransparentUpgradeableProxy(payable(0x777777c9898D384F785Ee44Acfe945efDFf5f3E0)); - TransparentUpgradeableProxy public rewardsManagerProxy; Lens public lensImplV1; Morpho public morphoImplV1; - IRewardsManager public rewardsManagerImplV1; Lens public lens; Morpho public morpho; - IRewardsManager public rewardsManager; - IIncentivesVault public incentivesVault; IEntryPositionsManager public entryPositionsManager; IExitPositionsManager public exitPositionsManager; IInterestRatesManager public interestRatesManager; diff --git a/src/aave-v2/IncentivesVault.sol b/src/aave-v2/IncentivesVault.sol deleted file mode 100644 index 534d89f7a..000000000 --- a/src/aave-v2/IncentivesVault.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity 0.8.13; - -import "./interfaces/IIncentivesVault.sol"; -import "./interfaces/IOracle.sol"; -import "./interfaces/IMorpho.sol"; - -import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; -import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; - -import "@openzeppelin/contracts/access/Ownable.sol"; - -/// @title IncentivesVault. -/// @author Morpho Labs. -/// @custom:contact security@morpho.xyz -/// @notice Contract handling Morpho incentives. -contract IncentivesVault is IIncentivesVault, Ownable { - using SafeTransferLib for ERC20; - using PercentageMath for uint256; - - /// STORAGE /// - - uint256 public constant MAX_BASIS_POINTS = 10_000; - - IMorpho public immutable morpho; // The address of the main Morpho contract. - ERC20 public immutable rewardToken; // The reward token. - ERC20 public immutable morphoToken; // The MORPHO token. - - IOracle public oracle; // The oracle used to get the price of MORPHO tokens against token reward tokens. - address public incentivesTreasuryVault; // The address of the incentives treasury vault. - uint256 public bonus; // The bonus percentage of MORPHO tokens to give to the user. - bool public isPaused; // Whether the trade of token rewards for MORPHO rewards is paused or not. - - /// EVENTS /// - - /// @notice Emitted when the oracle is set. - /// @param newOracle The new oracle set. - event OracleSet(address newOracle); - - /// @notice Emitted when the incentives treasury vault is set. - /// @param newIncentivesTreasuryVault The address of the incentives treasury vault. - event IncentivesTreasuryVaultSet(address newIncentivesTreasuryVault); - - /// @notice Emitted when the reward bonus is set. - /// @param newBonus The new bonus set. - event BonusSet(uint256 newBonus); - - /// @notice Emitted when the pause status is changed. - /// @param newStatus The new newStatus set. - event PauseStatusSet(bool newStatus); - - /// @notice Emitted when tokens are transferred to the DAO. - /// @param token The address of the token transferred. - /// @param amount The amount of token transferred to the DAO. - event TokensTransferred(address indexed token, uint256 amount); - - /// @notice Emitted when reward tokens are traded for MORPHO tokens. - /// @param receiver The address of the receiver. - /// @param rewardAmount The amount of reward token traded. - /// @param morphoAmount The amount of MORPHO transferred. - event RewardTokensTraded(address indexed receiver, uint256 rewardAmount, uint256 morphoAmount); - - /// ERRORS /// - - /// @notice Thrown when an other address than Morpho triggers the function. - error OnlyMorpho(); - - /// @notice Thrown when the vault is paused. - error VaultIsPaused(); - - /// @notice Thrown when the input is above the max basis points value (100%). - error ExceedsMaxBasisPoints(); - - /// CONSTRUCTOR /// - - /// @notice Constructs the IncentivesVault contract. - /// @param _morpho The main Morpho contract. - /// @param _morphoToken The MORPHO token. - /// @param _rewardToken The reward token. - /// @param _incentivesTreasuryVault The address of the incentives treasury vault. - /// @param _oracle The oracle. - constructor( - IMorpho _morpho, - ERC20 _morphoToken, - ERC20 _rewardToken, - address _incentivesTreasuryVault, - IOracle _oracle - ) { - morpho = _morpho; - morphoToken = _morphoToken; - rewardToken = _rewardToken; - incentivesTreasuryVault = _incentivesTreasuryVault; - oracle = _oracle; - } - - /// EXTERNAL /// - - /// @notice Sets the oracle. - /// @param _newOracle The address of the new oracle. - function setOracle(IOracle _newOracle) external onlyOwner { - oracle = _newOracle; - emit OracleSet(address(_newOracle)); - } - - /// @notice Sets the incentives treasury vault. - /// @param _newIncentivesTreasuryVault The address of the incentives treasury vault. - function setIncentivesTreasuryVault(address _newIncentivesTreasuryVault) external onlyOwner { - incentivesTreasuryVault = _newIncentivesTreasuryVault; - emit IncentivesTreasuryVaultSet(_newIncentivesTreasuryVault); - } - - /// @notice Sets the reward bonus. - /// @param _newBonus The new reward bonus. - function setBonus(uint256 _newBonus) external onlyOwner { - if (_newBonus > MAX_BASIS_POINTS) revert ExceedsMaxBasisPoints(); - - bonus = _newBonus; - emit BonusSet(_newBonus); - } - - /// @notice Sets the pause status. - /// @param _newStatus The new pause status. - function setPauseStatus(bool _newStatus) external onlyOwner { - isPaused = _newStatus; - emit PauseStatusSet(_newStatus); - } - - /// @notice Transfers the specified token to the DAO. - /// @param _token The address of the token to transfer. - /// @param _amount The amount of token to transfer to the DAO. - function transferTokensToDao(address _token, uint256 _amount) external onlyOwner { - ERC20(_token).safeTransfer(incentivesTreasuryVault, _amount); - emit TokensTransferred(_token, _amount); - } - - /// @notice Trades reward tokens for MORPHO tokens and sends them to the receiver. - /// @dev The amount of rewards to trade for MORPHO tokens is supposed to have been transferred to this contract before calling the function. - /// @param _receiver The address of the receiver. - /// @param _amount The amount claimed, to trade for MORPHO tokens. - function tradeRewardTokensForMorphoTokens(address _receiver, uint256 _amount) external { - if (msg.sender != address(morpho)) revert OnlyMorpho(); - if (isPaused) revert VaultIsPaused(); - - // Add a bonus on MORPHO rewards. - uint256 amountOut = oracle.consult(_amount).percentAdd(bonus); - morphoToken.safeTransfer(_receiver, amountOut); - - emit RewardTokensTraded(_receiver, _amount, amountOut); - } -} diff --git a/src/aave-v2/MatchingEngine.sol b/src/aave-v2/MatchingEngine.sol index 3715974dd..c4d4ee46d 100644 --- a/src/aave-v2/MatchingEngine.sol +++ b/src/aave-v2/MatchingEngine.sol @@ -310,15 +310,6 @@ abstract contract MatchingEngine is MorphoUtils { marketSuppliersOnPool.update(_user, formerValueOnPool, onPool, maxSortedUsers); marketSuppliersInP2P.update(_user, formerValueInP2P, inP2P, maxSortedUsers); - - if (formerValueOnPool != onPool && address(rewardsManager) != address(0)) - rewardsManager.updateUserAssetAndAccruedRewards( - aaveIncentivesController, - _user, - _poolToken, - formerValueOnPool, - IScaledBalanceToken(_poolToken).scaledTotalSupply() - ); } /// @notice Updates the given `_user`'s position in the borrower data structures. @@ -336,18 +327,5 @@ abstract contract MatchingEngine is MorphoUtils { marketBorrowersOnPool.update(_user, formerValueOnPool, onPool, maxSortedUsers); marketBorrowersInP2P.update(_user, formerValueInP2P, inP2P, maxSortedUsers); - - if (formerValueOnPool != onPool && address(rewardsManager) != address(0)) { - address variableDebtTokenAddress = pool - .getReserveData(market[_poolToken].underlyingToken) - .variableDebtTokenAddress; - rewardsManager.updateUserAssetAndAccruedRewards( - aaveIncentivesController, - _user, - variableDebtTokenAddress, - formerValueOnPool, - IScaledBalanceToken(variableDebtTokenAddress).scaledTotalSupply() - ); - } } } diff --git a/src/aave-v2/Morpho.sol b/src/aave-v2/Morpho.sol index a6ef14559..c0bef2bc6 100644 --- a/src/aave-v2/Morpho.sol +++ b/src/aave-v2/Morpho.sol @@ -12,19 +12,6 @@ contract Morpho is MorphoGovernance { using DelegateCall for address; using WadRayMath for uint256; - /// EVENTS /// - - /// @notice Emitted when a user claims rewards. - /// @param _user The address of the claimer. - /// @param _amountClaimed The amount of reward token claimed. - /// @param _traded Whether or not the pool tokens are traded against Morpho tokens. - event RewardsClaimed(address indexed _user, uint256 _amountClaimed, bool indexed _traded); - - /// ERRORS /// - - /// @notice Thrown when claiming rewards is paused. - error ClaimRewardsPaused(); - /// EXTERNAL /// /// @notice Supplies underlying tokens to a specific market. @@ -145,32 +132,6 @@ contract Morpho is MorphoGovernance { ); } - /// @notice Claims rewards for the given assets. - /// @param _assets The assets to claim rewards from (aToken or variable debt token). - /// @param _tradeForMorphoToken Whether or not to trade reward tokens for MORPHO tokens. - /// @return claimedAmount The amount of rewards claimed (in reward token). - function claimRewards(address[] calldata _assets, bool _tradeForMorphoToken) - external - nonReentrant - returns (uint256 claimedAmount) - { - if (isClaimRewardsPaused) revert ClaimRewardsPaused(); - claimedAmount = rewardsManager.claimRewards(aaveIncentivesController, _assets, msg.sender); - - if (claimedAmount > 0) { - if (_tradeForMorphoToken) { - aaveIncentivesController.claimRewards( - _assets, - claimedAmount, - address(incentivesVault) - ); - incentivesVault.tradeRewardTokensForMorphoTokens(msg.sender, claimedAmount); - } else aaveIncentivesController.claimRewards(_assets, claimedAmount, msg.sender); - - emit RewardsClaimed(msg.sender, claimedAmount, _tradeForMorphoToken); - } - } - /// INTERNAL /// function _supply( diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index e23a09b45..7d1a778c7 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -30,10 +30,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @param _newTreasuryVaultAddress The new address of the `treasuryVault`. event TreasuryVaultSet(address indexed _newTreasuryVaultAddress); - /// @notice Emitted when the address of the `incentivesVault` is set. - /// @param _newIncentivesVaultAddress The new address of the `incentivesVault`. - event IncentivesVaultSet(address indexed _newIncentivesVaultAddress); - /// @notice Emitted when the `entryPositionsManager` is set. /// @param _entryPositionsManager The new address of the `entryPositionsManager`. event EntryPositionsManagerSet(address indexed _entryPositionsManager); @@ -46,10 +42,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @param _interestRatesManager The new address of the `interestRatesManager`. event InterestRatesSet(address indexed _interestRatesManager); - /// @notice Emitted when the address of the `aaveIncentivesController` is set. - /// @param _aaveIncentivesController The new address of the `aaveIncentivesController`. - event AaveIncentivesControllerSet(address indexed _aaveIncentivesController); - /// @notice Emitted when the `reserveFactor` is set. /// @param _poolToken The address of the concerned market. /// @param _newValue The new value of the `reserveFactor`. @@ -105,10 +97,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @param _isDeprecated The new deprecated status. event IsDeprecatedSet(address indexed _poolToken, bool _isDeprecated); - /// @notice Emitted when claiming rewards is paused or unpaused. - /// @param _isPaused The new pause status. - event ClaimRewardsPauseStatusSet(bool _isPaused); - /// @notice Emitted when a new market is created. /// @param _poolToken The address of the market that has been created. /// @param _reserveFactor The reserve factor set for this market. @@ -227,20 +215,6 @@ abstract contract MorphoGovernance is MorphoUtils { emit TreasuryVaultSet(_treasuryVault); } - /// @notice Sets the `aaveIncentivesController`. - /// @param _aaveIncentivesController The address of the `aaveIncentivesController`. - function setAaveIncentivesController(address _aaveIncentivesController) external onlyOwner { - aaveIncentivesController = IAaveIncentivesController(_aaveIncentivesController); - emit AaveIncentivesControllerSet(_aaveIncentivesController); - } - - /// @notice Sets the `incentivesVault`. - /// @param _incentivesVault The new `incentivesVault`. - function setIncentivesVault(IIncentivesVault _incentivesVault) external onlyOwner { - incentivesVault = _incentivesVault; - emit IncentivesVaultSet(address(_incentivesVault)); - } - /// @notice Sets the `reserveFactor`. /// @param _poolToken The market on which to set the `_newReserveFactor`. /// @param _newReserveFactor The proportion of the interest earned by users sent to the DAO, in basis point. @@ -371,13 +345,6 @@ abstract contract MorphoGovernance is MorphoUtils { emit P2PStatusSet(_poolToken, _isP2PDisabled); } - /// @notice Sets `isClaimRewardsPaused`. - /// @param _isPaused The new pause status, true to pause the mechanism. - function setIsClaimRewardsPaused(bool _isPaused) external onlyOwner { - isClaimRewardsPaused = _isPaused; - emit ClaimRewardsPauseStatusSet(_isPaused); - } - /// @notice Sets a market as deprecated (allows liquidation of every position on this market). /// @param _poolToken The address of the market to update. /// @param _isDeprecated The new deprecated status, true to deprecate the market. diff --git a/src/aave-v2/MorphoStorage.sol b/src/aave-v2/MorphoStorage.sol index b5a476b34..ce6e8626d 100644 --- a/src/aave-v2/MorphoStorage.sol +++ b/src/aave-v2/MorphoStorage.sol @@ -5,8 +5,6 @@ import "./interfaces/aave/ILendingPool.sol"; import "./interfaces/IEntryPositionsManager.sol"; import "./interfaces/IExitPositionsManager.sol"; import "./interfaces/IInterestRatesManager.sol"; -import "./interfaces/IIncentivesVault.sol"; -import "./interfaces/IRewardsManager.sol"; import "@morpho-dao/morpho-data-structures/HeapOrdering.sol"; import "./libraries/Types.sol"; @@ -37,7 +35,7 @@ abstract contract MorphoStorage is OwnableUpgradeable, ReentrancyGuardUpgradeabl // and used for internal calculations to convert `stEth.balanceOf` into an amount in scaled units. uint256 public constant ST_ETH_BASE_REBASE_INDEX = 1_086492192583716523804482274; - bool public isClaimRewardsPaused; // Whether claiming rewards is paused or not. + bool public isClaimRewardsPaused; // Deprecated: whether claiming rewards is paused or not. uint256 public maxSortedUsers; // The max number of users to sort in the data structure. Types.MaxGasForMatching public defaultMaxGasForMatching; // The default max gas to consume within loops in matching engine functions. @@ -64,14 +62,14 @@ abstract contract MorphoStorage is OwnableUpgradeable, ReentrancyGuardUpgradeabl /// CONTRACTS AND ADDRESSES /// ILendingPoolAddressesProvider public addressesProvider; - IAaveIncentivesController public aaveIncentivesController; + address public aaveIncentivesController; // Deprecated. ILendingPool public pool; IEntryPositionsManager public entryPositionsManager; IExitPositionsManager public exitPositionsManager; IInterestRatesManager public interestRatesManager; - IIncentivesVault public incentivesVault; - IRewardsManager public rewardsManager; + address public incentivesVault; // Deprecated. + address public rewardsManager; // Deprecated. address public treasuryVault; /// APPENDIX STORAGE /// diff --git a/src/aave-v2/interfaces/IIncentivesVault.sol b/src/aave-v2/interfaces/IIncentivesVault.sol deleted file mode 100644 index b297db5df..000000000 --- a/src/aave-v2/interfaces/IIncentivesVault.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity >=0.5.0; - -import "./IOracle.sol"; - -interface IIncentivesVault { - function isPaused() external view returns (bool); - - function bonus() external view returns (uint256); - - function MAX_BASIS_POINTS() external view returns (uint256); - - function incentivesTreasuryVault() external view returns (address); - - function oracle() external view returns (IOracle); - - function setOracle(IOracle _newOracle) external; - - function setIncentivesTreasuryVault(address _newIncentivesTreasuryVault) external; - - function setBonus(uint256 _newBonus) external; - - function setPauseStatus(bool _newStatus) external; - - function transferTokensToDao(address _token, uint256 _amount) external; - - function tradeRewardTokensForMorphoTokens(address _to, uint256 _amount) external; -} diff --git a/src/aave-v2/interfaces/IMorpho.sol b/src/aave-v2/interfaces/IMorpho.sol index fb420b06f..b93883e05 100644 --- a/src/aave-v2/interfaces/IMorpho.sol +++ b/src/aave-v2/interfaces/IMorpho.sol @@ -6,8 +6,6 @@ import "./aave/ILendingPool.sol"; import "./IEntryPositionsManager.sol"; import "./IExitPositionsManager.sol"; import "./IInterestRatesManager.sol"; -import "./IIncentivesVault.sol"; -import "./IRewardsManager.sol"; import "../libraries/Types.sol"; @@ -40,12 +38,12 @@ interface IMorpho { function p2pBorrowIndex(address) external view returns (uint256); function poolIndexes(address) external view returns (Types.PoolIndexes memory); function interestRatesManager() external view returns (IInterestRatesManager); - function rewardsManager() external view returns (IRewardsManager); + function rewardsManager() external view returns (address); function entryPositionsManager() external view returns (IEntryPositionsManager); function exitPositionsManager() external view returns (IExitPositionsManager); - function aaveIncentivesController() external view returns (IAaveIncentivesController); + function aaveIncentivesController() external view returns (address); function addressesProvider() external view returns (ILendingPoolAddressesProvider); - function incentivesVault() external view returns (IIncentivesVault); + function incentivesVault() external view returns (address); function pool() external view returns (ILendingPool); function treasuryVault() external view returns (address); function borrowMask(address) external view returns (bytes32); @@ -74,7 +72,6 @@ interface IMorpho { function setReserveFactor(address _poolToken, uint256 _newReserveFactor) external; function setP2PIndexCursor(address _poolToken, uint16 _p2pIndexCursor) external; function setIsPausedForAllMarkets(bool _isPaused) external; - function setIsClaimRewardsPaused(bool _isPaused) external; function setIsSupplyPaused(address _poolToken, bool _isPaused) external; function setIsBorrowPaused(address _poolToken, bool _isPaused) external; function setIsWithdrawPaused(address _poolToken, bool _isPaused) external; @@ -97,5 +94,4 @@ interface IMorpho { function repay(address _poolToken, uint256 _amount) external; function repay(address _poolToken, address _onBehalf, uint256 _amount) external; function liquidate(address _poolTokenBorrowed, address _poolTokenCollateral, address _borrower, uint256 _amount) external; - function claimRewards(address[] calldata _assets, bool _tradeForMorphoToken) external returns (uint256 claimedAmount); } diff --git a/src/aave-v2/interfaces/IRewardsManager.sol b/src/aave-v2/interfaces/IRewardsManager.sol deleted file mode 100644 index 302c040f7..000000000 --- a/src/aave-v2/interfaces/IRewardsManager.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity >=0.5.0; - -import "./aave/IAaveIncentivesController.sol"; - -interface IRewardsManager { - function initialize(address _morpho) external; - - function getUserIndex(address, address) external returns (uint256); - - function getUserUnclaimedRewards(address[] calldata, address) external view returns (uint256); - - function claimRewards( - IAaveIncentivesController _aaveIncentivesController, - address[] calldata, - address - ) external returns (uint256); - - function updateUserAssetAndAccruedRewards( - IAaveIncentivesController _aaveIncentivesController, - address _user, - address _asset, - uint256 _userBalance, - uint256 _totalBalance - ) external; -} diff --git a/src/aave-v2/interfaces/aave/IAaveIncentivesController.sol b/src/aave-v2/interfaces/aave/IAaveIncentivesController.sol deleted file mode 100644 index 16b52d17d..000000000 --- a/src/aave-v2/interfaces/aave/IAaveIncentivesController.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity >=0.5.0; - -interface IAaveDistributionManager { - event AssetConfigUpdated(address indexed asset, uint256 emission); - event AssetIndexUpdated(address indexed asset, uint256 index); - event UserIndexUpdated(address indexed user, address indexed asset, uint256 index); - event DistributionEndUpdated(uint256 newDistributionEnd); - - /** - * @dev Sets the end date for the distribution - * @param distributionEnd The end date timestamp - **/ - function setDistributionEnd(uint256 distributionEnd) external; - - /** - * @dev Gets the end date for the distribution - * @return The end of the distribution - **/ - function getDistributionEnd() external view returns (uint256); - - /** - * @dev for backwards compatibility with the previous DistributionManager used - * @return The end of the distribution - **/ - function DISTRIBUTION_END() external view returns (uint256); - - /** - * @dev Returns the data of an user on a distribution - * @param user Address of the user - * @param asset The address of the reference asset of the distribution - * @return The new index - **/ - function getUserAssetData(address user, address asset) external view returns (uint256); - - /** - * @dev Returns the configuration of the distribution for a certain asset - * @param asset The address of the reference asset of the distribution - * @return The asset index, the emission per second and the last updated timestamp - **/ - function getAssetData(address asset) - external - view - returns ( - uint256, - uint256, - uint256 - ); -} - -interface IAaveIncentivesController is IAaveDistributionManager { - event RewardsAccrued(address indexed user, uint256 amount); - - event RewardsClaimed( - address indexed user, - address indexed to, - address indexed claimer, - uint256 amount - ); - - event ClaimerSet(address indexed user, address indexed claimer); - - /** - * @dev Whitelists an address to claim the rewards on behalf of another address - * @param user The address of the user - * @param claimer The address of the claimer - */ - function setClaimer(address user, address claimer) external; - - /** - * @dev Returns the whitelisted claimer for a certain address (0x0 if not set) - * @param user The address of the user - * @return The claimer address - */ - function getClaimer(address user) external view returns (address); - - /** - * @dev Configure assets for a certain rewards emission - * @param assets The assets to incentivize - * @param emissionsPerSecond The emission for each asset - */ - function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) - external; - - /** - * @dev Called by the corresponding asset on any update that affects the rewards distribution - * @param asset The address of the user - * @param userBalance The balance of the user of the asset in the lending pool - * @param totalSupply The total supply of the asset in the lending pool - **/ - function handleAction( - address asset, - uint256 userBalance, - uint256 totalSupply - ) external; - - /** - * @dev Returns the total of rewards of an user, already accrued + not yet accrued - * @param user The address of the user - * @return The rewards - **/ - function getRewardsBalance(address[] calldata assets, address user) - external - view - returns (uint256); - - /** - * @dev Claims reward for an user, on all the assets of the lending pool, accumulating the pending rewards - * @param amount Amount of rewards to claim - * @param to Address that will be receiving the rewards - * @return Rewards claimed - **/ - function claimRewards( - address[] calldata assets, - uint256 amount, - address to - ) external returns (uint256); - - /** - * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. The caller must - * be whitelisted via "allowClaimOnBehalf" function by the RewardsAdmin role manager - * @param amount Amount of rewards to claim - * @param user Address to check and claim rewards - * @param to Address that will be receiving the rewards - * @return Rewards claimed - **/ - function claimRewardsOnBehalf( - address[] calldata assets, - uint256 amount, - address user, - address to - ) external returns (uint256); - - /** - * @dev returns the unclaimed rewards of the user - * @param user the address of the user - * @return the unclaimed user rewards - */ - function getUserUnclaimedRewards(address user) external view returns (uint256); - - /** - * @dev for backward compatibility with previous implementation of the Incentives controller - */ - function REWARD_TOKEN() external view returns (address); - - struct AssetData { - uint128 emissionPerSecond; - uint128 lastUpdateTimestamp; - uint256 index; - } - - function assets(address) external view returns (AssetData memory); -} diff --git a/test/aave-v2/TestGovernance.t.sol b/test/aave-v2/TestGovernance.t.sol index 3fb283078..216f7ffdc 100644 --- a/test/aave-v2/TestGovernance.t.sol +++ b/test/aave-v2/TestGovernance.t.sol @@ -148,23 +148,6 @@ contract TestGovernance is TestSetup { assertEq(address(morpho.interestRatesManager()), address(interestRatesV2)); } - function testOnlyOwnerShouldSetIncentivesVault() public { - IIncentivesVault incentivesVaultV2 = new IncentivesVault( - IMorpho(address(morpho)), - morphoToken, - ERC20(address(1)), - address(2), - dumbOracle - ); - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - morpho.setIncentivesVault(incentivesVaultV2); - - morpho.setIncentivesVault(incentivesVaultV2); - assertEq(address(morpho.incentivesVault()), address(incentivesVaultV2)); - } - function testOnlyOwnerShouldSetTreasuryVault() public { address treasuryVaultV2 = address(2); @@ -176,15 +159,6 @@ contract TestGovernance is TestSetup { assertEq(address(morpho.treasuryVault()), treasuryVaultV2); } - function testOnlyOwnerCanSetIsClaimRewardsPaused() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - morpho.setIsClaimRewardsPaused(true); - - morpho.setIsClaimRewardsPaused(true); - assertTrue(morpho.isClaimRewardsPaused()); - } - function testOnlyOwnerCanSetPauseStatusForAllMarkets() public { hevm.prank(address(0)); hevm.expectRevert("Ownable: caller is not the owner"); diff --git a/test/aave-v2/TestIncentivesVault.t.sol b/test/aave-v2/TestIncentivesVault.t.sol deleted file mode 100644 index 5f6b9ef8a..000000000 --- a/test/aave-v2/TestIncentivesVault.t.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; - -import "./setup/TestSetup.sol"; - -contract TestIncentivesVault is TestSetup { - using SafeTransferLib for ERC20; - - function testShouldNotSetBonusAboveMaxBasisPoints() public { - uint256 moreThanMaxBasisPoints = PercentageMath.PERCENTAGE_FACTOR + 1; - hevm.expectRevert(abi.encodeWithSelector(IncentivesVault.ExceedsMaxBasisPoints.selector)); - incentivesVault.setBonus(moreThanMaxBasisPoints); - } - - function testOnlyOwnerShouldSetBonus() public { - uint256 bonusToSet = 1; - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setBonus(bonusToSet); - - incentivesVault.setBonus(bonusToSet); - assertEq(incentivesVault.bonus(), bonusToSet); - } - - function testOnlyOwnerShouldSetIncentivesTreasuryVault() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setIncentivesTreasuryVault(address(treasuryVault)); - - incentivesVault.setIncentivesTreasuryVault(address(treasuryVault)); - assertEq(incentivesVault.incentivesTreasuryVault(), address(treasuryVault)); - } - - function testOnlyOwnerShouldSetOracle() public { - IOracle oracle = IOracle(address(1)); - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setOracle(oracle); - - incentivesVault.setOracle(oracle); - assertEq(address(incentivesVault.oracle()), address(oracle)); - } - - function testOnlyOwnerShouldSetPauseStatus() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setPauseStatus(true); - - incentivesVault.setPauseStatus(true); - assertTrue(incentivesVault.isPaused()); - - incentivesVault.setPauseStatus(false); - assertFalse(incentivesVault.isPaused()); - } - - function testOnlyOwnerShouldTransferTokensToDao() public { - hevm.prank(address(supplier1)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.transferTokensToDao(address(morphoToken), 1); - - incentivesVault.transferTokensToDao(address(morphoToken), 1); - assertEq(ERC20(morphoToken).balanceOf(address(treasuryVault)), 1); - } - - function testFailWhenContractNotActive() public { - incentivesVault.setPauseStatus(true); - - hevm.prank(address(morphoProxy)); - incentivesVault.tradeRewardTokensForMorphoTokens(address(1), 0); - } - - function testOnlyMorphoShouldTriggerRewardTradeFunction() public { - incentivesVault.setIncentivesTreasuryVault(address(1)); - uint256 amount = 100; - deal(REWARD_TOKEN, address(morphoProxy), amount); - - hevm.prank(address(morphoProxy)); - ERC20(REWARD_TOKEN).safeApprove(address(incentivesVault), amount); - - hevm.expectRevert(abi.encodeWithSignature("OnlyMorpho()")); - incentivesVault.tradeRewardTokensForMorphoTokens(address(2), amount); - - hevm.prank(address(morphoProxy)); - incentivesVault.tradeRewardTokensForMorphoTokens(address(2), amount); - } - - function testShouldGiveTheRightAmountOfRewards() public { - incentivesVault.setIncentivesTreasuryVault(address(1)); - uint256 toApprove = 1_000 ether; - deal(REWARD_TOKEN, address(morphoProxy), toApprove); - - hevm.prank(address(morphoProxy)); - ERC20(REWARD_TOKEN).safeApprove(address(incentivesVault), toApprove); - uint256 amount = 100; - - // O% bonus. - uint256 balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(address(morphoProxy)); - incentivesVault.tradeRewardTokensForMorphoTokens(address(2), amount); - uint256 balanceAfter = ERC20(morphoToken).balanceOf(address(2)); - assertEq(balanceAfter - balanceBefore, 100); - - // 10% bonus. - incentivesVault.setBonus(1_000); - balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(address(morphoProxy)); - incentivesVault.tradeRewardTokensForMorphoTokens(address(2), amount); - balanceAfter = ERC20(morphoToken).balanceOf(address(2)); - assertEq(balanceAfter - balanceBefore, 110); - } -} diff --git a/test/aave-v2/helpers/User.sol b/test/aave-v2/helpers/User.sol index e9ecb4689..3fee80b8b 100644 --- a/test/aave-v2/helpers/User.sol +++ b/test/aave-v2/helpers/User.sol @@ -10,12 +10,10 @@ contract User { Morpho internal morpho; ILendingPool public pool; - IAaveIncentivesController public aaveIncentivesController; constructor(Morpho _morpho) { morpho = _morpho; pool = _morpho.pool(); - aaveIncentivesController = _morpho.aaveIncentivesController(); } receive() external payable {} @@ -122,10 +120,6 @@ contract User { pool.borrow(_underlyingTokenAddress, _amount, 2, 0, address(this)); // 2 : variable rate | 0 : no refferal code } - function aaveClaimRewards(address[] memory assets) external { - aaveIncentivesController.claimRewards(assets, type(uint256).max, address(this)); - } - function liquidate( address _poolTokenBorrowed, address _poolTokenCollateral, @@ -145,13 +139,6 @@ contract User { morpho.setDefaultMaxGasForMatching(_maxGasForMatching); } - function claimRewards(address[] calldata _assets, bool _toSwap) - external - returns (uint256 claimedAmount) - { - return morpho.claimRewards(_assets, _toSwap); - } - function setTreasuryVault(address _newTreasuryVault) external { morpho.setTreasuryVault(_newTreasuryVault); } @@ -179,10 +166,4 @@ contract User { function setIsLiquidateBorrowPaused(address _poolToken, bool _isPaused) external { morpho.setIsLiquidateBorrowPaused(_poolToken, _isPaused); } - - function setMorphoAddresses(Morpho _morpho) public { - morpho = _morpho; - pool = _morpho.pool(); - aaveIncentivesController = _morpho.aaveIncentivesController(); - } } diff --git a/test/aave-v2/setup/TestSetup.sol b/test/aave-v2/setup/TestSetup.sol index e450bc11a..543e8282a 100644 --- a/test/aave-v2/setup/TestSetup.sol +++ b/test/aave-v2/setup/TestSetup.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity ^0.8.0; -import "src/aave-v2/interfaces/aave/IAaveIncentivesController.sol"; import "src/aave-v2/interfaces/aave/IVariableDebtToken.sol"; import "src/aave-v2/interfaces/aave/IAToken.sol"; import "src/aave-v2/interfaces/IMorpho.sol"; @@ -14,7 +13,6 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import "src/aave-v2/libraries/Types.sol"; import {InterestRatesManager} from "src/aave-v2/InterestRatesManager.sol"; -import {IncentivesVault} from "src/aave-v2/IncentivesVault.sol"; import {MatchingEngine} from "src/aave-v2/MatchingEngine.sol"; import {EntryPositionsManager} from "src/aave-v2/EntryPositionsManager.sol"; import {ExitPositionsManager} from "src/aave-v2/ExitPositionsManager.sol"; @@ -93,7 +91,6 @@ contract TestSetup is Config, Utils { treasuryVault = new User(morpho); morpho.setTreasuryVault(address(treasuryVault)); - morpho.setAaveIncentivesController(address(aaveIncentivesController)); /// Create markets /// @@ -105,19 +102,10 @@ contract TestSetup is Config, Utils { hevm.warp(block.timestamp + 100); - /// Create Morpho token, deploy Incentives Vault and activate rewards /// + /// Create Morpho token /// morphoToken = new MorphoToken(address(this)); dumbOracle = new DumbOracle(); - incentivesVault = new IncentivesVault( - IMorpho(address(morpho)), - morphoToken, - ERC20(REWARD_TOKEN), - address(treasuryVault), - dumbOracle - ); - morphoToken.transfer(address(incentivesVault), 1_000_000 ether); - morpho.setIncentivesVault(incentivesVault); lensImplV1 = new Lens(address(morpho)); lensProxy = new TransparentUpgradeableProxy(address(lensImplV1), address(proxyAdmin), ""); @@ -174,13 +162,11 @@ contract TestSetup is Config, Utils { function setContractsLabels() internal { hevm.label(address(morpho), "Morpho"); hevm.label(address(morphoToken), "MorphoToken"); - hevm.label(address(aaveIncentivesController), "AaveIncentivesController"); hevm.label(address(poolAddressesProvider), "PoolAddressesProvider"); hevm.label(address(pool), "Pool"); hevm.label(address(oracle), "AaveOracle"); hevm.label(address(treasuryVault), "TreasuryVault"); hevm.label(address(interestRatesManager), "InterestRatesManager"); - hevm.label(address(incentivesVault), "IncentivesVault"); } function createSigners(uint256 _nbOfSigners) internal { diff --git a/test/prod/aave-v2/TestLifecycle.t.sol b/test/prod/aave-v2/TestLifecycle.t.sol index b93183642..1c6ed8f65 100644 --- a/test/prod/aave-v2/TestLifecycle.t.sol +++ b/test/prod/aave-v2/TestLifecycle.t.sol @@ -37,7 +37,6 @@ contract TestLifecycle is TestSetup { uint256 scaledPoolBalance; // MorphoPosition position; - uint256 unclaimedRewardsBefore; } function _initMarketSideTest(TestMarket memory _market, uint256 _amount) @@ -105,21 +104,6 @@ contract TestLifecycle is TestSetup { string.concat(supply.market.symbol, " borrow delta matched") ); - address[] memory poolTokens = new address[](1); - poolTokens[0] = supply.market.poolToken; - if (address(rewardsManager) != address(0)) { - supply.unclaimedRewardsBefore = rewardsManager.getUserUnclaimedRewards( - poolTokens, - address(user) - ); - - assertEq( - supply.unclaimedRewardsBefore, - 0, - string.concat(supply.market.symbol, " unclaimed rewards") - ); - } - assertEq( ERC20(supply.market.underlying).balanceOf(address(morpho)), supply.morphoUnderlyingBalanceBefore, @@ -157,17 +141,6 @@ contract TestLifecycle is TestSetup { (supply.position.p2p, supply.position.pool, supply.position.total) = lens .getCurrentSupplyBalanceInOf(supply.market.poolToken, address(user)); - - if ( - supply.position.pool > 0 && - address(rewardsManager) != address(0) && - block.timestamp < aaveIncentivesController.DISTRIBUTION_END() - ) - assertGt( - rewardsManager.getUserUnclaimedRewards(poolTokens, address(user)), - supply.unclaimedRewardsBefore, - string.concat(supply.market.symbol, " unclaimed rewards after supply") - ); } function _borrow(TestMarket memory _market, uint256 _amount) @@ -214,15 +187,6 @@ contract TestLifecycle is TestSetup { string.concat(borrow.market.symbol, " supply delta matched") ); - address[] memory borrowedPoolTokens = new address[](1); - borrowedPoolTokens[0] = borrow.market.poolToken; - if (address(rewardsManager) != address(0)) { - borrow.unclaimedRewardsBefore = rewardsManager.getUserUnclaimedRewards( - borrowedPoolTokens, - address(user) - ); - } - assertEq( ERC20(borrow.market.underlying).balanceOf(address(morpho)), borrow.morphoUnderlyingBalanceBefore, @@ -260,17 +224,6 @@ contract TestLifecycle is TestSetup { (borrow.position.p2p, borrow.position.pool, borrow.position.total) = lens .getCurrentBorrowBalanceInOf(borrow.market.poolToken, address(user)); - - if ( - borrow.position.pool > 0 && - address(rewardsManager) != address(0) && - block.timestamp < aaveIncentivesController.DISTRIBUTION_END() - ) - assertGt( - rewardsManager.getUserUnclaimedRewards(borrowedPoolTokens, address(user)), - borrow.unclaimedRewardsBefore, - string.concat(borrow.market.symbol, " unclaimed rewards after borrow") - ); } function _repay(MarketSideTest memory borrow) internal virtual { diff --git a/test/prod/aave-v2/setup/TestSetup.sol b/test/prod/aave-v2/setup/TestSetup.sol index 377c4b1c2..4899f4432 100644 --- a/test/prod/aave-v2/setup/TestSetup.sol +++ b/test/prod/aave-v2/setup/TestSetup.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity ^0.8.0; -import "src/aave-v2/interfaces/aave/IAaveIncentivesController.sol"; import "src/aave-v2/interfaces/aave/IVariableDebtToken.sol"; import "src/aave-v2/interfaces/aave/IAToken.sol"; import "src/aave-v2/interfaces/lido/ILido.sol"; @@ -13,7 +12,6 @@ import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; import "@morpho-dao/morpho-utils/math/Math.sol"; import {InterestRatesManager} from "src/aave-v2/InterestRatesManager.sol"; -import {IncentivesVault} from "src/aave-v2/IncentivesVault.sol"; import {MatchingEngine} from "src/aave-v2/MatchingEngine.sol"; import {PositionsManagerUtils} from "src/aave-v2/PositionsManagerUtils.sol"; import {EntryPositionsManager} from "src/aave-v2/EntryPositionsManager.sol"; @@ -68,7 +66,6 @@ contract TestSetup is Config, Test { function initContracts() internal { lens = Lens(address(lensProxy)); morpho = Morpho(payable(morphoProxy)); - incentivesVault = morpho.incentivesVault(); entryPositionsManager = morpho.entryPositionsManager(); exitPositionsManager = morpho.exitPositionsManager(); interestRatesManager = morpho.interestRatesManager(); @@ -107,14 +104,12 @@ contract TestSetup is Config, Test { function setContractsLabels() internal { vm.label(address(poolAddressesProvider), "PoolAddressesProvider"); - vm.label(address(aaveIncentivesController), "IncentivesController"); vm.label(address(pool), "LendingPool"); vm.label(address(proxyAdmin), "ProxyAdmin"); vm.label(address(morphoImplV1), "MorphoImplV1"); vm.label(address(morpho), "Morpho"); vm.label(address(interestRatesManager), "InterestRatesManager"); vm.label(address(oracle), "Oracle"); - vm.label(address(incentivesVault), "IncentivesVault"); vm.label(address(lens), "Lens"); vm.label(address(aave), "AAVE"); From 342fb693626d9c22566423f100c0ecb520cb60b8 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 14:21:41 -0500 Subject: [PATCH 014/105] Rename liquidation threshold value --- src/aave-v2/ExitPositionsManager.sol | 4 ++- src/aave-v2/MorphoUtils.sol | 6 +++-- src/aave-v2/lens/UsersLens.sol | 10 +++---- src/aave-v2/libraries/Types.sol | 2 +- test/aave-v2/TestLens.t.sol | 40 +++++++++++++++------------- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/aave-v2/ExitPositionsManager.sol b/src/aave-v2/ExitPositionsManager.sol index 7636b0ad4..c933668bf 100644 --- a/src/aave-v2/ExitPositionsManager.sol +++ b/src/aave-v2/ExitPositionsManager.sol @@ -675,7 +675,9 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { Types.LiquidityData memory values = _liquidityData(_user, _poolToken, _withdrawnAmount, 0); return - values.debt > 0 ? values.liquidationThreshold.wadDiv(values.debt) : type(uint256).max; + values.debt > 0 + ? values.liquidationThresholdValue.wadDiv(values.debt) + : type(uint256).max; } /// @dev Checks whether the user can withdraw or not. diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index ffb4a6b16..71f0e5c06 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -318,7 +318,7 @@ abstract contract MorphoUtils is MorphoStorage { // Update LT variable for withdraw. if (assetCollateralValue > 0) - values.liquidationThreshold += assetCollateralValue.percentMul( + values.liquidationThresholdValue += assetCollateralValue.percentMul( assetData.liquidationThreshold ); @@ -326,7 +326,9 @@ abstract contract MorphoUtils is MorphoStorage { if (_poolToken == vars.poolToken && _amountWithdrawn > 0) { uint256 withdrawn = (_amountWithdrawn * vars.underlyingPrice) / assetData.tokenUnit; values.collateral -= withdrawn; - values.liquidationThreshold -= withdrawn.percentMul(assetData.liquidationThreshold); + values.liquidationThresholdValue -= withdrawn.percentMul( + assetData.liquidationThreshold + ); values.maxDebt -= withdrawn.percentMul(assetData.ltv); } } diff --git a/src/aave-v2/lens/UsersLens.sol b/src/aave-v2/lens/UsersLens.sol index e847b48c2..b3e0d401e 100644 --- a/src/aave-v2/lens/UsersLens.sol +++ b/src/aave-v2/lens/UsersLens.sol @@ -75,7 +75,7 @@ abstract contract UsersLens is IndexesLens { if ( liquidityData.debt > 0 && - liquidityData.liquidationThreshold.wadDiv(liquidityData.debt) <= + liquidityData.liquidationThresholdValue.wadDiv(liquidityData.debt) <= HEALTH_FACTOR_LIQUIDATION_THRESHOLD ) return (0, 0); @@ -98,7 +98,7 @@ abstract contract UsersLens is IndexesLens { if (assetData.liquidationThreshold > 0) withdrawable = Math.min( withdrawable, - ((liquidityData.liquidationThreshold - liquidityData.debt).percentDiv( + ((liquidityData.liquidationThresholdValue - liquidityData.debt).percentDiv( assetData.liquidationThreshold ) * assetData.tokenUnit) / assetData.underlyingPrice ); @@ -224,7 +224,7 @@ abstract contract UsersLens is IndexesLens { liquidityData.collateral += assetData.collateral; liquidityData.maxDebt += assetData.collateral.percentMul(assetData.ltv); - liquidityData.liquidationThreshold += assetData.collateral.percentMul( + liquidityData.liquidationThresholdValue += assetData.collateral.percentMul( assetData.liquidationThreshold ); liquidityData.debt += assetData.debt; @@ -241,7 +241,7 @@ abstract contract UsersLens is IndexesLens { liquidityData.collateral -= assetCollateral; liquidityData.maxDebt -= assetCollateral.percentMul(assetData.ltv); - liquidityData.liquidationThreshold -= assetCollateral.percentMul( + liquidityData.liquidationThresholdValue -= assetCollateral.percentMul( assetData.liquidationThreshold ); } @@ -324,7 +324,7 @@ abstract contract UsersLens is IndexesLens { ); if (liquidityData.debt == 0) return type(uint256).max; - return liquidityData.liquidationThreshold.wadDiv(liquidityData.debt); + return liquidityData.liquidationThresholdValue.wadDiv(liquidityData.debt); } /// @dev Checks whether a liquidation can be performed on a given user. diff --git a/src/aave-v2/libraries/Types.sol b/src/aave-v2/libraries/Types.sol index 7bbde9265..896efe29c 100644 --- a/src/aave-v2/libraries/Types.sol +++ b/src/aave-v2/libraries/Types.sol @@ -55,7 +55,7 @@ library Types { struct LiquidityData { uint256 collateral; // The collateral value (in ETH). uint256 maxDebt; // The max debt value (in ETH). - uint256 liquidationThreshold; // The liquidation threshold value (in ETH). + uint256 liquidationThresholdValue; // The liquidation threshold value (in ETH). uint256 debt; // The debt value (in ETH). } diff --git a/test/aave-v2/TestLens.t.sol b/test/aave-v2/TestLens.t.sol index 326cf5444..6474160b0 100644 --- a/test/aave-v2/TestLens.t.sol +++ b/test/aave-v2/TestLens.t.sol @@ -506,21 +506,21 @@ contract TestLens is TestSetup { expectedStates.collateral = (amount * underlyingPriceDai) / tokenUnitDai; expectedStates.debt = (toBorrow * underlyingPriceUsdc) / tokenUnitUsdc; - expectedStates.liquidationThreshold = expectedStates.collateral.percentMul( + expectedStates.liquidationThresholdValue = expectedStates.collateral.percentMul( liquidationThresholdDai ); expectedStates.maxDebt = expectedStates.collateral.percentMul(ltvDai); - uint256 healthFactor = states.liquidationThreshold.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThreshold.wadDiv( + uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( expectedStates.debt ); assertEq(states.collateral, expectedStates.collateral, "collateral"); assertEq(states.debt, expectedStates.debt, "debt"); assertEq( - states.liquidationThreshold, - expectedStates.liquidationThreshold, + states.liquidationThresholdValue, + expectedStates.liquidationThresholdValue, "liquidationThreshold" ); assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); @@ -550,7 +550,7 @@ contract TestLens is TestSetup { uint256 collateralValueToAdd = (to6Decimals(amount) * oracle.getAssetPrice(usdc)) / 10**decimals; expectedStates.collateral += collateralValueToAdd; - expectedStates.liquidationThreshold += collateralValueToAdd.percentMul( + expectedStates.liquidationThresholdValue += collateralValueToAdd.percentMul( liquidationThreshold ); expectedStates.maxDebt += collateralValueToAdd.percentMul(ltv); @@ -559,7 +559,7 @@ contract TestLens is TestSetup { (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); collateralValueToAdd = (amount * oracle.getAssetPrice(dai)) / 10**decimals; expectedStates.collateral += collateralValueToAdd; - expectedStates.liquidationThreshold += collateralValueToAdd.percentMul( + expectedStates.liquidationThresholdValue += collateralValueToAdd.percentMul( liquidationThreshold ); expectedStates.maxDebt += collateralValueToAdd.percentMul(ltv); @@ -572,16 +572,16 @@ contract TestLens is TestSetup { (, , , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); expectedStates.debt += (to6Decimals(toBorrow) * oracle.getAssetPrice(usdt)) / 10**decimals; - uint256 healthFactor = states.liquidationThreshold.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThreshold.wadDiv( + uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( expectedStates.debt ); assertApproxEqAbs(states.collateral, expectedStates.collateral, 2, "collateral"); assertApproxEqAbs(states.debt, expectedStates.debt, 1, "debt"); assertEq( - states.liquidationThreshold, - expectedStates.liquidationThreshold, + states.liquidationThresholdValue, + expectedStates.liquidationThresholdValue, "liquidationThreshold" ); assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); @@ -639,14 +639,18 @@ contract TestLens is TestSetup { uint256 collateralValueUsdt = (to6Decimals(amount) * oracle.getAssetPrice(usdt)) / 10**decimals; expectedStates.collateral += collateralValueUsdt; - expectedStates.liquidationThreshold += collateralValueUsdt.percentMul(liquidationThreshold); + expectedStates.liquidationThresholdValue += collateralValueUsdt.percentMul( + liquidationThreshold + ); expectedStates.maxDebt += collateralValueUsdt.percentMul(ltv); // DAI data (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); uint256 collateralValueDai = (amount * oracle.getAssetPrice(dai)) / 10**decimals; expectedStates.collateral += collateralValueDai; - expectedStates.liquidationThreshold += collateralValueDai.percentMul(liquidationThreshold); + expectedStates.liquidationThresholdValue += collateralValueDai.percentMul( + liquidationThreshold + ); expectedStates.maxDebt += collateralValueDai.percentMul(ltv); // USDC data @@ -657,16 +661,16 @@ contract TestLens is TestSetup { (, , , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); expectedStates.debt += (toBorrow * oracle.getAssetPrice(usdt)) / 10**decimals; - uint256 healthFactor = states.liquidationThreshold.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThreshold.wadDiv( + uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( expectedStates.debt ); assertApproxEqAbs(states.collateral, expectedStates.collateral, 1e3, "collateral"); assertEq(states.debt, expectedStates.debt, "debt"); assertEq( - states.liquidationThreshold, - expectedStates.liquidationThreshold, + states.liquidationThresholdValue, + expectedStates.liquidationThresholdValue, "liquidationThreshold" ); assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); @@ -1248,7 +1252,7 @@ contract TestLens is TestSetup { uint256 toRepay = lens.computeLiquidationRepayAmount(address(borrower1), aUsdc, aDai); - if (states.debt <= states.liquidationThreshold) { + if (states.debt <= states.liquidationThresholdValue) { assertEq(toRepay, 0, "Should return 0 when the position is solvent"); return; } From 874e11eada83cff8b0b3b93e884e7834423cfeb7 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 8 Dec 2022 20:23:23 +0100 Subject: [PATCH 015/105] Silence return variables --- src/compound/MorphoUtils.sol | 12 ++++-------- src/compound/interfaces/IMorpho.sol | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/compound/MorphoUtils.sol b/src/compound/MorphoUtils.sol index de401fab9..fc7ba181e 100644 --- a/src/compound/MorphoUtils.sol +++ b/src/compound/MorphoUtils.sol @@ -38,18 +38,14 @@ abstract contract MorphoUtils is MorphoStorage { /// @notice Returns all markets entered by a given user. /// @param _user The address of the user. - /// @return enteredMarkets_ The list of markets entered by this user. - function getEnteredMarkets(address _user) - external - view - returns (address[] memory enteredMarkets_) - { + /// @return The list of markets entered by this user. + function getEnteredMarkets(address _user) external view returns (address[] memory) { return enteredMarkets[_user]; } /// @notice Returns all created markets. - /// @return marketsCreated_ The list of market addresses. - function getAllMarkets() external view returns (address[] memory marketsCreated_) { + /// @return The list of market addresses. + function getAllMarkets() external view returns (address[] memory) { return marketsCreated; } diff --git a/src/compound/interfaces/IMorpho.sol b/src/compound/interfaces/IMorpho.sol index 44a0bfb46..187101bc9 100644 --- a/src/compound/interfaces/IMorpho.sol +++ b/src/compound/interfaces/IMorpho.sol @@ -42,8 +42,8 @@ interface IMorpho { /// GETTERS /// - function getEnteredMarkets(address _user) external view returns (address[] memory enteredMarkets_); - function getAllMarkets() external view returns (address[] memory marketsCreated_); + function getEnteredMarkets(address _user) external view returns (address[] memory); + function getAllMarkets() external view returns (address[] memory); function getHead(address _poolToken, Types.PositionType _positionType) external view returns (address head); function getNext(address _poolToken, Types.PositionType _positionType, address _user) external view returns (address next); From a3118308e6bce6173f79b77f10be02961af0ef7f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 8 Dec 2022 20:48:19 +0100 Subject: [PATCH 016/105] Update storage snapshot --- snapshots/.storage-layout-aave-v2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/snapshots/.storage-layout-aave-v2 b/snapshots/.storage-layout-aave-v2 index f0c43e4ff..e09a189e9 100644 --- a/snapshots/.storage-layout-aave-v2 +++ b/snapshots/.storage-layout-aave-v2 @@ -30,13 +30,13 @@ | deltas | mapping(address => struct Types.Delta) | 166 | 0 | 32 | src/aave-v2/Morpho.sol:Morpho | | borrowMask | mapping(address => bytes32) | 167 | 0 | 32 | src/aave-v2/Morpho.sol:Morpho | | addressesProvider | contract ILendingPoolAddressesProvider | 168 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | -| aaveIncentivesController | contract IAaveIncentivesController | 169 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | +| aaveIncentivesController | address | 169 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | pool | contract ILendingPool | 170 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | entryPositionsManager | contract IEntryPositionsManager | 171 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | exitPositionsManager | contract IExitPositionsManager | 172 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | interestRatesManager | contract IInterestRatesManager | 173 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | -| incentivesVault | contract IIncentivesVault | 174 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | -| rewardsManager | contract IRewardsManager | 175 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | +| incentivesVault | address | 174 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | +| rewardsManager | address | 175 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | treasuryVault | address | 176 | 0 | 20 | src/aave-v2/Morpho.sol:Morpho | | marketPauseStatus | mapping(address => struct Types.MarketPauseStatus) | 177 | 0 | 32 | src/aave-v2/Morpho.sol:Morpho | From afd4c929b8641064f0c13c313d30ff05977d8e2e Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 16:04:01 -0500 Subject: [PATCH 017/105] Add Compound error return --- src/compound/MorphoGovernance.sol | 5 ++++- src/compound/PositionsManager.sol | 18 ++++++++++++++---- test/compound/TestGovernance.t.sol | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 1839d02fd..3099a79dd 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -127,6 +127,9 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when the address is the zero address. error ZeroAddress(); + /// @notice Throws back Compound errors. + error CompoundError(uint256 errorCode); + /// UPGRADE /// /// @notice Initializes the Morpho contract. @@ -440,7 +443,7 @@ abstract contract MorphoGovernance is MorphoUtils { address[] memory marketToEnter = new address[](1); marketToEnter[0] = _poolToken; uint256[] memory results = comptroller.enterMarkets(marketToEnter); - if (results[0] != 0) revert MarketCreationFailedOnCompound(); + if (results[0] != 0) revert CompoundError(results[0]); // Same initial index as Compound. uint256 initialIndex; diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 2d2086617..aa34fefde 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -179,6 +179,9 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @notice Thrown when someone tries to liquidate but the liquidation with this asset as debt is paused. error LiquidateBorrowIsPaused(); + /// @notice Throws back Compound errors. + error CompoundError(uint256 errorCode); + /// STRUCTS /// // Struct to avoid stack too deep. @@ -926,7 +929,8 @@ contract PositionsManager is IPositionsManager, MatchingEngine { ICEther(_poolToken).mint{value: _amount}(); } else { _underlyingToken.safeApprove(_poolToken, _amount); - if (ICToken(_poolToken).mint(_amount) != 0) revert MintOnCompoundFailed(); + uint256 errorCode = ICToken(_poolToken).mint(_amount); + if (errorCode != 0) revert CompoundError(errorCode); } } @@ -936,7 +940,10 @@ contract PositionsManager is IPositionsManager, MatchingEngine { function _withdrawFromPool(address _poolToken, uint256 _amount) internal { // Withdraw only what is possible. The remaining dust is taken from the contract balance. _amount = CompoundMath.min(ICToken(_poolToken).balanceOfUnderlying(address(this)), _amount); - if (ICToken(_poolToken).redeemUnderlying(_amount) != 0) revert RedeemOnCompoundFailed(); + + uint256 errorCode = ICToken(_poolToken).redeemUnderlying(_amount); + if (errorCode != 0) revert CompoundError(errorCode); + if (_poolToken == cEth) IWETH(address(wEth)).deposit{value: _amount}(); // Turn the ETH received in wETH. } @@ -944,7 +951,9 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _poolToken The address of the pool token. /// @param _amount The amount of token (in underlying). function _borrowFromPool(address _poolToken, uint256 _amount) internal { - if ((ICToken(_poolToken).borrow(_amount) != 0)) revert BorrowOnCompoundFailed(); + uint256 errorCode = ICToken(_poolToken).borrow(_amount); + if (errorCode != 0) revert CompoundError(errorCode); + if (_poolToken == cEth) IWETH(address(wEth)).deposit{value: _amount}(); // Turn the ETH received in wETH. } @@ -969,7 +978,8 @@ contract PositionsManager is IPositionsManager, MatchingEngine { ICEther(_poolToken).repayBorrow{value: _amount}(); } else { _underlyingToken.safeApprove(_poolToken, _amount); - if (ICToken(_poolToken).repayBorrow(_amount) != 0) revert RepayOnCompoundFailed(); + uint256 errorCode = ICToken(_poolToken).repayBorrow(_amount); + if (errorCode != 0) revert CompoundError(errorCode); } } } diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index c3f647811..3ac17e899 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -20,7 +20,7 @@ contract TestGovernance is TestSetup { function testShouldRevertWhenCreatingMarketWithAnImproperMarket() public { Types.MarketParameters memory marketParams = Types.MarketParameters(3_333, 0); - hevm.expectRevert(abi.encodeWithSignature("MarketCreationFailedOnCompound()")); + hevm.expectRevert(abi.encodeWithSignature("CompoundError(uint256)", 9)); morpho.createMarket(address(supplier1), marketParams); } From 61269a676a602a70c9fff538f08ea47b2c0b913a Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 17:06:49 -0500 Subject: [PATCH 018/105] Add checks for deprecate and borrow pauses --- src/aave-v2/MorphoGovernance.sol | 8 ++++++++ src/compound/MorphoGovernance.sol | 9 +++++++++ test/aave-v2/TestGovernance.t.sol | 16 ++++++++++++++++ test/aave-v2/TestLens.t.sol | 1 + test/aave-v2/TestLiquidate.t.sol | 5 +++-- test/compound/TestGovernance.t.sol | 16 ++++++++++++++++ test/compound/TestLens.t.sol | 1 + test/compound/TestLiquidate.t.sol | 5 +++-- 8 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 2015ac2fa..7e108ce52 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -139,6 +139,12 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when the address is the zero address. error ZeroAddress(); + /// @notice Thrown when market borrow is not paused. + error BorrowNotPaused(); + + /// @notice Thrown when market is deprecated. + error MarketIsDeprecated(); + /// UPGRADE /// /// @notice Initializes the Morpho contract. @@ -302,6 +308,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { + if (marketPauseStatus[_poolToken].isDeprecated && !_isPaused) revert MarketIsDeprecated(); marketPauseStatus[_poolToken].isBorrowPaused = _isPaused; emit IsBorrowPausedSet(_poolToken, _isPaused); } @@ -397,6 +404,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { + if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 1839d02fd..d31e75b8d 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -127,6 +127,12 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when the address is the zero address. error ZeroAddress(); + /// @notice Thrown when market borrow is not paused. + error BorrowNotPaused(); + + /// @notice Thrown when market is deprecated. + error MarketIsDeprecated(); + /// UPGRADE /// /// @notice Initializes the Morpho contract. @@ -279,6 +285,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { + if (marketPauseStatus[_poolToken].isDeprecated && !_isPaused) revert MarketIsDeprecated(); marketPauseStatus[_poolToken].isBorrowPaused = _isPaused; emit IsBorrowPausedSet(_poolToken, _isPaused); } @@ -374,6 +381,8 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { + if (!marketPauseStatus[_poolToken].isBorrowPaused && _isDeprecated) + revert BorrowNotPaused(); marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/test/aave-v2/TestGovernance.t.sol b/test/aave-v2/TestGovernance.t.sol index ba4e308cc..45b288dcc 100644 --- a/test/aave-v2/TestGovernance.t.sol +++ b/test/aave-v2/TestGovernance.t.sol @@ -203,6 +203,8 @@ contract TestGovernance is TestSetup { } function testOnlyOwnerShouldSetDeprecatedMarket() public { + morpho.setIsBorrowPaused(aDai, true); + hevm.prank(address(supplier1)); hevm.expectRevert("Ownable: caller is not the owner"); morpho.setIsDeprecated(aDai, true); @@ -440,4 +442,18 @@ contract TestGovernance is TestSetup { function testFailCallIncreaseP2PDeltasFromImplementation() public { exitPositionsManager.increaseP2PDeltasLogic(aDai, 0); } + + function testDeprecateCycle() public { + hevm.expectRevert(abi.encodeWithSignature("BorrowNotPaused()")); + morpho.setIsDeprecated(aDai, true); + + morpho.setIsBorrowPaused(aDai, true); + morpho.setIsDeprecated(aDai, true); + + hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); + morpho.setIsBorrowPaused(aDai, false); + + morpho.setIsDeprecated(aDai, false); + morpho.setIsBorrowPaused(aDai, false); + } } diff --git a/test/aave-v2/TestLens.t.sol b/test/aave-v2/TestLens.t.sol index 326cf5444..c7c2e6a44 100644 --- a/test/aave-v2/TestLens.t.sol +++ b/test/aave-v2/TestLens.t.sol @@ -1524,6 +1524,7 @@ contract TestLens is TestSetup { } function testGetMarketPauseStatusesDeprecatedMarket() public { + morpho.setIsBorrowPaused(aDai, true); morpho.setIsDeprecated(aDai, true); assertTrue(lens.getMarketPauseStatus(aDai).isDeprecated); } diff --git a/test/aave-v2/TestLiquidate.t.sol b/test/aave-v2/TestLiquidate.t.sol index 01ac0cabc..2cfa347b9 100644 --- a/test/aave-v2/TestLiquidate.t.sol +++ b/test/aave-v2/TestLiquidate.t.sol @@ -30,12 +30,13 @@ contract TestLiquidate is TestSetup { uint256 amount = 10_000 ether; uint256 collateral = to6Decimals(3 * amount); - morpho.setIsDeprecated(aDai, true); - borrower1.approve(usdc, address(morpho), collateral); borrower1.supply(aUsdc, collateral); borrower1.borrow(aDai, amount); + morpho.setIsBorrowPaused(aDai, true); + morpho.setIsDeprecated(aDai, true); + (, uint256 supplyOnPoolBefore) = morpho.supplyBalanceInOf(aUsdc, address(borrower1)); (, uint256 borrowOnPoolBefore) = morpho.borrowBalanceInOf(aDai, address(borrower1)); diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index c3f647811..b796507d5 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -230,6 +230,8 @@ contract TestGovernance is TestSetup { } function testOnlyOwnerShouldSetDeprecatedMarket() public { + morpho.setIsBorrowPaused(cDai, true); + hevm.prank(address(supplier1)); hevm.expectRevert("Ownable: caller is not the owner"); morpho.setIsDeprecated(cDai, true); @@ -447,4 +449,18 @@ contract TestGovernance is TestSetup { function testFailCallIncreaseP2PDeltasFromImplementation() public { positionsManager.increaseP2PDeltasLogic(cDai, 0); } + + function testDeprecateCycle() public { + hevm.expectRevert(abi.encodeWithSignature("BorrowNotPaused()")); + morpho.setIsDeprecated(cDai, true); + + morpho.setIsBorrowPaused(cDai, true); + morpho.setIsDeprecated(cDai, true); + + hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); + morpho.setIsBorrowPaused(cDai, false); + + morpho.setIsDeprecated(cDai, false); + morpho.setIsBorrowPaused(cDai, false); + } } diff --git a/test/compound/TestLens.t.sol b/test/compound/TestLens.t.sol index 20e0e77e6..3504164e1 100644 --- a/test/compound/TestLens.t.sol +++ b/test/compound/TestLens.t.sol @@ -1606,6 +1606,7 @@ contract TestLens is TestSetup { } function testGetMarketPauseStatusesDeprecatedMarket() public { + morpho.setIsBorrowPaused(cDai, true); morpho.setIsDeprecated(cDai, true); assertTrue(lens.getMarketPauseStatus(cDai).isDeprecated); } diff --git a/test/compound/TestLiquidate.t.sol b/test/compound/TestLiquidate.t.sol index ef95b854e..9be81566a 100644 --- a/test/compound/TestLiquidate.t.sol +++ b/test/compound/TestLiquidate.t.sol @@ -29,12 +29,13 @@ contract TestLiquidate is TestSetup { uint256 amount = 10_000 ether; uint256 collateral = to6Decimals(3 * amount); - morpho.setIsDeprecated(cDai, true); - borrower1.approve(usdc, address(morpho), collateral); borrower1.supply(cUsdc, collateral); borrower1.borrow(cDai, amount); + morpho.setIsBorrowPaused(cDai, true); + morpho.setIsDeprecated(cDai, true); + moveOneBlockForwardBorrowRepay(); (, uint256 supplyOnPoolBefore) = morpho.supplyBalanceInOf(cUsdc, address(borrower1)); From 29d9b631b7806b04f65bc3f89f1030467c6466b0 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 17:30:13 -0500 Subject: [PATCH 019/105] Fix natspec --- src/compound/MorphoGovernance.sol | 2 +- src/compound/MorphoUtils.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 1839d02fd..99d8c4a7e 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -107,7 +107,7 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Emitted when a new market is created. /// @param _poolToken The address of the market that has been created. /// @param _reserveFactor The reserve factor set for this market. - /// @param _poolToken The P2P index cursor set for this market. + /// @param _p2pIndexCursor The P2P index cursor set for this market. event MarketCreated(address indexed _poolToken, uint16 _reserveFactor, uint16 _p2pIndexCursor); /// ERRORS /// diff --git a/src/compound/MorphoUtils.sol b/src/compound/MorphoUtils.sol index de401fab9..050cdc0fe 100644 --- a/src/compound/MorphoUtils.sol +++ b/src/compound/MorphoUtils.sol @@ -94,7 +94,7 @@ abstract contract MorphoUtils is MorphoStorage { } /// @notice Updates the peer-to-peer indexes. - /// @dev Note: This function updates the exchange rate on Compound. As a consequence only a call to exchangeRatesStored() is necessary to get the most up to date exchange rate. + /// @dev Note: This function updates the exchange rate on Compound. As a consequence only a call to exchangeRateStored() is necessary to get the most up to date exchange rate. /// @param _poolToken The address of the market to update. function updateP2PIndexes(address _poolToken) external isMarketCreated(_poolToken) { _updateP2PIndexes(_poolToken); @@ -103,7 +103,7 @@ abstract contract MorphoUtils is MorphoStorage { /// INTERNAL /// /// @dev Updates the peer-to-peer indexes. - /// @dev Note: This function updates the exchange rate on Compound. As a consequence only a call to exchangeRatesStored() is necessary to get the most up to date exchange rate. + /// @dev Note: This function updates the exchange rate on Compound. As a consequence only a call to exchangeRateStored() is necessary to get the most up to date exchange rate. /// @param _poolToken The address of the market to update. function _updateP2PIndexes(address _poolToken) internal { address(interestRatesManager).functionDelegateCall( From 3788f8dbf606e4febe3932d166d2b246ccd6f830 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 Dec 2022 17:32:28 -0500 Subject: [PATCH 020/105] Add aave natspec fix --- src/aave-v2/MorphoGovernance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 2015ac2fa..f73ea8583 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -116,7 +116,7 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Emitted when a new market is created. /// @param _poolToken The address of the market that has been created. /// @param _reserveFactor The reserve factor set for this market. - /// @param _poolToken The P2P index cursor set for this market. + /// @param _p2pIndexCursor The P2P index cursor set for this market. event MarketCreated(address indexed _poolToken, uint16 _reserveFactor, uint16 _p2pIndexCursor); /// ERRORS /// From 48a36c4089eb6dc01758da04ca16431efe4c9737 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 09:10:13 +0100 Subject: [PATCH 021/105] Remove deprecated getters --- src/aave-v2/interfaces/IMorpho.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/aave-v2/interfaces/IMorpho.sol b/src/aave-v2/interfaces/IMorpho.sol index b93883e05..4c043af24 100644 --- a/src/aave-v2/interfaces/IMorpho.sol +++ b/src/aave-v2/interfaces/IMorpho.sol @@ -38,12 +38,9 @@ interface IMorpho { function p2pBorrowIndex(address) external view returns (uint256); function poolIndexes(address) external view returns (Types.PoolIndexes memory); function interestRatesManager() external view returns (IInterestRatesManager); - function rewardsManager() external view returns (address); function entryPositionsManager() external view returns (IEntryPositionsManager); function exitPositionsManager() external view returns (IExitPositionsManager); - function aaveIncentivesController() external view returns (address); function addressesProvider() external view returns (ILendingPoolAddressesProvider); - function incentivesVault() external view returns (address); function pool() external view returns (ILendingPool); function treasuryVault() external view returns (address); function borrowMask(address) external view returns (bytes32); From 9a21aafe47216cf5805a274a6426f27896d9b301 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 09:18:32 +0100 Subject: [PATCH 022/105] Remove unecessary stack variables --- src/aave-v2/MatchingEngine.sol | 42 +++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/aave-v2/MatchingEngine.sol b/src/aave-v2/MatchingEngine.sol index c4d4ee46d..96d2d3491 100644 --- a/src/aave-v2/MatchingEngine.sol +++ b/src/aave-v2/MatchingEngine.sol @@ -299,33 +299,43 @@ abstract contract MatchingEngine is MorphoUtils { /// @param _poolToken The address of the market on which to update the suppliers data structure. /// @param _user The address of the user. function _updateSupplierInDS(address _poolToken, address _user) internal { - Types.SupplyBalance storage supplierSupplyBalance = supplyBalanceInOf[_poolToken][_user]; - uint256 onPool = supplierSupplyBalance.onPool; - uint256 inP2P = supplierSupplyBalance.inP2P; + Types.SupplyBalance memory supplierSupplyBalance = supplyBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketSuppliersOnPool = suppliersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketSuppliersInP2P = suppliersInP2P[_poolToken]; - uint256 formerValueOnPool = marketSuppliersOnPool.getValueOf(_user); - uint256 formerValueInP2P = marketSuppliersInP2P.getValueOf(_user); - - marketSuppliersOnPool.update(_user, formerValueOnPool, onPool, maxSortedUsers); - marketSuppliersInP2P.update(_user, formerValueInP2P, inP2P, maxSortedUsers); + marketSuppliersOnPool.update( + _user, + marketSuppliersOnPool.getValueOf(_user), + supplierSupplyBalance.onPool, + maxSortedUsers + ); + marketSuppliersInP2P.update( + _user, + marketSuppliersInP2P.getValueOf(_user), + supplierSupplyBalance.inP2P, + maxSortedUsers + ); } /// @notice Updates the given `_user`'s position in the borrower data structures. /// @param _poolToken The address of the market on which to update the borrowers data structure. /// @param _user The address of the user. function _updateBorrowerInDS(address _poolToken, address _user) internal { - Types.BorrowBalance storage borrowerBorrowBalance = borrowBalanceInOf[_poolToken][_user]; - uint256 onPool = borrowerBorrowBalance.onPool; - uint256 inP2P = borrowerBorrowBalance.inP2P; + Types.BorrowBalance memory borrowerBorrowBalance = borrowBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketBorrowersOnPool = borrowersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketBorrowersInP2P = borrowersInP2P[_poolToken]; - uint256 formerValueOnPool = marketBorrowersOnPool.getValueOf(_user); - uint256 formerValueInP2P = marketBorrowersInP2P.getValueOf(_user); - - marketBorrowersOnPool.update(_user, formerValueOnPool, onPool, maxSortedUsers); - marketBorrowersInP2P.update(_user, formerValueInP2P, inP2P, maxSortedUsers); + marketBorrowersOnPool.update( + _user, + marketBorrowersOnPool.getValueOf(_user), + borrowerBorrowBalance.onPool, + maxSortedUsers + ); + marketBorrowersInP2P.update( + _user, + marketBorrowersInP2P.getValueOf(_user), + borrowerBorrowBalance.inP2P, + maxSortedUsers + ); } } From e3ba4364a8b02b50f4028cf07c5366df7c0cc942 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 9 Dec 2022 09:55:14 +0100 Subject: [PATCH 023/105] =?UTF-8?q?=F0=9F=94=A5=20(#1539)=20Remove=20unuse?= =?UTF-8?q?d=20struct=20in=20EntryPositionManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/aave-v2/EntryPositionsManager.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index f0741bd3f..0a79f5ddd 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -72,13 +72,6 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils uint256 toRepay; } - // Struct to avoid stack too deep. - struct BorrowAllowedVars { - uint256 i; - bytes32 userMarkets; - uint256 numberOfMarketsCreated; - } - /// LOGIC /// /// @dev Implements supply logic. From 27b425d669484547984ac0d8540799c22443598d Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 9 Dec 2022 09:57:14 +0100 Subject: [PATCH 024/105] =?UTF-8?q?=F0=9F=94=A5=20(#1540)=20Remove=20unuse?= =?UTF-8?q?d=20struct=20in=20ExitPositionManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/aave-v2/ExitPositionsManager.sol | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/aave-v2/ExitPositionsManager.sol b/src/aave-v2/ExitPositionsManager.sol index 7636b0ad4..9724efef0 100644 --- a/src/aave-v2/ExitPositionsManager.sol +++ b/src/aave-v2/ExitPositionsManager.sol @@ -136,13 +136,6 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { bool liquidationAllowed; // Whether the liquidation is allowed or not. } - // Struct to avoid stack too deep. - struct HealthFactorVars { - uint256 i; - bytes32 userMarkets; - uint256 numberOfMarketsCreated; - } - /// LOGIC /// /// @dev Implements withdraw logic with security checks. @@ -666,11 +659,8 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { address _poolToken, uint256 _withdrawnAmount ) internal returns (uint256) { - HealthFactorVars memory vars; - vars.userMarkets = userMarkets[_user]; - // If the user is not borrowing any asset, return an infinite health factor. - if (!_isBorrowingAny(vars.userMarkets)) return type(uint256).max; + if (!_isBorrowingAny(userMarkets[_user])) return type(uint256).max; Types.LiquidityData memory values = _liquidityData(_user, _poolToken, _withdrawnAmount, 0); From 2033dd087f59c6d1b2ca5bc117d3d6909df7f686 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 9 Dec 2022 10:04:02 +0100 Subject: [PATCH 025/105] =?UTF-8?q?=F0=9F=8E=A8=20Move=20struct=20to=20cor?= =?UTF-8?q?rect=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/aave-v2/MorphoUtils.sol | 14 +++++++++++++- src/aave-v2/libraries/Types.sol | 9 --------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index ffb4a6b16..2a08ff437 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -32,6 +32,18 @@ abstract contract MorphoUtils is MorphoStorage { /// @notice Thrown when the market is not created yet. error MarketNotCreated(); + /// STRUCTS /// + + // Struct to avoid stack too deep. + struct LiquidityVars { + address poolToken; + uint256 poolTokensLength; + bytes32 userMarkets; + bytes32 borrowMask; + address underlyingToken; + uint256 underlyingPrice; + } + /// MODIFIERS /// /// @notice Prevents to update a market not created yet. @@ -254,7 +266,7 @@ abstract contract MorphoUtils is MorphoStorage { ) internal returns (Types.LiquidityData memory values) { IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); Types.AssetLiquidityData memory assetData; - Types.LiquidityStackVars memory vars; + LiquidityVars memory vars; DataTypes.UserConfigurationMap memory morphoPoolConfig = pool.getUserConfiguration( address(this) diff --git a/src/aave-v2/libraries/Types.sol b/src/aave-v2/libraries/Types.sol index 7bbde9265..1ae500702 100644 --- a/src/aave-v2/libraries/Types.sol +++ b/src/aave-v2/libraries/Types.sol @@ -85,13 +85,4 @@ library Types { bool isLiquidateBorrowPaused; // Whether the liquidatation on this market as borrow is paused or not. bool isDeprecated; // Whether a market is deprecated or not. } - - struct LiquidityStackVars { - address poolToken; - uint256 poolTokensLength; - bytes32 userMarkets; - bytes32 borrowMask; - address underlyingToken; - uint256 underlyingPrice; - } } From 0d6aae38d31a981409b099b44a04a1d6d215246e Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 11:52:54 +0100 Subject: [PATCH 026/105] Remove setIncentivesVault --- src/aave-v2/interfaces/IMorpho.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aave-v2/interfaces/IMorpho.sol b/src/aave-v2/interfaces/IMorpho.sol index 4c043af24..4384e1a7d 100644 --- a/src/aave-v2/interfaces/IMorpho.sol +++ b/src/aave-v2/interfaces/IMorpho.sol @@ -60,7 +60,6 @@ interface IMorpho { function setMaxSortedUsers(uint256 _newMaxSortedUsers) external; function setDefaultMaxGasForMatching(Types.MaxGasForMatching memory _maxGasForMatching) external; - function setIncentivesVault(address _newIncentivesVault) external; function setExitPositionsManager(IExitPositionsManager _exitPositionsManager) external; function setEntryPositionsManager(IEntryPositionsManager _entryPositionsManager) external; function setInterestRatesManager(IInterestRatesManager _interestRatesManager) external; From 907b0a991394731781c66bd27424b064a0194870 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 12:04:22 +0100 Subject: [PATCH 027/105] Rename liquidity data vars --- src/aave-v2/EntryPositionsManager.sol | 2 +- src/aave-v2/ExitPositionsManager.sol | 4 +- src/aave-v2/MorphoUtils.sol | 18 ++-- src/aave-v2/lens/UsersLens.sol | 41 ++++---- src/aave-v2/libraries/Types.sol | 12 +-- test/aave-v2/TestLens.t.sol | 138 +++++++++++++------------- 6 files changed, 111 insertions(+), 104 deletions(-) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index f0741bd3f..f99f204fe 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -290,6 +290,6 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils uint256 _borrowedAmount ) internal returns (bool) { Types.LiquidityData memory values = _liquidityData(_user, _poolToken, 0, _borrowedAmount); - return values.debt <= values.maxDebt; + return values.debtEth <= values.borrowableEth; } } diff --git a/src/aave-v2/ExitPositionsManager.sol b/src/aave-v2/ExitPositionsManager.sol index c933668bf..9e0523f15 100644 --- a/src/aave-v2/ExitPositionsManager.sol +++ b/src/aave-v2/ExitPositionsManager.sol @@ -675,8 +675,8 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { Types.LiquidityData memory values = _liquidityData(_user, _poolToken, _withdrawnAmount, 0); return - values.debt > 0 - ? values.liquidationThresholdValue.wadDiv(values.debt) + values.debtEth > 0 + ? values.liquidationThresholdEth.wadDiv(values.debtEth) : type(uint256).max; } diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index 71f0e5c06..75182c5fc 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -290,7 +290,7 @@ abstract contract MorphoUtils is MorphoStorage { } if (_isBorrowing(vars.userMarkets, vars.borrowMask)) { - values.debt += _debtValue( + values.debtEth += _debtValue( vars.poolToken, _user, vars.underlyingPrice, @@ -307,29 +307,31 @@ abstract contract MorphoUtils is MorphoStorage { vars.underlyingPrice, assetData.tokenUnit ); - values.collateral += assetCollateralValue; + values.collateralEth += assetCollateralValue; // Calculate LTV for borrow. - values.maxDebt += assetCollateralValue.percentMul(assetData.ltv); + values.borrowableEth += assetCollateralValue.percentMul(assetData.ltv); } // Update debt variable for borrowed token. if (_poolToken == vars.poolToken && _amountBorrowed > 0) - values.debt += (_amountBorrowed * vars.underlyingPrice).divUp(assetData.tokenUnit); + values.debtEth += (_amountBorrowed * vars.underlyingPrice).divUp( + assetData.tokenUnit + ); // Update LT variable for withdraw. if (assetCollateralValue > 0) - values.liquidationThresholdValue += assetCollateralValue.percentMul( + values.liquidationThresholdEth += assetCollateralValue.percentMul( assetData.liquidationThreshold ); // Subtract withdrawn amount from liquidation threshold and collateral. if (_poolToken == vars.poolToken && _amountWithdrawn > 0) { uint256 withdrawn = (_amountWithdrawn * vars.underlyingPrice) / assetData.tokenUnit; - values.collateral -= withdrawn; - values.liquidationThresholdValue -= withdrawn.percentMul( + values.collateralEth -= withdrawn; + values.liquidationThresholdEth -= withdrawn.percentMul( assetData.liquidationThreshold ); - values.maxDebt -= withdrawn.percentMul(assetData.ltv); + values.borrowableEth -= withdrawn.percentMul(assetData.ltv); } } } diff --git a/src/aave-v2/lens/UsersLens.sol b/src/aave-v2/lens/UsersLens.sol index b3e0d401e..f2f976cbe 100644 --- a/src/aave-v2/lens/UsersLens.sol +++ b/src/aave-v2/lens/UsersLens.sol @@ -74,8 +74,8 @@ abstract contract UsersLens is IndexesLens { ); if ( - liquidityData.debt > 0 && - liquidityData.liquidationThresholdValue.wadDiv(liquidityData.debt) <= + liquidityData.debtEth > 0 && + liquidityData.liquidationThresholdEth.wadDiv(liquidityData.debtEth) <= HEALTH_FACTOR_LIQUIDATION_THRESHOLD ) return (0, 0); @@ -83,22 +83,22 @@ abstract contract UsersLens is IndexesLens { _poolToken ); - if (liquidityData.debt < liquidityData.maxDebt) + if (liquidityData.debtEth < liquidityData.borrowableEth) borrowable = Math.min( poolTokenBalance, - ((liquidityData.maxDebt - liquidityData.debt) * assetData.tokenUnit) / + ((liquidityData.borrowableEth - liquidityData.debtEth) * assetData.tokenUnit) / assetData.underlyingPrice ); withdrawable = Math.min( poolTokenBalance, - (assetData.collateral * assetData.tokenUnit) / assetData.underlyingPrice + (assetData.collateralEth * assetData.tokenUnit) / assetData.underlyingPrice ); if (assetData.liquidationThreshold > 0) withdrawable = Math.min( withdrawable, - ((liquidityData.liquidationThresholdValue - liquidityData.debt).percentDiv( + ((liquidityData.liquidationThresholdEth - liquidityData.debtEth).percentDiv( assetData.liquidationThreshold ) * assetData.tokenUnit) / assetData.underlyingPrice ); @@ -222,26 +222,25 @@ abstract contract UsersLens is IndexesLens { oracle ); - liquidityData.collateral += assetData.collateral; - liquidityData.maxDebt += assetData.collateral.percentMul(assetData.ltv); - liquidityData.liquidationThresholdValue += assetData.collateral.percentMul( + liquidityData.collateralEth += assetData.collateralEth; + liquidityData.borrowableEth += assetData.collateralEth.percentMul(assetData.ltv); + liquidityData.liquidationThresholdEth += assetData.collateralEth.percentMul( assetData.liquidationThreshold ); - liquidityData.debt += assetData.debt; + liquidityData.debtEth += assetData.debtEth; if (_poolToken == poolToken) { if (_borrowedAmount > 0) - liquidityData.debt += (_borrowedAmount * assetData.underlyingPrice).divUp( - assetData.tokenUnit - ); + liquidityData.debtEth += (_borrowedAmount * assetData.underlyingPrice) + .divUp(assetData.tokenUnit); if (_withdrawnAmount > 0) { uint256 assetCollateral = (_withdrawnAmount * assetData.underlyingPrice) / assetData.tokenUnit; - liquidityData.collateral -= assetCollateral; - liquidityData.maxDebt -= assetCollateral.percentMul(assetData.ltv); - liquidityData.liquidationThresholdValue -= assetCollateral.percentMul( + liquidityData.collateralEth -= assetCollateral; + liquidityData.borrowableEth -= assetCollateral.percentMul(assetData.ltv); + liquidityData.liquidationThresholdEth -= assetCollateral.percentMul( assetData.liquidationThreshold ); } @@ -291,8 +290,10 @@ abstract contract UsersLens is IndexesLens { ); assetData.tokenUnit = 10**assetData.decimals; - assetData.debt = (totalDebtBalance * assetData.underlyingPrice).divUp(assetData.tokenUnit); - assetData.collateral = + assetData.debtEth = (totalDebtBalance * assetData.underlyingPrice).divUp( + assetData.tokenUnit + ); + assetData.collateralEth = (totalCollateralBalance * assetData.underlyingPrice) / assetData.tokenUnit; } @@ -322,9 +323,9 @@ abstract contract UsersLens is IndexesLens { _withdrawnAmount, _borrowedAmount ); - if (liquidityData.debt == 0) return type(uint256).max; + if (liquidityData.debtEth == 0) return type(uint256).max; - return liquidityData.liquidationThresholdValue.wadDiv(liquidityData.debt); + return liquidityData.liquidationThresholdEth.wadDiv(liquidityData.debtEth); } /// @dev Checks whether a liquidation can be performed on a given user. diff --git a/src/aave-v2/libraries/Types.sol b/src/aave-v2/libraries/Types.sol index 896efe29c..b1f07b533 100644 --- a/src/aave-v2/libraries/Types.sol +++ b/src/aave-v2/libraries/Types.sol @@ -48,15 +48,15 @@ library Types { uint256 liquidationThreshold; // The liquidation threshold applied on this token (in basis point). uint256 ltv; // The LTV applied on this token (in basis point). uint256 underlyingPrice; // The price of the token (in ETH). - uint256 collateral; // The collateral value of the asset (in ETH). - uint256 debt; // The debt value of the asset (in ETH). + uint256 collateralEth; // The collateral value of the asset (in ETH). + uint256 debtEth; // The debt value of the asset (in ETH). } struct LiquidityData { - uint256 collateral; // The collateral value (in ETH). - uint256 maxDebt; // The max debt value (in ETH). - uint256 liquidationThresholdValue; // The liquidation threshold value (in ETH). - uint256 debt; // The debt value (in ETH). + uint256 collateralEth; // The collateral value (in ETH). + uint256 borrowableEth; // The max debt value (in ETH). + uint256 liquidationThresholdEth; // The liquidation threshold value (in ETH). + uint256 debtEth; // The debt value (in ETH). } // Variables are packed together to save gas (will not exceed their limit during Morpho's lifetime). diff --git a/test/aave-v2/TestLens.t.sol b/test/aave-v2/TestLens.t.sol index 6474160b0..39e833d8c 100644 --- a/test/aave-v2/TestLens.t.sol +++ b/test/aave-v2/TestLens.t.sol @@ -41,8 +41,8 @@ contract TestLens is TestSetup { assertEq(assetData.decimals, decimals); assertEq(assetData.underlyingPrice, underlyingPrice); assertEq(assetData.tokenUnit, tokenUnit); - assertEq(assetData.collateral, 0); - assertEq(assetData.debt, 0); + assertEq(assetData.collateralEth, 0); + assertEq(assetData.debtEth, 0); } function testUserLiquidityDataForAssetWithSupply() public { @@ -68,8 +68,8 @@ contract TestLens is TestSetup { assertEq(assetData.liquidationThreshold, liquidationThreshold, "liquidationThreshold"); assertEq(assetData.underlyingPrice, underlyingPrice, "underlyingPrice"); assertEq(assetData.tokenUnit, tokenUnit, "tokenUnit"); - assertEq(assetData.collateral, collateral, "collateral"); - assertEq(assetData.debt, 0, "debt"); + assertEq(assetData.collateralEth, collateral, "collateral"); + assertEq(assetData.debtEth, 0, "debt"); } function testUserLiquidityDataForAssetWithSupplyAndBorrow() public { @@ -99,8 +99,8 @@ contract TestLens is TestSetup { assertEq(assetData.underlyingPrice, underlyingPrice, "underlyingPrice"); assertEq(assetData.decimals, decimals, "decimals"); assertEq(assetData.tokenUnit, tokenUnit, "tokenUnit"); - assertApproxEqAbs(assetData.collateral, collateral, 2, "collateral"); - assertEq(assetData.debt, debt, "debt"); + assertApproxEqAbs(assetData.collateralEth, collateral, 2, "collateral"); + assertEq(assetData.debtEth, debt, "debt"); } function testUserLiquidityDataForAssetWithSupplyAndBorrowWithMultipleAssets() public { @@ -131,7 +131,7 @@ contract TestLens is TestSetup { .getParamsMemory(); expectedDataUsdc.underlyingPrice = oracle.getAssetPrice(usdc); expectedDataUsdc.tokenUnit = 10**decimalsUsdc; - expectedDataUsdc.debt = + expectedDataUsdc.debtEth = (toBorrow * expectedDataUsdc.underlyingPrice) / expectedDataUsdc.tokenUnit; @@ -147,8 +147,8 @@ contract TestLens is TestSetup { "underlyingPriceUsdc" ); assertEq(assetDataUsdc.tokenUnit, expectedDataUsdc.tokenUnit, "tokenUnitUsdc"); - assertEq(assetDataUsdc.collateral, 0, "collateralUsdc"); - assertEq(assetDataUsdc.debt, expectedDataUsdc.debt, "debtUsdc"); + assertEq(assetDataUsdc.collateralEth, 0, "collateralUsdc"); + assertEq(assetDataUsdc.debtEth, expectedDataUsdc.debtEth, "debtUsdc"); Types.AssetLiquidityData memory expectedDataDai; uint256 decimalsDai; @@ -158,7 +158,7 @@ contract TestLens is TestSetup { .getParamsMemory(); expectedDataDai.underlyingPrice = oracle.getAssetPrice(dai); expectedDataDai.tokenUnit = 10**decimalsDai; - expectedDataDai.collateral = + expectedDataDai.collateralEth = (amount * expectedDataDai.underlyingPrice) / expectedDataDai.tokenUnit; @@ -174,8 +174,8 @@ contract TestLens is TestSetup { "underlyingPriceDai" ); assertEq(assetDataDai.tokenUnit, expectedDataDai.tokenUnit, "tokenUnitDai"); - assertEq(assetDataDai.collateral, expectedDataDai.collateral, "collateralDai"); - assertEq(assetDataDai.debt, 0, "debtDai"); + assertEq(assetDataDai.collateralEth, expectedDataDai.collateralEth, "collateralDai"); + assertEq(assetDataDai.debtEth, 0, "debtDai"); } function testMaxCapacitiesWithNothing() public { @@ -206,9 +206,10 @@ contract TestLens is TestSetup { oracle ); - uint256 expectedBorrowableUsdc = (assetDataUsdc.collateral.percentMul(assetDataUsdc.ltv) * - assetDataUsdc.tokenUnit) / assetDataUsdc.underlyingPrice; - uint256 expectedBorrowableDai = (assetDataUsdc.collateral.percentMul(assetDataUsdc.ltv) * + uint256 expectedBorrowableUsdc = (assetDataUsdc.collateralEth.percentMul( + assetDataUsdc.ltv + ) * assetDataUsdc.tokenUnit) / assetDataUsdc.underlyingPrice; + uint256 expectedBorrowableDai = (assetDataUsdc.collateralEth.percentMul(assetDataUsdc.ltv) * assetDataDai.tokenUnit) / assetDataDai.underlyingPrice; (uint256 withdrawable, uint256 borrowable) = lens.getUserMaxCapacitiesForAsset( @@ -460,8 +461,9 @@ contract TestLens is TestSetup { (uint256 withdrawableUsdc, ) = lens.getUserMaxCapacitiesForAsset(address(borrower1), aUsdc); (, uint256 borrowableUsdt) = lens.getUserMaxCapacitiesForAsset(address(borrower1), aUsdt); - uint256 expectedBorrowableUsdt = ((assetDataUsdc.collateral.percentMul(assetDataUsdc.ltv) + - assetDataDai.collateral.percentMul(assetDataDai.ltv)) * assetDataUsdt.tokenUnit) / + uint256 expectedBorrowableUsdt = ((assetDataUsdc.collateralEth.percentMul( + assetDataUsdc.ltv + ) + assetDataDai.collateralEth.percentMul(assetDataDai.ltv)) * assetDataUsdt.tokenUnit) / assetDataUsdt.underlyingPrice; assertEq(withdrawableUsdc, to6Decimals(amount), "unexpected new withdrawable usdc"); @@ -504,26 +506,26 @@ contract TestLens is TestSetup { uint256 underlyingPriceDai = oracle.getAssetPrice(dai); uint256 tokenUnitDai = 10**decimalsDai; - expectedStates.collateral = (amount * underlyingPriceDai) / tokenUnitDai; - expectedStates.debt = (toBorrow * underlyingPriceUsdc) / tokenUnitUsdc; - expectedStates.liquidationThresholdValue = expectedStates.collateral.percentMul( + expectedStates.collateralEth = (amount * underlyingPriceDai) / tokenUnitDai; + expectedStates.debtEth = (toBorrow * underlyingPriceUsdc) / tokenUnitUsdc; + expectedStates.liquidationThresholdEth = expectedStates.collateralEth.percentMul( liquidationThresholdDai ); - expectedStates.maxDebt = expectedStates.collateral.percentMul(ltvDai); + expectedStates.borrowableEth = expectedStates.collateralEth.percentMul(ltvDai); - uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( - expectedStates.debt + uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( + expectedStates.debtEth ); - assertEq(states.collateral, expectedStates.collateral, "collateral"); - assertEq(states.debt, expectedStates.debt, "debt"); + assertEq(states.collateralEth, expectedStates.collateralEth, "collateral"); + assertEq(states.debtEth, expectedStates.debtEth, "debt"); assertEq( - states.liquidationThresholdValue, - expectedStates.liquidationThresholdValue, + states.liquidationThresholdEth, + expectedStates.liquidationThresholdEth, "liquidationThreshold" ); - assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); + assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertEq(healthFactor, expectedHealthFactor, "healthFactor"); } @@ -549,42 +551,44 @@ contract TestLens is TestSetup { .getParamsMemory(); uint256 collateralValueToAdd = (to6Decimals(amount) * oracle.getAssetPrice(usdc)) / 10**decimals; - expectedStates.collateral += collateralValueToAdd; - expectedStates.liquidationThresholdValue += collateralValueToAdd.percentMul( + expectedStates.collateralEth += collateralValueToAdd; + expectedStates.liquidationThresholdEth += collateralValueToAdd.percentMul( liquidationThreshold ); - expectedStates.maxDebt += collateralValueToAdd.percentMul(ltv); + expectedStates.borrowableEth += collateralValueToAdd.percentMul(ltv); // DAI data (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); collateralValueToAdd = (amount * oracle.getAssetPrice(dai)) / 10**decimals; - expectedStates.collateral += collateralValueToAdd; - expectedStates.liquidationThresholdValue += collateralValueToAdd.percentMul( + expectedStates.collateralEth += collateralValueToAdd; + expectedStates.liquidationThresholdEth += collateralValueToAdd.percentMul( liquidationThreshold ); - expectedStates.maxDebt += collateralValueToAdd.percentMul(ltv); + expectedStates.borrowableEth += collateralValueToAdd.percentMul(ltv); // WBTC data (, , , decimals, ) = pool.getConfiguration(wbtc).getParamsMemory(); - expectedStates.debt += (toBorrowWbtc * oracle.getAssetPrice(wbtc)) / 10**decimals; + expectedStates.debtEth += (toBorrowWbtc * oracle.getAssetPrice(wbtc)) / 10**decimals; // USDT data (, , , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); - expectedStates.debt += (to6Decimals(toBorrow) * oracle.getAssetPrice(usdt)) / 10**decimals; + expectedStates.debtEth += + (to6Decimals(toBorrow) * oracle.getAssetPrice(usdt)) / + 10**decimals; - uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( - expectedStates.debt + uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( + expectedStates.debtEth ); - assertApproxEqAbs(states.collateral, expectedStates.collateral, 2, "collateral"); - assertApproxEqAbs(states.debt, expectedStates.debt, 1, "debt"); + assertApproxEqAbs(states.collateralEth, expectedStates.collateralEth, 2, "collateral"); + assertApproxEqAbs(states.debtEth, expectedStates.debtEth, 1, "debt"); assertEq( - states.liquidationThresholdValue, - expectedStates.liquidationThresholdValue, + states.liquidationThresholdEth, + expectedStates.liquidationThresholdEth, "liquidationThreshold" ); - assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); + assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertApproxEqAbs(healthFactor, expectedHealthFactor, 1e4, "healthFactor"); } @@ -638,42 +642,42 @@ contract TestLens is TestSetup { (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); uint256 collateralValueUsdt = (to6Decimals(amount) * oracle.getAssetPrice(usdt)) / 10**decimals; - expectedStates.collateral += collateralValueUsdt; - expectedStates.liquidationThresholdValue += collateralValueUsdt.percentMul( + expectedStates.collateralEth += collateralValueUsdt; + expectedStates.liquidationThresholdEth += collateralValueUsdt.percentMul( liquidationThreshold ); - expectedStates.maxDebt += collateralValueUsdt.percentMul(ltv); + expectedStates.borrowableEth += collateralValueUsdt.percentMul(ltv); // DAI data (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); uint256 collateralValueDai = (amount * oracle.getAssetPrice(dai)) / 10**decimals; - expectedStates.collateral += collateralValueDai; - expectedStates.liquidationThresholdValue += collateralValueDai.percentMul( + expectedStates.collateralEth += collateralValueDai; + expectedStates.liquidationThresholdEth += collateralValueDai.percentMul( liquidationThreshold ); - expectedStates.maxDebt += collateralValueDai.percentMul(ltv); + expectedStates.borrowableEth += collateralValueDai.percentMul(ltv); // USDC data (, , , decimals, ) = pool.getConfiguration(usdc).getParamsMemory(); - expectedStates.debt += (toBorrow * oracle.getAssetPrice(usdc)) / 10**decimals; + expectedStates.debtEth += (toBorrow * oracle.getAssetPrice(usdc)) / 10**decimals; // USDT data (, , , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); - expectedStates.debt += (toBorrow * oracle.getAssetPrice(usdt)) / 10**decimals; + expectedStates.debtEth += (toBorrow * oracle.getAssetPrice(usdt)) / 10**decimals; - uint256 healthFactor = states.liquidationThresholdValue.wadDiv(states.debt); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdValue.wadDiv( - expectedStates.debt + uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( + expectedStates.debtEth ); - assertApproxEqAbs(states.collateral, expectedStates.collateral, 1e3, "collateral"); - assertEq(states.debt, expectedStates.debt, "debt"); + assertApproxEqAbs(states.collateralEth, expectedStates.collateralEth, 1e3, "collateral"); + assertEq(states.debtEth, expectedStates.debtEth, "debt"); assertEq( - states.liquidationThresholdValue, - expectedStates.liquidationThresholdValue, + states.liquidationThresholdEth, + expectedStates.liquidationThresholdEth, "liquidationThreshold" ); - assertEq(states.maxDebt, expectedStates.maxDebt, "maxDebt"); + assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertEq(healthFactor, expectedHealthFactor, "healthFactor"); } @@ -1252,7 +1256,7 @@ contract TestLens is TestSetup { uint256 toRepay = lens.computeLiquidationRepayAmount(address(borrower1), aUsdc, aDai); - if (states.debt <= states.liquidationThresholdValue) { + if (states.debtEth <= states.liquidationThresholdEth) { assertEq(toRepay, 0, "Should return 0 when the position is solvent"); return; } @@ -1274,14 +1278,14 @@ contract TestLens is TestSetup { } while (lens.isLiquidatable(address(borrower1)) && toRepay > 0); // either the liquidatee's position (borrow value divided by supply value) was under the [1 / liquidationBonus] threshold and returned to a solvent position - if (states.collateral.percentDiv(liquidationBonus) > states.debt) { + if (states.collateralEth.percentDiv(liquidationBonus) > states.debtEth) { assertFalse(lens.isLiquidatable(address(borrower1)), "borrower1 liquidatable"); } else { // or the liquidator has drained all the collateral states = lens.getUserBalanceStates(address(borrower1)); assertGt( - states.debt, - states.collateral.percentDiv(liquidationBonus), + states.debtEth, + states.collateralEth.percentDiv(liquidationBonus), "debt value under collateral value" ); assertEq(toRepay, 0, "to repay not zero"); @@ -1289,8 +1293,8 @@ contract TestLens is TestSetup { } else { // liquidator cannot repay anything iff 1 wei of borrow is greater than the repayable collateral + the liquidation bonus assertGt( - states.debt, - states.collateral.percentDiv(liquidationBonus), + states.debtEth, + states.collateralEth.percentDiv(liquidationBonus), "debt value under collateral value" ); } From 6d6755ea6190aaebb80de7ce0f85dbfb6aeb8c79 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 12:07:47 +0100 Subject: [PATCH 028/105] Rename liquidity data --- src/compound/MorphoUtils.sol | 21 +++--- src/compound/lens/UsersLens.sol | 69 +++++++++--------- src/compound/libraries/Types.sol | 12 ++-- test/compound/TestLens.t.sol | 117 +++++++++++++++---------------- 4 files changed, 108 insertions(+), 111 deletions(-) diff --git a/src/compound/MorphoUtils.sol b/src/compound/MorphoUtils.sol index de401fab9..1161467e0 100644 --- a/src/compound/MorphoUtils.sol +++ b/src/compound/MorphoUtils.sol @@ -126,23 +126,22 @@ abstract contract MorphoUtils is MorphoStorage { uint256 numberOfEnteredMarkets = enteredMarkets[_user].length; Types.AssetLiquidityData memory assetData; - uint256 maxDebtValue; - uint256 debtValue; + uint256 maxDebtUsd; + uint256 debtUsd; uint256 i; while (i < numberOfEnteredMarkets) { address poolTokenEntered = enteredMarkets[_user][i]; assetData = _getUserLiquidityDataForAsset(_user, poolTokenEntered, oracle); - maxDebtValue += assetData.maxDebtValue; - debtValue += assetData.debtValue; + maxDebtUsd += assetData.maxDebtUsd; + debtUsd += assetData.debtUsd; if (_poolToken == poolTokenEntered) { - if (_borrowedAmount > 0) - debtValue += _borrowedAmount.mul(assetData.underlyingPrice); + if (_borrowedAmount > 0) debtUsd += _borrowedAmount.mul(assetData.underlyingPrice); if (_withdrawnAmount > 0) - maxDebtValue -= _withdrawnAmount.mul(assetData.underlyingPrice).mul( + maxDebtUsd -= _withdrawnAmount.mul(assetData.underlyingPrice).mul( assetData.collateralFactor ); } @@ -152,7 +151,7 @@ abstract contract MorphoUtils is MorphoStorage { } } - return debtValue > maxDebtValue; + return debtUsd > maxDebtUsd; } /// @notice Returns the data related to `_poolToken` for the `_user`. @@ -170,13 +169,13 @@ abstract contract MorphoUtils is MorphoStorage { if (assetData.underlyingPrice == 0) revert CompoundOracleFailed(); (, assetData.collateralFactor, ) = comptroller.markets(_poolToken); - assetData.collateralValue = _getUserSupplyBalanceInOf(_poolToken, _user).mul( + assetData.collateralUsd = _getUserSupplyBalanceInOf(_poolToken, _user).mul( assetData.underlyingPrice ); - assetData.debtValue = _getUserBorrowBalanceInOf(_poolToken, _user).mul( + assetData.debtUsd = _getUserBorrowBalanceInOf(_poolToken, _user).mul( assetData.underlyingPrice ); - assetData.maxDebtValue = assetData.collateralValue.mul(assetData.collateralFactor); + assetData.maxDebtUsd = assetData.collateralUsd.mul(assetData.collateralFactor); } /// @dev Returns the supply balance of `_user` in the `_poolToken` market. diff --git a/src/compound/lens/UsersLens.sol b/src/compound/lens/UsersLens.sol index 456acff9c..833aefdb0 100644 --- a/src/compound/lens/UsersLens.sol +++ b/src/compound/lens/UsersLens.sol @@ -51,8 +51,8 @@ abstract contract UsersLens is IndexesLens { if (_poolToken != poolTokenEntered) { assetData = getUserLiquidityDataForAsset(_user, poolTokenEntered, false, oracle); - data.maxDebtValue += assetData.maxDebtValue; - data.debtValue += assetData.debtValue; + data.maxDebtUsd += assetData.maxDebtUsd; + data.debtUsd += assetData.debtUsd; } unchecked { @@ -62,11 +62,11 @@ abstract contract UsersLens is IndexesLens { assetData = getUserLiquidityDataForAsset(_user, _poolToken, true, oracle); - data.maxDebtValue += assetData.maxDebtValue; - data.debtValue += assetData.debtValue; + data.maxDebtUsd += assetData.maxDebtUsd; + data.debtUsd += assetData.debtUsd; // Not possible to withdraw nor borrow. - if (data.maxDebtValue < data.debtValue) return (0, 0); + if (data.maxDebtUsd < data.debtUsd) return (0, 0); uint256 poolTokenBalance = _poolToken == morpho.cEth() ? _poolToken.balance @@ -74,11 +74,11 @@ abstract contract UsersLens is IndexesLens { borrowable = Math.min( poolTokenBalance, - (data.maxDebtValue - data.debtValue).div(assetData.underlyingPrice) + (data.maxDebtUsd - data.debtUsd).div(assetData.underlyingPrice) ); withdrawable = Math.min( poolTokenBalance, - assetData.collateralValue.div(assetData.underlyingPrice) + assetData.collateralUsd.div(assetData.underlyingPrice) ); if (assetData.collateralFactor != 0) { @@ -145,10 +145,10 @@ abstract contract UsersLens is IndexesLens { view returns (uint256) { - (, uint256 debtValue, uint256 maxDebtValue) = getUserBalanceStates(_user, _updatedMarkets); - if (debtValue == 0) return type(uint256).max; + (, uint256 debtUsd, uint256 maxDebtUsd) = getUserBalanceStates(_user, _updatedMarkets); + if (debtUsd == 0) return type(uint256).max; - return maxDebtValue.div(debtValue); + return maxDebtUsd.div(debtUsd); } /// PUBLIC /// @@ -156,16 +156,16 @@ abstract contract UsersLens is IndexesLens { /// @notice Returns the collateral value, debt value and max debt value of a given user. /// @param _user The user to determine liquidity for. /// @param _updatedMarkets The list of markets of which to compute virtually updated pool and peer-to-peer indexes. - /// @return collateralValue The collateral value of the user. - /// @return debtValue The current debt value of the user. - /// @return maxDebtValue The maximum possible debt value of the user. + /// @return collateralUsd The collateral value of the user. + /// @return debtUsd The current debt value of the user. + /// @return maxDebtUsd The maximum possible debt value of the user. function getUserBalanceStates(address _user, address[] calldata _updatedMarkets) public view returns ( - uint256 collateralValue, - uint256 debtValue, - uint256 maxDebtValue + uint256 collateralUsd, + uint256 debtUsd, + uint256 maxDebtUsd ) { ICompoundOracle oracle = ICompoundOracle(comptroller.oracle()); @@ -195,9 +195,9 @@ abstract contract UsersLens is IndexesLens { oracle ); - collateralValue += assetData.collateralValue; - maxDebtValue += assetData.maxDebtValue; - debtValue += assetData.debtValue; + collateralUsd += assetData.collateralUsd; + maxDebtUsd += assetData.maxDebtUsd; + debtUsd += assetData.debtUsd; unchecked { ++i; @@ -260,14 +260,14 @@ abstract contract UsersLens is IndexesLens { /// @param _poolToken The market to hypothetically withdraw/borrow in. /// @param _withdrawnAmount The number of tokens to hypothetically withdraw (in underlying). /// @param _borrowedAmount The amount of tokens to hypothetically borrow (in underlying). - /// @return debtValue The current debt value of the user. - /// @return maxDebtValue The maximum debt value possible of the user. + /// @return debtUsd The current debt value of the user. + /// @return maxDebtUsd The maximum debt value possible of the user. function getUserHypotheticalBalanceStates( address _user, address _poolToken, uint256 _withdrawnAmount, uint256 _borrowedAmount - ) public view returns (uint256 debtValue, uint256 maxDebtValue) { + ) public view returns (uint256 debtUsd, uint256 maxDebtUsd) { ICompoundOracle oracle = ICompoundOracle(comptroller.oracle()); address[] memory enteredMarkets = morpho.getEnteredMarkets(_user); @@ -282,18 +282,17 @@ abstract contract UsersLens is IndexesLens { oracle ); - maxDebtValue += assetData.maxDebtValue; - debtValue += assetData.debtValue; + maxDebtUsd += assetData.maxDebtUsd; + debtUsd += assetData.debtUsd; unchecked { ++i; } if (_poolToken == poolTokenEntered) { - if (_borrowedAmount > 0) - debtValue += _borrowedAmount.mul(assetData.underlyingPrice); + if (_borrowedAmount > 0) debtUsd += _borrowedAmount.mul(assetData.underlyingPrice); if (_withdrawnAmount > 0) - maxDebtValue -= _withdrawnAmount.mul(assetData.underlyingPrice).mul( + maxDebtUsd -= _withdrawnAmount.mul(assetData.underlyingPrice).mul( assetData.collateralFactor ); } @@ -324,21 +323,21 @@ abstract contract UsersLens is IndexesLens { uint256 poolBorrowIndex ) = getIndexes(_poolToken, _getUpdatedIndexes); - assetData.collateralValue = _getUserSupplyBalanceInOf( + assetData.collateralUsd = _getUserSupplyBalanceInOf( _poolToken, _user, p2pSupplyIndex, poolSupplyIndex ).mul(assetData.underlyingPrice); - assetData.debtValue = _getUserBorrowBalanceInOf( + assetData.debtUsd = _getUserBorrowBalanceInOf( _poolToken, _user, p2pBorrowIndex, poolBorrowIndex ).mul(assetData.underlyingPrice); - assetData.maxDebtValue = assetData.collateralValue.mul(assetData.collateralFactor); + assetData.maxDebtUsd = assetData.collateralUsd.mul(assetData.collateralFactor); } /// @dev Checks whether the user has enough collateral to maintain such a borrow position. @@ -353,8 +352,8 @@ abstract contract UsersLens is IndexesLens { ICompoundOracle oracle = ICompoundOracle(comptroller.oracle()); address[] memory enteredMarkets = morpho.getEnteredMarkets(_user); - uint256 maxDebtValue; - uint256 debtValue; + uint256 maxDebtUsd; + uint256 debtUsd; uint256 nbEnteredMarkets = enteredMarkets.length; uint256 nbUpdatedMarkets = _updatedMarkets.length; @@ -380,15 +379,15 @@ abstract contract UsersLens is IndexesLens { oracle ); - maxDebtValue += assetData.maxDebtValue; - debtValue += assetData.debtValue; + maxDebtUsd += assetData.maxDebtUsd; + debtUsd += assetData.debtUsd; unchecked { ++i; } } - return debtValue > maxDebtValue; + return debtUsd > maxDebtUsd; } /// INTERNAL /// diff --git a/src/compound/libraries/Types.sol b/src/compound/libraries/Types.sol index 1cf9703fd..9706d5c49 100644 --- a/src/compound/libraries/Types.sol +++ b/src/compound/libraries/Types.sol @@ -43,17 +43,17 @@ library Types { } struct AssetLiquidityData { - uint256 collateralValue; // The collateral value of the asset. - uint256 maxDebtValue; // The maximum possible debt value of the asset. - uint256 debtValue; // The debt value of the asset. + uint256 collateralUsd; // The collateral value of the asset (in USD, wad). + uint256 maxDebtUsd; // The maximum possible debt value of the asset (in USD, wad). + uint256 debtUsd; // The debt value of the asset (in USD, wad). uint256 underlyingPrice; // The price of the token. uint256 collateralFactor; // The liquidation threshold applied on this token. } struct LiquidityData { - uint256 collateralValue; // The collateral value. - uint256 maxDebtValue; // The maximum debt value possible. - uint256 debtValue; // The debt value. + uint256 collateralUsd; // The collateral value (in USD, wad). + uint256 maxDebtUsd; // The maximum debt value possible (in USD, wad). + uint256 debtUsd; // The debt value (in USD, wad). } // Variables are packed together to save gas (will not exceed their limit during Morpho's lifetime). diff --git a/test/compound/TestLens.t.sol b/test/compound/TestLens.t.sol index 20e0e77e6..aa4a8ccbc 100644 --- a/test/compound/TestLens.t.sol +++ b/test/compound/TestLens.t.sol @@ -7,10 +7,10 @@ contract TestLens is TestSetup { using CompoundMath for uint256; struct UserBalanceStates { - uint256 collateralValue; - uint256 debtValue; - uint256 maxDebtValue; - uint256 liquidationValue; + uint256 collateralUsd; + uint256 debtUsd; + uint256 maxDebtUsd; + uint256 liquidationUsd; } struct UserBalance { @@ -32,9 +32,9 @@ contract TestLens is TestSetup { assertEq(assetData.collateralFactor, collateralFactor); assertEq(assetData.underlyingPrice, underlyingPrice); - assertEq(assetData.collateralValue, 0); - assertEq(assetData.maxDebtValue, 0); - assertEq(assetData.debtValue, 0); + assertEq(assetData.collateralUsd, 0); + assertEq(assetData.maxDebtUsd, 0); + assertEq(assetData.debtUsd, 0); } function testUserLiquidityDataForAssetWithSupply() public { @@ -59,9 +59,9 @@ contract TestLens is TestSetup { assertEq(assetData.collateralFactor, collateralFactor, "collateralFactor"); assertEq(assetData.underlyingPrice, underlyingPrice, "underlyingPrice"); - assertEq(assetData.collateralValue, collateralValue, "collateralValue"); - assertEq(assetData.maxDebtValue, maxDebtValue, "maxDebtValue"); - assertEq(assetData.debtValue, 0, "debtValue"); + assertEq(assetData.collateralUsd, collateralValue, "collateralValue"); + assertEq(assetData.maxDebtUsd, maxDebtValue, "maxDebtValue"); + assertEq(assetData.debtUsd, 0, "debtValue"); } struct Indexes { @@ -107,9 +107,9 @@ contract TestLens is TestSetup { uint256 debtValue = toBorrow.div(p2pBorrowIndex).mul(p2pBorrowIndex).mul(underlyingPrice); assertEq(assetData.underlyingPrice, underlyingPrice, "underlyingPrice"); - assertEq(assetData.collateralValue, collateralValue, "collateralValue"); - assertEq(assetData.maxDebtValue, maxDebtValue, "maxDebtValue"); - assertEq(assetData.debtValue, debtValue, "debtValue"); + assertEq(assetData.collateralUsd, collateralValue, "collateralValue"); + assertEq(assetData.maxDebtUsd, maxDebtValue, "maxDebtValue"); + assertEq(assetData.debtUsd, debtValue, "debtValue"); } function testUserLiquidityDataForAssetWithSupplyAndBorrowWithMultipleAssets() public { @@ -138,7 +138,7 @@ contract TestLens is TestSetup { Types.AssetLiquidityData memory expectedDataCUsdc; expectedDataCUsdc.underlyingPrice = oracle.getUnderlyingPrice(cUsdc); - expectedDataCUsdc.debtValue = getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()) + expectedDataCUsdc.debtUsd = getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()) .mul(expectedDataCUsdc.underlyingPrice); assertEq( @@ -146,9 +146,9 @@ contract TestLens is TestSetup { expectedDataCUsdc.underlyingPrice, "underlyingPriceUsdc" ); - assertEq(assetDataCUsdc.collateralValue, 0, "collateralValue"); - assertEq(assetDataCUsdc.maxDebtValue, 0, "maxDebtValue"); - assertEq(assetDataCUsdc.debtValue, expectedDataCUsdc.debtValue, "debtValueUsdc"); + assertEq(assetDataCUsdc.collateralUsd, 0, "collateralValue"); + assertEq(assetDataCUsdc.maxDebtUsd, 0, "maxDebtValue"); + assertEq(assetDataCUsdc.debtUsd, expectedDataCUsdc.debtUsd, "debtValueUsdc"); // Avoid stack too deep error. Types.AssetLiquidityData memory expectedDataCDai; @@ -156,11 +156,11 @@ contract TestLens is TestSetup { (, expectedDataCDai.collateralFactor, ) = comptroller.markets(cDai); expectedDataCDai.underlyingPrice = oracle.getUnderlyingPrice(cDai); - expectedDataCDai.collateralValue = getBalanceOnCompound( + expectedDataCDai.collateralUsd = getBalanceOnCompound( amount, ICToken(cDai).exchangeRateStored() ).mul(expectedDataCDai.underlyingPrice); - expectedDataCDai.maxDebtValue = expectedDataCDai.collateralValue.mul( + expectedDataCDai.maxDebtUsd = expectedDataCDai.collateralUsd.mul( expectedDataCDai.collateralFactor ); @@ -175,13 +175,9 @@ contract TestLens is TestSetup { "underlyingPriceDai" ); - assertEq( - assetDataCDai.collateralValue, - expectedDataCDai.collateralValue, - "collateralValueDai" - ); - assertEq(assetDataCDai.maxDebtValue, expectedDataCDai.maxDebtValue, "maxDebtValueDai"); - assertEq(assetDataCDai.debtValue, 0, "debtValueDai"); + assertEq(assetDataCDai.collateralUsd, expectedDataCDai.collateralUsd, "collateralValueDai"); + assertEq(assetDataCDai.maxDebtUsd, expectedDataCDai.maxDebtUsd, "maxDebtValueDai"); + assertEq(assetDataCDai.debtUsd, 0, "debtValueDai"); } function testMaxCapacitiesWithNothing() public { @@ -214,10 +210,10 @@ contract TestLens is TestSetup { oracle ); - uint256 expectedBorrowableUsdc = assetDataCUsdc.maxDebtValue.div( + uint256 expectedBorrowableUsdc = assetDataCUsdc.maxDebtUsd.div( assetDataCUsdc.underlyingPrice ); - uint256 expectedBorrowableDai = assetDataCUsdc.maxDebtValue.div( + uint256 expectedBorrowableDai = assetDataCUsdc.maxDebtUsd.div( assetDataCDai.underlyingPrice ); @@ -476,8 +472,9 @@ contract TestLens is TestSetup { (uint256 withdrawableUsdc, ) = lens.getUserMaxCapacitiesForAsset(address(borrower1), cUsdc); (, uint256 borrowableUsdt) = lens.getUserMaxCapacitiesForAsset(address(borrower1), cUsdt); - uint256 expectedBorrowableUsdt = (assetDataCDai.maxDebtValue + assetDataCUsdc.maxDebtValue) - .div(assetDataCUsdt.underlyingPrice); + uint256 expectedBorrowableUsdt = (assetDataCDai.maxDebtUsd + assetDataCUsdc.maxDebtUsd).div( + assetDataCUsdt.underlyingPrice + ); assertEq( withdrawableUsdc, @@ -516,7 +513,7 @@ contract TestLens is TestSetup { UserBalanceStates memory states; UserBalanceStates memory expectedStates; - (states.collateralValue, states.debtValue, states.maxDebtValue) = lens.getUserBalanceStates( + (states.collateralUsd, states.debtUsd, states.maxDebtUsd) = lens.getUserBalanceStates( address(borrower1), new address[](0) ); @@ -526,19 +523,19 @@ contract TestLens is TestSetup { // DAI data (, uint256 collateralFactor, ) = comptroller.markets(cDai); uint256 underlyingPriceDai = oracle.getUnderlyingPrice(cDai); - expectedStates.collateralValue = getBalanceOnCompound( + expectedStates.collateralUsd = getBalanceOnCompound( amount, ICToken(cDai).exchangeRateStored() ).mul(underlyingPriceDai); - expectedStates.debtValue = getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()).mul( + expectedStates.debtUsd = getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()).mul( underlyingPriceUsdc ); - expectedStates.maxDebtValue = expectedStates.collateralValue.mul(collateralFactor); + expectedStates.maxDebtUsd = expectedStates.collateralUsd.mul(collateralFactor); - assertEq(states.collateralValue, expectedStates.collateralValue, "Collateral Value"); - assertEq(states.maxDebtValue, expectedStates.maxDebtValue, "Max Debt Value"); - assertEq(states.debtValue, expectedStates.debtValue, "Debt Value"); + assertEq(states.collateralUsd, expectedStates.collateralUsd, "Collateral Value"); + assertEq(states.maxDebtUsd, expectedStates.maxDebtUsd, "Max Debt Value"); + assertEq(states.debtUsd, expectedStates.debtUsd, "Debt Value"); } function testUserBalanceStatesWithSupplyAndBorrowWithMultipleAssets() public { @@ -562,36 +559,36 @@ contract TestLens is TestSetup { to6Decimals(amount), ICToken(cUsdc).exchangeRateStored() ).mul(oracle.getUnderlyingPrice(cUsdc)); - expectedStates.collateralValue += collateralValueToAdd; + expectedStates.collateralUsd += collateralValueToAdd; (, uint256 collateralFactor, ) = comptroller.markets(cUsdc); - expectedStates.maxDebtValue += collateralValueToAdd.mul(collateralFactor); + expectedStates.maxDebtUsd += collateralValueToAdd.mul(collateralFactor); // DAI data collateralValueToAdd = getBalanceOnCompound(amount, ICToken(cDai).exchangeRateStored()).mul( oracle.getUnderlyingPrice(cDai) ); - expectedStates.collateralValue += collateralValueToAdd; + expectedStates.collateralUsd += collateralValueToAdd; (, collateralFactor, ) = comptroller.markets(cDai); - expectedStates.maxDebtValue += collateralValueToAdd.mul(collateralFactor); + expectedStates.maxDebtUsd += collateralValueToAdd.mul(collateralFactor); // BAT - expectedStates.debtValue += getBalanceOnCompound(toBorrow, ICToken(cBat).borrowIndex()).mul( + expectedStates.debtUsd += getBalanceOnCompound(toBorrow, ICToken(cBat).borrowIndex()).mul( oracle.getUnderlyingPrice(cBat) ); // USDT - expectedStates.debtValue += getBalanceOnCompound( + expectedStates.debtUsd += getBalanceOnCompound( to6Decimals(toBorrow), ICToken(cUsdt).borrowIndex() ).mul(oracle.getUnderlyingPrice(cUsdt)); - (states.collateralValue, states.debtValue, states.maxDebtValue) = lens.getUserBalanceStates( + (states.collateralUsd, states.debtUsd, states.maxDebtUsd) = lens.getUserBalanceStates( address(borrower1), new address[](0) ); - assertEq(states.collateralValue, expectedStates.collateralValue, "Collateral Value"); - assertEq(states.debtValue, expectedStates.debtValue, "Debt Value"); - assertEq(states.maxDebtValue, expectedStates.maxDebtValue, "Max Debt Value"); + assertEq(states.collateralUsd, expectedStates.collateralUsd, "Collateral Value"); + assertEq(states.debtUsd, expectedStates.debtUsd, "Debt Value"); + assertEq(states.maxDebtUsd, expectedStates.maxDebtUsd, "Max Debt Value"); } /// This test is to check that a call to getUserLiquidityDataForAsset with USDT doesn't end @@ -657,7 +654,7 @@ contract TestLens is TestSetup { UserBalanceStates memory states; UserBalanceStates memory expectedStates; - (states.collateralValue, states.debtValue, states.maxDebtValue) = lens.getUserBalanceStates( + (states.collateralUsd, states.debtUsd, states.maxDebtUsd) = lens.getUserBalanceStates( address(borrower1), new address[](0) ); @@ -679,27 +676,29 @@ contract TestLens is TestSetup { uint256 underlyingPrice = oracle.getUnderlyingPrice(cUsdt); uint256 collateralValueToAdd = total.mul(underlyingPrice); - expectedStates.collateralValue += collateralValueToAdd; - expectedStates.maxDebtValue += collateralValueToAdd.mul(collateralFactor); + expectedStates.collateralUsd += collateralValueToAdd; + expectedStates.maxDebtUsd += collateralValueToAdd.mul(collateralFactor); // DAI data (, collateralFactor, ) = comptroller.markets(cDai); collateralValueToAdd = getBalanceOnCompound(amount, ICToken(cDai).exchangeRateCurrent()) .mul(oracle.getUnderlyingPrice(cDai)); - expectedStates.collateralValue += collateralValueToAdd; - expectedStates.maxDebtValue += collateralValueToAdd.mul(collateralFactor); + expectedStates.collateralUsd += collateralValueToAdd; + expectedStates.maxDebtUsd += collateralValueToAdd.mul(collateralFactor); // USDC data - expectedStates.debtValue += getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()) - .mul(oracle.getUnderlyingPrice(cUsdc)); + expectedStates.debtUsd += getBalanceOnCompound(toBorrow, ICToken(cUsdc).borrowIndex()).mul( + oracle.getUnderlyingPrice(cUsdc) + ); // USDT data - expectedStates.debtValue += getBalanceOnCompound(toBorrow, ICToken(cUsdt).borrowIndex()) - .mul(oracle.getUnderlyingPrice(cUsdt)); + expectedStates.debtUsd += getBalanceOnCompound(toBorrow, ICToken(cUsdt).borrowIndex()).mul( + oracle.getUnderlyingPrice(cUsdt) + ); - assertEq(states.collateralValue, expectedStates.collateralValue, "Collateral Value"); - assertEq(states.debtValue, expectedStates.debtValue, "Debt Value"); - assertEq(states.maxDebtValue, expectedStates.maxDebtValue, "Max Debt Value"); + assertEq(states.collateralUsd, expectedStates.collateralUsd, "Collateral Value"); + assertEq(states.debtUsd, expectedStates.debtUsd, "Debt Value"); + assertEq(states.maxDebtUsd, expectedStates.maxDebtUsd, "Max Debt Value"); } function testGetMainMarketData() public { From fce4f15496affda207d4d87a9b780d98a89c98c6 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 15:09:18 +0100 Subject: [PATCH 029/105] Add backwards ABI compatibility --- src/aave-v2/MatchingEngine.sol | 12 ++++++------ src/aave-v2/Morpho.sol | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/MatchingEngine.sol b/src/aave-v2/MatchingEngine.sol index 96d2d3491..852f05c86 100644 --- a/src/aave-v2/MatchingEngine.sol +++ b/src/aave-v2/MatchingEngine.sol @@ -299,20 +299,20 @@ abstract contract MatchingEngine is MorphoUtils { /// @param _poolToken The address of the market on which to update the suppliers data structure. /// @param _user The address of the user. function _updateSupplierInDS(address _poolToken, address _user) internal { - Types.SupplyBalance memory supplierSupplyBalance = supplyBalanceInOf[_poolToken][_user]; + Types.SupplyBalance memory supplyBalance = supplyBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketSuppliersOnPool = suppliersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketSuppliersInP2P = suppliersInP2P[_poolToken]; marketSuppliersOnPool.update( _user, marketSuppliersOnPool.getValueOf(_user), - supplierSupplyBalance.onPool, + supplyBalance.onPool, maxSortedUsers ); marketSuppliersInP2P.update( _user, marketSuppliersInP2P.getValueOf(_user), - supplierSupplyBalance.inP2P, + supplyBalance.inP2P, maxSortedUsers ); } @@ -321,20 +321,20 @@ abstract contract MatchingEngine is MorphoUtils { /// @param _poolToken The address of the market on which to update the borrowers data structure. /// @param _user The address of the user. function _updateBorrowerInDS(address _poolToken, address _user) internal { - Types.BorrowBalance memory borrowerBorrowBalance = borrowBalanceInOf[_poolToken][_user]; + Types.BorrowBalance memory borrowBalance = borrowBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketBorrowersOnPool = borrowersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketBorrowersInP2P = borrowersInP2P[_poolToken]; marketBorrowersOnPool.update( _user, marketBorrowersOnPool.getValueOf(_user), - borrowerBorrowBalance.onPool, + borrowBalance.onPool, maxSortedUsers ); marketBorrowersInP2P.update( _user, marketBorrowersInP2P.getValueOf(_user), - borrowerBorrowBalance.inP2P, + borrowBalance.inP2P, maxSortedUsers ); } diff --git a/src/aave-v2/Morpho.sol b/src/aave-v2/Morpho.sol index c0bef2bc6..7a7b5fa42 100644 --- a/src/aave-v2/Morpho.sol +++ b/src/aave-v2/Morpho.sol @@ -132,6 +132,9 @@ contract Morpho is MorphoGovernance { ); } + /// @notice Deprecated. + function claimRewards(address[] calldata, bool) external returns (uint256) {} + /// INTERNAL /// function _supply( From 249e0356f15b34c29f347a00bc4cc5446a3cc409 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 15:13:25 +0100 Subject: [PATCH 030/105] Improve liquidity data naming --- src/aave-v2/ExitPositionsManager.sol | 5 +-- src/aave-v2/MorphoUtils.sol | 6 +-- src/aave-v2/lens/UsersLens.sol | 10 ++--- src/aave-v2/libraries/Types.sol | 4 +- src/compound/libraries/Types.sol | 2 +- test/aave-v2/TestLens.t.sol | 56 ++++++++-------------------- 6 files changed, 26 insertions(+), 57 deletions(-) diff --git a/src/aave-v2/ExitPositionsManager.sol b/src/aave-v2/ExitPositionsManager.sol index 9e0523f15..0a9c6adaa 100644 --- a/src/aave-v2/ExitPositionsManager.sol +++ b/src/aave-v2/ExitPositionsManager.sol @@ -674,10 +674,7 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { Types.LiquidityData memory values = _liquidityData(_user, _poolToken, _withdrawnAmount, 0); - return - values.debtEth > 0 - ? values.liquidationThresholdEth.wadDiv(values.debtEth) - : type(uint256).max; + return values.debtEth > 0 ? values.maxDebtEth.wadDiv(values.debtEth) : type(uint256).max; } /// @dev Checks whether the user can withdraw or not. diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index 75182c5fc..70a48f7b0 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -320,7 +320,7 @@ abstract contract MorphoUtils is MorphoStorage { // Update LT variable for withdraw. if (assetCollateralValue > 0) - values.liquidationThresholdEth += assetCollateralValue.percentMul( + values.maxDebtEth += assetCollateralValue.percentMul( assetData.liquidationThreshold ); @@ -328,9 +328,7 @@ abstract contract MorphoUtils is MorphoStorage { if (_poolToken == vars.poolToken && _amountWithdrawn > 0) { uint256 withdrawn = (_amountWithdrawn * vars.underlyingPrice) / assetData.tokenUnit; values.collateralEth -= withdrawn; - values.liquidationThresholdEth -= withdrawn.percentMul( - assetData.liquidationThreshold - ); + values.maxDebtEth -= withdrawn.percentMul(assetData.liquidationThreshold); values.borrowableEth -= withdrawn.percentMul(assetData.ltv); } } diff --git a/src/aave-v2/lens/UsersLens.sol b/src/aave-v2/lens/UsersLens.sol index f2f976cbe..0c099324b 100644 --- a/src/aave-v2/lens/UsersLens.sol +++ b/src/aave-v2/lens/UsersLens.sol @@ -75,7 +75,7 @@ abstract contract UsersLens is IndexesLens { if ( liquidityData.debtEth > 0 && - liquidityData.liquidationThresholdEth.wadDiv(liquidityData.debtEth) <= + liquidityData.maxDebtEth.wadDiv(liquidityData.debtEth) <= HEALTH_FACTOR_LIQUIDATION_THRESHOLD ) return (0, 0); @@ -98,7 +98,7 @@ abstract contract UsersLens is IndexesLens { if (assetData.liquidationThreshold > 0) withdrawable = Math.min( withdrawable, - ((liquidityData.liquidationThresholdEth - liquidityData.debtEth).percentDiv( + ((liquidityData.maxDebtEth - liquidityData.debtEth).percentDiv( assetData.liquidationThreshold ) * assetData.tokenUnit) / assetData.underlyingPrice ); @@ -224,7 +224,7 @@ abstract contract UsersLens is IndexesLens { liquidityData.collateralEth += assetData.collateralEth; liquidityData.borrowableEth += assetData.collateralEth.percentMul(assetData.ltv); - liquidityData.liquidationThresholdEth += assetData.collateralEth.percentMul( + liquidityData.maxDebtEth += assetData.collateralEth.percentMul( assetData.liquidationThreshold ); liquidityData.debtEth += assetData.debtEth; @@ -240,7 +240,7 @@ abstract contract UsersLens is IndexesLens { liquidityData.collateralEth -= assetCollateral; liquidityData.borrowableEth -= assetCollateral.percentMul(assetData.ltv); - liquidityData.liquidationThresholdEth -= assetCollateral.percentMul( + liquidityData.maxDebtEth -= assetCollateral.percentMul( assetData.liquidationThreshold ); } @@ -325,7 +325,7 @@ abstract contract UsersLens is IndexesLens { ); if (liquidityData.debtEth == 0) return type(uint256).max; - return liquidityData.liquidationThresholdEth.wadDiv(liquidityData.debtEth); + return liquidityData.maxDebtEth.wadDiv(liquidityData.debtEth); } /// @dev Checks whether a liquidation can be performed on a given user. diff --git a/src/aave-v2/libraries/Types.sol b/src/aave-v2/libraries/Types.sol index b1f07b533..610d8a443 100644 --- a/src/aave-v2/libraries/Types.sol +++ b/src/aave-v2/libraries/Types.sol @@ -54,8 +54,8 @@ library Types { struct LiquidityData { uint256 collateralEth; // The collateral value (in ETH). - uint256 borrowableEth; // The max debt value (in ETH). - uint256 liquidationThresholdEth; // The liquidation threshold value (in ETH). + uint256 borrowableEth; // The maximum debt value allowed to borrow (in ETH). + uint256 maxDebtEth; // The maximum debt value allowed before being liquidatable (in ETH). uint256 debtEth; // The debt value (in ETH). } diff --git a/src/compound/libraries/Types.sol b/src/compound/libraries/Types.sol index 9706d5c49..aa016283f 100644 --- a/src/compound/libraries/Types.sol +++ b/src/compound/libraries/Types.sol @@ -52,7 +52,7 @@ library Types { struct LiquidityData { uint256 collateralUsd; // The collateral value (in USD, wad). - uint256 maxDebtUsd; // The maximum debt value possible (in USD, wad). + uint256 maxDebtUsd; // The maximum debt value allowed before being liquidatable (in USD, wad). uint256 debtUsd; // The debt value (in USD, wad). } diff --git a/test/aave-v2/TestLens.t.sol b/test/aave-v2/TestLens.t.sol index 39e833d8c..269ba82d4 100644 --- a/test/aave-v2/TestLens.t.sol +++ b/test/aave-v2/TestLens.t.sol @@ -508,23 +508,17 @@ contract TestLens is TestSetup { expectedStates.collateralEth = (amount * underlyingPriceDai) / tokenUnitDai; expectedStates.debtEth = (toBorrow * underlyingPriceUsdc) / tokenUnitUsdc; - expectedStates.liquidationThresholdEth = expectedStates.collateralEth.percentMul( + expectedStates.maxDebtEth = expectedStates.collateralEth.percentMul( liquidationThresholdDai ); expectedStates.borrowableEth = expectedStates.collateralEth.percentMul(ltvDai); - uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( - expectedStates.debtEth - ); + uint256 healthFactor = states.maxDebtEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.maxDebtEth.wadDiv(expectedStates.debtEth); assertEq(states.collateralEth, expectedStates.collateralEth, "collateral"); assertEq(states.debtEth, expectedStates.debtEth, "debt"); - assertEq( - states.liquidationThresholdEth, - expectedStates.liquidationThresholdEth, - "liquidationThreshold" - ); + assertEq(states.maxDebtEth, expectedStates.maxDebtEth, "liquidationThreshold"); assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertEq(healthFactor, expectedHealthFactor, "healthFactor"); } @@ -552,18 +546,14 @@ contract TestLens is TestSetup { uint256 collateralValueToAdd = (to6Decimals(amount) * oracle.getAssetPrice(usdc)) / 10**decimals; expectedStates.collateralEth += collateralValueToAdd; - expectedStates.liquidationThresholdEth += collateralValueToAdd.percentMul( - liquidationThreshold - ); + expectedStates.maxDebtEth += collateralValueToAdd.percentMul(liquidationThreshold); expectedStates.borrowableEth += collateralValueToAdd.percentMul(ltv); // DAI data (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); collateralValueToAdd = (amount * oracle.getAssetPrice(dai)) / 10**decimals; expectedStates.collateralEth += collateralValueToAdd; - expectedStates.liquidationThresholdEth += collateralValueToAdd.percentMul( - liquidationThreshold - ); + expectedStates.maxDebtEth += collateralValueToAdd.percentMul(liquidationThreshold); expectedStates.borrowableEth += collateralValueToAdd.percentMul(ltv); // WBTC data @@ -576,18 +566,12 @@ contract TestLens is TestSetup { (to6Decimals(toBorrow) * oracle.getAssetPrice(usdt)) / 10**decimals; - uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( - expectedStates.debtEth - ); + uint256 healthFactor = states.maxDebtEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.maxDebtEth.wadDiv(expectedStates.debtEth); assertApproxEqAbs(states.collateralEth, expectedStates.collateralEth, 2, "collateral"); assertApproxEqAbs(states.debtEth, expectedStates.debtEth, 1, "debt"); - assertEq( - states.liquidationThresholdEth, - expectedStates.liquidationThresholdEth, - "liquidationThreshold" - ); + assertEq(states.maxDebtEth, expectedStates.maxDebtEth, "liquidationThreshold"); assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertApproxEqAbs(healthFactor, expectedHealthFactor, 1e4, "healthFactor"); } @@ -643,18 +627,14 @@ contract TestLens is TestSetup { uint256 collateralValueUsdt = (to6Decimals(amount) * oracle.getAssetPrice(usdt)) / 10**decimals; expectedStates.collateralEth += collateralValueUsdt; - expectedStates.liquidationThresholdEth += collateralValueUsdt.percentMul( - liquidationThreshold - ); + expectedStates.maxDebtEth += collateralValueUsdt.percentMul(liquidationThreshold); expectedStates.borrowableEth += collateralValueUsdt.percentMul(ltv); // DAI data (ltv, liquidationThreshold, , decimals, ) = pool.getConfiguration(dai).getParamsMemory(); uint256 collateralValueDai = (amount * oracle.getAssetPrice(dai)) / 10**decimals; expectedStates.collateralEth += collateralValueDai; - expectedStates.liquidationThresholdEth += collateralValueDai.percentMul( - liquidationThreshold - ); + expectedStates.maxDebtEth += collateralValueDai.percentMul(liquidationThreshold); expectedStates.borrowableEth += collateralValueDai.percentMul(ltv); // USDC data @@ -665,18 +645,12 @@ contract TestLens is TestSetup { (, , , decimals, ) = pool.getConfiguration(usdt).getParamsMemory(); expectedStates.debtEth += (toBorrow * oracle.getAssetPrice(usdt)) / 10**decimals; - uint256 healthFactor = states.liquidationThresholdEth.wadDiv(states.debtEth); - uint256 expectedHealthFactor = expectedStates.liquidationThresholdEth.wadDiv( - expectedStates.debtEth - ); + uint256 healthFactor = states.maxDebtEth.wadDiv(states.debtEth); + uint256 expectedHealthFactor = expectedStates.maxDebtEth.wadDiv(expectedStates.debtEth); assertApproxEqAbs(states.collateralEth, expectedStates.collateralEth, 1e3, "collateral"); assertEq(states.debtEth, expectedStates.debtEth, "debt"); - assertEq( - states.liquidationThresholdEth, - expectedStates.liquidationThresholdEth, - "liquidationThreshold" - ); + assertEq(states.maxDebtEth, expectedStates.maxDebtEth, "liquidationThreshold"); assertEq(states.borrowableEth, expectedStates.borrowableEth, "maxDebt"); assertEq(healthFactor, expectedHealthFactor, "healthFactor"); } @@ -1256,7 +1230,7 @@ contract TestLens is TestSetup { uint256 toRepay = lens.computeLiquidationRepayAmount(address(borrower1), aUsdc, aDai); - if (states.debtEth <= states.liquidationThresholdEth) { + if (states.debtEth <= states.maxDebtEth) { assertEq(toRepay, 0, "Should return 0 when the position is solvent"); return; } From d8265c5df24b319d7cffa9c7c4c7250b248c9208 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 15:17:33 +0100 Subject: [PATCH 031/105] Save MSU to memory --- src/aave-v2/MatchingEngine.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aave-v2/MatchingEngine.sol b/src/aave-v2/MatchingEngine.sol index 852f05c86..58e5ddaa3 100644 --- a/src/aave-v2/MatchingEngine.sol +++ b/src/aave-v2/MatchingEngine.sol @@ -302,6 +302,7 @@ abstract contract MatchingEngine is MorphoUtils { Types.SupplyBalance memory supplyBalance = supplyBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketSuppliersOnPool = suppliersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketSuppliersInP2P = suppliersInP2P[_poolToken]; + uint256 maxSortedUsers = maxSortedUsers; marketSuppliersOnPool.update( _user, @@ -324,6 +325,7 @@ abstract contract MatchingEngine is MorphoUtils { Types.BorrowBalance memory borrowBalance = borrowBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketBorrowersOnPool = borrowersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketBorrowersInP2P = borrowersInP2P[_poolToken]; + uint256 maxSortedUsers = maxSortedUsers; marketBorrowersOnPool.update( _user, From 8c572f6e49953cd2532d590c7d82f9f10d08390c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 9 Dec 2022 15:23:53 +0100 Subject: [PATCH 032/105] Factorized IRModel --- src/aave-v2/InterestRatesManager.sol | 4 +- src/aave-v2/lens/IndexesLens.sol | 4 +- src/aave-v2/libraries/InterestRatesModel.sol | 37 +++---------------- src/compound/InterestRatesManager.sol | 4 +- src/compound/lens/IndexesLens.sol | 4 +- src/compound/libraries/InterestRatesModel.sol | 37 +++---------------- 6 files changed, 20 insertions(+), 70 deletions(-) diff --git a/src/aave-v2/InterestRatesManager.sol b/src/aave-v2/InterestRatesManager.sol index 25b5409b6..b19d26fee 100644 --- a/src/aave-v2/InterestRatesManager.sol +++ b/src/aave-v2/InterestRatesManager.sol @@ -129,7 +129,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { _params.reserveFactor ); - newP2PSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + newP2PSupplyIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, @@ -139,7 +139,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { p2pAmount: _params.delta.p2pSupplyAmount }) ); - newP2PBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + newP2PBorrowIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, diff --git a/src/aave-v2/lens/IndexesLens.sol b/src/aave-v2/lens/IndexesLens.sol index 30a7d42e9..21d39ea3a 100644 --- a/src/aave-v2/lens/IndexesLens.sol +++ b/src/aave-v2/lens/IndexesLens.sol @@ -81,7 +81,7 @@ abstract contract IndexesLens is LensStorage { market.reserveFactor ); - indexes.p2pSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + indexes.p2pSupplyIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, @@ -91,7 +91,7 @@ abstract contract IndexesLens is LensStorage { p2pAmount: delta.p2pSupplyAmount }) ); - indexes.p2pBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + indexes.p2pBorrowIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, diff --git a/src/aave-v2/libraries/InterestRatesModel.sol b/src/aave-v2/libraries/InterestRatesModel.sol index a170fe559..0eea983d5 100644 --- a/src/aave-v2/libraries/InterestRatesModel.sol +++ b/src/aave-v2/libraries/InterestRatesModel.sol @@ -83,16 +83,16 @@ library InterestRatesModel { } } - /// @notice Computes and returns the new peer-to-peer supply index of a market given its parameters. + /// @notice Computes and returns the new peer-to-peer supply/borrow index of a market given its parameters. /// @param _params The computation parameters. - /// @return newP2PSupplyIndex The updated peer-to-peer index (in ray). - function computeP2PSupplyIndex(P2PIndexComputeParams memory _params) + /// @return newP2PIndex The updated peer-to-peer index (in ray). + function computeP2PIndex(P2PIndexComputeParams memory _params) internal pure - returns (uint256 newP2PSupplyIndex) + returns (uint256 newP2PIndex) { if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { - newP2PSupplyIndex = _params.lastP2PIndex.rayMul(_params.p2pGrowthFactor); + newP2PIndex = _params.lastP2PIndex.rayMul(_params.p2pGrowthFactor); } else { uint256 shareOfTheDelta = Math.min( _params.p2pDelta.rayMul(_params.lastPoolIndex).rayDiv( @@ -101,32 +101,7 @@ library InterestRatesModel { WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. ); // In ray. - newP2PSupplyIndex = _params.lastP2PIndex.rayMul( - (WadRayMath.RAY - shareOfTheDelta).rayMul(_params.p2pGrowthFactor) + - shareOfTheDelta.rayMul(_params.poolGrowthFactor) - ); - } - } - - /// @notice Computes and returns the new peer-to-peer borrow index of a market given its parameters. - /// @param _params The computation parameters. - /// @return newP2PBorrowIndex The updated peer-to-peer index (in ray). - function computeP2PBorrowIndex(P2PIndexComputeParams memory _params) - internal - pure - returns (uint256 newP2PBorrowIndex) - { - if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { - newP2PBorrowIndex = _params.lastP2PIndex.rayMul(_params.p2pGrowthFactor); - } else { - uint256 shareOfTheDelta = Math.min( - _params.p2pDelta.rayMul(_params.lastPoolIndex).rayDiv( - _params.p2pAmount.rayMul(_params.lastP2PIndex) - ), // Using ray division of an amount in underlying decimals by an amount in underlying decimals yields a value in ray. - WadRayMath.RAY // To avoid shareOfTheDelta > 1 with rounding errors. - ); // In ray. - - newP2PBorrowIndex = _params.lastP2PIndex.rayMul( + newP2PIndex = _params.lastP2PIndex.rayMul( (WadRayMath.RAY - shareOfTheDelta).rayMul(_params.p2pGrowthFactor) + shareOfTheDelta.rayMul(_params.poolGrowthFactor) ); diff --git a/src/compound/InterestRatesManager.sol b/src/compound/InterestRatesManager.sol index 9b2dfe8ef..78098a5ab 100644 --- a/src/compound/InterestRatesManager.sol +++ b/src/compound/InterestRatesManager.sol @@ -108,7 +108,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { _params.reserveFactor ); - newP2PSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + newP2PSupplyIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, @@ -118,7 +118,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { p2pAmount: _params.delta.p2pSupplyAmount }) ); - newP2PBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + newP2PBorrowIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, diff --git a/src/compound/lens/IndexesLens.sol b/src/compound/lens/IndexesLens.sol index ca048e3e8..77881b88d 100644 --- a/src/compound/lens/IndexesLens.sol +++ b/src/compound/lens/IndexesLens.sol @@ -132,7 +132,7 @@ abstract contract IndexesLens is LensStorage { marketParams.reserveFactor ); - indexes.p2pSupplyIndex = InterestRatesModel.computeP2PSupplyIndex( + indexes.p2pSupplyIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, @@ -142,7 +142,7 @@ abstract contract IndexesLens is LensStorage { p2pAmount: delta.p2pSupplyAmount }) ); - indexes.p2pBorrowIndex = InterestRatesModel.computeP2PBorrowIndex( + indexes.p2pBorrowIndex = InterestRatesModel.computeP2PIndex( InterestRatesModel.P2PIndexComputeParams({ poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, diff --git a/src/compound/libraries/InterestRatesModel.sol b/src/compound/libraries/InterestRatesModel.sol index 518fadb07..ce3114e3f 100644 --- a/src/compound/libraries/InterestRatesModel.sol +++ b/src/compound/libraries/InterestRatesModel.sol @@ -80,16 +80,16 @@ library InterestRatesModel { } } - /// @notice Computes and returns the new peer-to-peer supply index of a market given its parameters. + /// @notice Computes and returns the new peer-to-peer supply/borrow index of a market given its parameters. /// @param _params The computation parameters. - /// @return newP2PSupplyIndex The updated peer-to-peer index (in wad). - function computeP2PSupplyIndex(P2PIndexComputeParams memory _params) + /// @return newP2PIndex The updated peer-to-peer index (in wad). + function computeP2PIndex(P2PIndexComputeParams memory _params) internal pure - returns (uint256 newP2PSupplyIndex) + returns (uint256 newP2PIndex) { if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { - newP2PSupplyIndex = _params.lastP2PIndex.mul(_params.p2pGrowthFactor); + newP2PIndex = _params.lastP2PIndex.mul(_params.p2pGrowthFactor); } else { uint256 shareOfTheDelta = Math.min( (_params.p2pDelta.mul(_params.lastPoolIndex)).div( @@ -98,32 +98,7 @@ library InterestRatesModel { CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. ); - newP2PSupplyIndex = _params.lastP2PIndex.mul( - (CompoundMath.WAD - shareOfTheDelta).mul(_params.p2pGrowthFactor) + - shareOfTheDelta.mul(_params.poolGrowthFactor) - ); - } - } - - /// @notice Computes and returns the new peer-to-peer borrow index of a market given its parameters. - /// @param _params The computation parameters. - /// @return newP2PBorrowIndex The updated peer-to-peer index (in wad). - function computeP2PBorrowIndex(P2PIndexComputeParams memory _params) - internal - pure - returns (uint256 newP2PBorrowIndex) - { - if (_params.p2pAmount == 0 || _params.p2pDelta == 0) { - newP2PBorrowIndex = _params.lastP2PIndex.mul(_params.p2pGrowthFactor); - } else { - uint256 shareOfTheDelta = Math.min( - (_params.p2pDelta.mul(_params.lastPoolIndex)).div( - (_params.p2pAmount).mul(_params.lastP2PIndex) - ), - CompoundMath.WAD // To avoid shareOfTheDelta > 1 with rounding errors. - ); - - newP2PBorrowIndex = _params.lastP2PIndex.mul( + newP2PIndex = _params.lastP2PIndex.mul( (CompoundMath.WAD - shareOfTheDelta).mul(_params.p2pGrowthFactor) + shareOfTheDelta.mul(_params.poolGrowthFactor) ); From 77e7faceb52e39221f17292b10e83ab9d0d83975 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 9 Dec 2022 15:42:14 -0500 Subject: [PATCH 033/105] Rework conditions --- src/aave-v2/MorphoGovernance.sol | 2 +- src/compound/MorphoGovernance.sol | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 7e108ce52..176db361b 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -308,7 +308,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - if (marketPauseStatus[_poolToken].isDeprecated && !_isPaused) revert MarketIsDeprecated(); + if (!_isPaused && marketPauseStatus[_poolToken].isDeprecated) revert MarketIsDeprecated(); marketPauseStatus[_poolToken].isBorrowPaused = _isPaused; emit IsBorrowPausedSet(_poolToken, _isPaused); } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index d31e75b8d..2d151afc5 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -285,7 +285,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - if (marketPauseStatus[_poolToken].isDeprecated && !_isPaused) revert MarketIsDeprecated(); + if (!_isPaused && marketPauseStatus[_poolToken].isDeprecated) revert MarketIsDeprecated(); marketPauseStatus[_poolToken].isBorrowPaused = _isPaused; emit IsBorrowPausedSet(_poolToken, _isPaused); } @@ -381,8 +381,7 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - if (!marketPauseStatus[_poolToken].isBorrowPaused && _isDeprecated) - revert BorrowNotPaused(); + if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } From 3f2d0b91c9971cf1e5eb28a38f7da9e8211f1cc1 Mon Sep 17 00:00:00 2001 From: patrick Date: Sat, 10 Dec 2022 11:31:22 -0500 Subject: [PATCH 034/105] Remove unused errors --- src/compound/MorphoGovernance.sol | 3 --- src/compound/PositionsManager.sol | 12 ------------ 2 files changed, 15 deletions(-) diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 3099a79dd..f455b028b 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -112,9 +112,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// ERRORS /// - /// @notice Thrown when the creation of a market failed on Compound. - error MarketCreationFailedOnCompound(); - /// @notice Thrown when the input is above the max basis points value (100%). error ExceedsMaxBasisPoints(); diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index aa34fefde..815e66036 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -125,18 +125,6 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @notice Thrown when the amount repaid during the liquidation is above what is allowed to be repaid. error AmountAboveWhatAllowedToRepay(); - /// @notice Thrown when the borrow on Compound failed. - error BorrowOnCompoundFailed(); - - /// @notice Thrown when the redeem on Compound failed . - error RedeemOnCompoundFailed(); - - /// @notice Thrown when the repay on Compound failed. - error RepayOnCompoundFailed(); - - /// @notice Thrown when the mint on Compound failed. - error MintOnCompoundFailed(); - /// @notice Thrown when user is not a member of the market. error UserNotMemberOfMarket(); From cb373fef2020ac5703cf0a269452ed180250b917 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 12 Dec 2022 12:06:50 +0100 Subject: [PATCH 035/105] Renamed shadowing variable --- src/aave-v2/MatchingEngine.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/MatchingEngine.sol b/src/aave-v2/MatchingEngine.sol index 58e5ddaa3..4f4a4ad04 100644 --- a/src/aave-v2/MatchingEngine.sol +++ b/src/aave-v2/MatchingEngine.sol @@ -302,19 +302,19 @@ abstract contract MatchingEngine is MorphoUtils { Types.SupplyBalance memory supplyBalance = supplyBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketSuppliersOnPool = suppliersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketSuppliersInP2P = suppliersInP2P[_poolToken]; - uint256 maxSortedUsers = maxSortedUsers; + uint256 maxSortedUsersMem = maxSortedUsers; marketSuppliersOnPool.update( _user, marketSuppliersOnPool.getValueOf(_user), supplyBalance.onPool, - maxSortedUsers + maxSortedUsersMem ); marketSuppliersInP2P.update( _user, marketSuppliersInP2P.getValueOf(_user), supplyBalance.inP2P, - maxSortedUsers + maxSortedUsersMem ); } @@ -325,19 +325,19 @@ abstract contract MatchingEngine is MorphoUtils { Types.BorrowBalance memory borrowBalance = borrowBalanceInOf[_poolToken][_user]; HeapOrdering.HeapArray storage marketBorrowersOnPool = borrowersOnPool[_poolToken]; HeapOrdering.HeapArray storage marketBorrowersInP2P = borrowersInP2P[_poolToken]; - uint256 maxSortedUsers = maxSortedUsers; + uint256 maxSortedUsersMem = maxSortedUsers; marketBorrowersOnPool.update( _user, marketBorrowersOnPool.getValueOf(_user), borrowBalance.onPool, - maxSortedUsers + maxSortedUsersMem ); marketBorrowersInP2P.update( _user, marketBorrowersInP2P.getValueOf(_user), borrowBalance.inP2P, - maxSortedUsers + maxSortedUsersMem ); } } From 303288cd303484d1171a36aabf25e60315c758eb Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 12 Dec 2022 17:15:17 -0500 Subject: [PATCH 036/105] Implement comments --- src/compound/MorphoGovernance.sol | 8 ++++---- src/compound/PositionsManager.sol | 23 ++++++++++++++++------- test/compound/TestGovernance.t.sol | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index f455b028b..08b8659c3 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -112,6 +112,9 @@ abstract contract MorphoGovernance is MorphoUtils { /// ERRORS /// + /// @notice Thrown when the creation of a market failed on Compound and kicks back Compound error code. + error MarketCreationFailedOnCompound(uint256 errorCode); + /// @notice Thrown when the input is above the max basis points value (100%). error ExceedsMaxBasisPoints(); @@ -124,9 +127,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when the address is the zero address. error ZeroAddress(); - /// @notice Throws back Compound errors. - error CompoundError(uint256 errorCode); - /// UPGRADE /// /// @notice Initializes the Morpho contract. @@ -440,7 +440,7 @@ abstract contract MorphoGovernance is MorphoUtils { address[] memory marketToEnter = new address[](1); marketToEnter[0] = _poolToken; uint256[] memory results = comptroller.enterMarkets(marketToEnter); - if (results[0] != 0) revert CompoundError(results[0]); + if (results[0] != 0) revert MarketCreationFailedOnCompound(results[0]); // Same initial index as Compound. uint256 initialIndex; diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 815e66036..f43c27fe4 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -125,6 +125,18 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @notice Thrown when the amount repaid during the liquidation is above what is allowed to be repaid. error AmountAboveWhatAllowedToRepay(); + /// @notice Thrown when the borrow on Compound failed and throws back the Compound error code. + error BorrowOnCompoundFailed(uint256 errorCode); + + /// @notice Thrown when the redeem on Compound failed and throws back the Compound error code. + error RedeemOnCompoundFailed(uint256 errorCode); + + /// @notice Thrown when the repay on Compound failed and throws back the Compound error code. + error RepayOnCompoundFailed(uint256 errorCode); + + /// @notice Thrown when the mint on Compound failed and throws back the Compound error code. + error MintOnCompoundFailed(uint256 errorCode); + /// @notice Thrown when user is not a member of the market. error UserNotMemberOfMarket(); @@ -167,9 +179,6 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @notice Thrown when someone tries to liquidate but the liquidation with this asset as debt is paused. error LiquidateBorrowIsPaused(); - /// @notice Throws back Compound errors. - error CompoundError(uint256 errorCode); - /// STRUCTS /// // Struct to avoid stack too deep. @@ -918,7 +927,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { } else { _underlyingToken.safeApprove(_poolToken, _amount); uint256 errorCode = ICToken(_poolToken).mint(_amount); - if (errorCode != 0) revert CompoundError(errorCode); + if (errorCode != 0) revert MintOnCompoundFailed(errorCode); } } @@ -930,7 +939,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { _amount = CompoundMath.min(ICToken(_poolToken).balanceOfUnderlying(address(this)), _amount); uint256 errorCode = ICToken(_poolToken).redeemUnderlying(_amount); - if (errorCode != 0) revert CompoundError(errorCode); + if (errorCode != 0) revert RedeemOnCompoundFailed(errorCode); if (_poolToken == cEth) IWETH(address(wEth)).deposit{value: _amount}(); // Turn the ETH received in wETH. } @@ -940,7 +949,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { /// @param _amount The amount of token (in underlying). function _borrowFromPool(address _poolToken, uint256 _amount) internal { uint256 errorCode = ICToken(_poolToken).borrow(_amount); - if (errorCode != 0) revert CompoundError(errorCode); + if (errorCode != 0) revert BorrowOnCompoundFailed(errorCode); if (_poolToken == cEth) IWETH(address(wEth)).deposit{value: _amount}(); // Turn the ETH received in wETH. } @@ -967,7 +976,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { } else { _underlyingToken.safeApprove(_poolToken, _amount); uint256 errorCode = ICToken(_poolToken).repayBorrow(_amount); - if (errorCode != 0) revert CompoundError(errorCode); + if (errorCode != 0) revert RepayOnCompoundFailed(errorCode); } } } diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index 3ac17e899..d5cc187d0 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -20,7 +20,7 @@ contract TestGovernance is TestSetup { function testShouldRevertWhenCreatingMarketWithAnImproperMarket() public { Types.MarketParameters memory marketParams = Types.MarketParameters(3_333, 0); - hevm.expectRevert(abi.encodeWithSignature("CompoundError(uint256)", 9)); + hevm.expectRevert(abi.encodeWithSignature("MarketCreationFailedOnCompound(uint256)", 9)); morpho.createMarket(address(supplier1), marketParams); } From 02d6aac66c22e8bf24890c17345eb8b86355bbad Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 12 Dec 2022 17:36:15 -0500 Subject: [PATCH 037/105] Implement comments --- src/aave-v2/MorphoUtils.sol | 9 +-------- test/aave-v2/TestBorrow.t.sol | 18 ------------------ 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index ffb4a6b16..6e7c50efe 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -273,18 +273,11 @@ abstract contract MorphoUtils is MorphoStorage { if (vars.poolToken != _poolToken) _updateIndexes(vars.poolToken); + // Assumes that the Morpho contract has enabled the asset as collateral in the underlying pool. (assetData.ltv, assetData.liquidationThreshold, , assetData.decimals, ) = pool .getConfiguration(vars.underlyingToken) .getParamsMemory(); - // LTV and liquidation threshold should be zero if Morpho has not enabled this asset as collateral. - if ( - !morphoPoolConfig.isUsingAsCollateral(pool.getReserveData(vars.underlyingToken).id) - ) { - assetData.ltv = 0; - assetData.liquidationThreshold = 0; - } - unchecked { assetData.tokenUnit = 10**assetData.decimals; } diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index 5efe02eae..fc56b15f3 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -247,24 +247,6 @@ contract TestBorrow is TestSetup { borrower1.borrow(aUsdc, amount); } - function testShouldNotBorrowWithDisabledCollateral() public { - uint256 amount = 100 ether; - - borrower1.approve(dai, type(uint256).max); - borrower1.supply(aDai, amount * 10); - - // Give Morpho a position on the pool to be able to unset the DAI asset as collateral. - // Without this position on the pool, it's not possible to do so. - supplier1.approve(usdc, to6Decimals(amount)); - supplier1.supply(aUsdc, to6Decimals(amount)); - - vm.prank(address(morpho)); - pool.setUserUseReserveAsCollateral(dai, false); - - hevm.expectRevert(EntryPositionsManager.UnauthorisedBorrow.selector); - borrower1.borrow(aUsdc, to6Decimals(amount)); - } - function testBorrowLargerThanDeltaShouldClearDelta() public { // Allows only 10 unmatch suppliers. From 6cf76cc395229b13784a66122bc2a3c038b09626 Mon Sep 17 00:00:00 2001 From: Patrick Kim Date: Tue, 13 Dec 2022 09:35:58 -0500 Subject: [PATCH 038/105] Update src/aave-v2/MorphoUtils.sol Co-authored-by: Romain Milon Signed-off-by: Patrick Kim --- src/aave-v2/MorphoUtils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index 6e7c50efe..5e4c71151 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -273,7 +273,7 @@ abstract contract MorphoUtils is MorphoStorage { if (vars.poolToken != _poolToken) _updateIndexes(vars.poolToken); - // Assumes that the Morpho contract has enabled the asset as collateral in the underlying pool. + // Assumes that the Morpho contract has not disabled the asset as collateral in the underlying pool. (assetData.ltv, assetData.liquidationThreshold, , assetData.decimals, ) = pool .getConfiguration(vars.underlyingToken) .getParamsMemory(); From 96bb4fed745ed058b61cf5ae3881f546e142f45b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 13 Dec 2022 15:54:53 +0100 Subject: [PATCH 039/105] Renamed local variable --- src/aave-v2/MorphoUtils.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index e7df0da48..23ce9919e 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -311,21 +311,20 @@ abstract contract MorphoUtils is MorphoStorage { } // Cache current asset collateral value. - uint256 assetCollateralValue; if (_isSupplying(vars.userMarkets, vars.borrowMask)) { - assetCollateralValue = _collateralValue( + uint256 assetCollateralEth = _collateralValue( vars.poolToken, _user, vars.underlyingPrice, assetData.tokenUnit ); - values.collateralEth += assetCollateralValue; + values.collateralEth += assetCollateralEth; // Calculate LTV for borrow. - values.borrowableEth += assetCollateralValue.percentMul(assetData.ltv); + values.borrowableEth += assetCollateralEth.percentMul(assetData.ltv); // Update LT variable for withdraw. - if (assetCollateralValue > 0) - values.maxDebtEth += assetCollateralValue.percentMul( + if (assetCollateralEth > 0) + values.maxDebtEth += assetCollateralEth.percentMul( assetData.liquidationThreshold ); } From 0fb18c30025981791ec938d92eaf6049338c691e Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 14 Dec 2022 17:31:40 -0500 Subject: [PATCH 040/105] Add zero amount check to liquidate --- src/aave-v2/ExitPositionsManager.sol | 1 + src/compound/PositionsManager.sol | 1 + test/aave-v2/TestLiquidate.t.sol | 19 +++++++++++++++++++ test/compound/TestLiquidate.t.sol | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/src/aave-v2/ExitPositionsManager.sol b/src/aave-v2/ExitPositionsManager.sol index 8c23fdd2a..7d0fc424e 100644 --- a/src/aave-v2/ExitPositionsManager.sol +++ b/src/aave-v2/ExitPositionsManager.sol @@ -200,6 +200,7 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { address _borrower, uint256 _amount ) external { + if (_amount == 0) revert AmountIsZero(); Types.Market memory collateralMarket = market[_poolTokenCollateral]; if (!collateralMarket.isCreated) revert MarketNotCreated(); if (marketPauseStatus[_poolTokenCollateral].isLiquidateCollateralPaused) diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index d4391ddb1..0e4be1742 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -486,6 +486,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { address _borrower, uint256 _amount ) external { + if (_amount == 0) revert AmountIsZero(); if (!marketStatus[_poolTokenCollateral].isCreated) revert MarketNotCreated(); if (marketPauseStatus[_poolTokenCollateral].isLiquidateCollateralPaused) revert LiquidateCollateralIsPaused(); diff --git a/test/aave-v2/TestLiquidate.t.sol b/test/aave-v2/TestLiquidate.t.sol index 01ac0cabc..ab8f64914 100644 --- a/test/aave-v2/TestLiquidate.t.sol +++ b/test/aave-v2/TestLiquidate.t.sol @@ -26,6 +26,25 @@ contract TestLiquidate is TestSetup { liquidator.liquidate(aDai, aUsdc, address(borrower1), toRepay); } + function testShouldNotLiquidateZero() public { + uint256 collateral = 100_000 ether; + + borrower1.approve(usdc, address(morpho), to6Decimals(collateral)); + borrower1.supply(aUsdc, to6Decimals(collateral)); + + (, uint256 amount) = lens.getUserMaxCapacitiesForAsset(address(borrower1), aDai); + borrower1.borrow(aDai, amount); + + // Change Oracle + SimplePriceOracle customOracle = createAndSetCustomPriceOracle(); + customOracle.setDirectPrice(usdc, (oracle.getAssetPrice(usdc) * 95) / 100); + + User liquidator = borrower3; + liquidator.approve(dai, address(morpho), amount); + hevm.expectRevert(abi.encodeWithSignature("AmountIsZero()")); + liquidator.liquidate(aDai, aUsdc, address(borrower1), 0); + } + function testLiquidateWhenMarketDeprecated() public { uint256 amount = 10_000 ether; uint256 collateral = to6Decimals(3 * amount); diff --git a/test/compound/TestLiquidate.t.sol b/test/compound/TestLiquidate.t.sol index ef95b854e..032c37c21 100644 --- a/test/compound/TestLiquidate.t.sol +++ b/test/compound/TestLiquidate.t.sol @@ -25,6 +25,27 @@ contract TestLiquidate is TestSetup { liquidator.liquidate(cDai, cUsdc, address(borrower1), toRepay); } + function testShouldNotLiquidateZero() public { + uint256 collateral = 100_000 ether; + + borrower1.approve(usdc, address(morpho), to6Decimals(collateral)); + borrower1.supply(cUsdc, to6Decimals(collateral)); + + (, uint256 amount) = lens.getUserMaxCapacitiesForAsset(address(borrower1), cDai); + borrower1.borrow(cDai, amount); + + moveOneBlockForwardBorrowRepay(); + + // Change Oracle. + SimplePriceOracle customOracle = createAndSetCustomPriceOracle(); + customOracle.setDirectPrice(usdc, (oracle.getUnderlyingPrice(cUsdc) * 98) / 100); + + User liquidator = borrower3; + liquidator.approve(dai, address(morpho), amount); + hevm.expectRevert(abi.encodeWithSignature("AmountIsZero()")); + liquidator.liquidate(cDai, cUsdc, address(borrower1), 0); + } + function testLiquidateWhenMarketDeprecated() public { uint256 amount = 10_000 ether; uint256 collateral = to6Decimals(3 * amount); From ddc0be56bb2a05914886f9ffdc03a59d7570d142 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 15 Dec 2022 12:28:12 -0500 Subject: [PATCH 041/105] Address comments --- test/aave-v2/TestLiquidate.t.sol | 16 +--------------- test/compound/TestLiquidate.t.sol | 18 +----------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/test/aave-v2/TestLiquidate.t.sol b/test/aave-v2/TestLiquidate.t.sol index ab8f64914..87a89fd08 100644 --- a/test/aave-v2/TestLiquidate.t.sol +++ b/test/aave-v2/TestLiquidate.t.sol @@ -27,22 +27,8 @@ contract TestLiquidate is TestSetup { } function testShouldNotLiquidateZero() public { - uint256 collateral = 100_000 ether; - - borrower1.approve(usdc, address(morpho), to6Decimals(collateral)); - borrower1.supply(aUsdc, to6Decimals(collateral)); - - (, uint256 amount) = lens.getUserMaxCapacitiesForAsset(address(borrower1), aDai); - borrower1.borrow(aDai, amount); - - // Change Oracle - SimplePriceOracle customOracle = createAndSetCustomPriceOracle(); - customOracle.setDirectPrice(usdc, (oracle.getAssetPrice(usdc) * 95) / 100); - - User liquidator = borrower3; - liquidator.approve(dai, address(morpho), amount); hevm.expectRevert(abi.encodeWithSignature("AmountIsZero()")); - liquidator.liquidate(aDai, aUsdc, address(borrower1), 0); + borrower2.liquidate(aDai, aUsdc, address(borrower1), 0); } function testLiquidateWhenMarketDeprecated() public { diff --git a/test/compound/TestLiquidate.t.sol b/test/compound/TestLiquidate.t.sol index 032c37c21..c9bc8d15d 100644 --- a/test/compound/TestLiquidate.t.sol +++ b/test/compound/TestLiquidate.t.sol @@ -26,24 +26,8 @@ contract TestLiquidate is TestSetup { } function testShouldNotLiquidateZero() public { - uint256 collateral = 100_000 ether; - - borrower1.approve(usdc, address(morpho), to6Decimals(collateral)); - borrower1.supply(cUsdc, to6Decimals(collateral)); - - (, uint256 amount) = lens.getUserMaxCapacitiesForAsset(address(borrower1), cDai); - borrower1.borrow(cDai, amount); - - moveOneBlockForwardBorrowRepay(); - - // Change Oracle. - SimplePriceOracle customOracle = createAndSetCustomPriceOracle(); - customOracle.setDirectPrice(usdc, (oracle.getUnderlyingPrice(cUsdc) * 98) / 100); - - User liquidator = borrower3; - liquidator.approve(dai, address(morpho), amount); hevm.expectRevert(abi.encodeWithSignature("AmountIsZero()")); - liquidator.liquidate(cDai, cUsdc, address(borrower1), 0); + borrower2.liquidate(cDai, cUsdc, address(borrower1), 0); } function testLiquidateWhenMarketDeprecated() public { From cdac89d2b2efdcc8339715084bd39bf12360a4b0 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 15 Dec 2022 12:31:21 -0500 Subject: [PATCH 042/105] Remove unused config --- src/aave-v2/MorphoUtils.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/aave-v2/MorphoUtils.sol b/src/aave-v2/MorphoUtils.sol index b46983151..9c699e2c4 100644 --- a/src/aave-v2/MorphoUtils.sol +++ b/src/aave-v2/MorphoUtils.sol @@ -5,7 +5,6 @@ import "./interfaces/aave/IPriceOracleGetter.sol"; import "./interfaces/aave/IAToken.sol"; import "./libraries/aave/ReserveConfiguration.sol"; -import "./libraries/aave/UserConfiguration.sol"; import "@morpho-dao/morpho-utils/DelegateCall.sol"; import "@morpho-dao/morpho-utils/math/WadRayMath.sol"; @@ -20,7 +19,6 @@ import "./MorphoStorage.sol"; /// @notice Modifiers, getters and other util functions for Morpho. abstract contract MorphoUtils is MorphoStorage { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; - using UserConfiguration for DataTypes.UserConfigurationMap; using HeapOrdering for HeapOrdering.HeapArray; using PercentageMath for uint256; using DelegateCall for address; @@ -268,9 +266,6 @@ abstract contract MorphoUtils is MorphoStorage { Types.AssetLiquidityData memory assetData; LiquidityVars memory vars; - DataTypes.UserConfigurationMap memory morphoPoolConfig = pool.getUserConfiguration( - address(this) - ); vars.poolTokensLength = marketsCreated.length; vars.userMarkets = userMarkets[_user]; From 60e65f9c6aa04645a0ab3b68192edbefb3821ea3 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 15 Dec 2022 14:13:12 -0500 Subject: [PATCH 043/105] Add supply and borrow check for isFrozen --- src/aave-v2/EntryPositionsManager.sol | 15 ++++++++++++--- .../libraries/aave/ReserveConfiguration.sol | 6 +----- test/aave-v2/TestBorrow.t.sol | 17 +++++++++++++++++ test/aave-v2/TestSupply.t.sol | 18 ++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index f50b5f65a..3adf793d5 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -63,6 +63,9 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils /// @notice Thrown when someone tries to borrow but the borrow is paused. error BorrowIsPaused(); + /// @notice Thrown when underlying pool is frozen. + error FrozenOnPool(); + /// STRUCTS /// // Struct to avoid stack too deep. @@ -90,13 +93,15 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (_onBehalf == address(0)) revert AddressIsZero(); if (_amount == 0) revert AmountIsZero(); Types.Market memory market = market[_poolToken]; + ERC20 underlyingToken = ERC20(market.underlyingToken); + if (!market.isCreated) revert MarketNotCreated(); if (marketPauseStatus[_poolToken].isSupplyPaused) revert SupplyIsPaused(); + if (pool.getConfiguration(address(underlyingToken)).getFrozen()) revert FrozenOnPool(); _updateIndexes(_poolToken); _setSupplying(_onBehalf, borrowMask[_poolToken], true); - ERC20 underlyingToken = ERC20(market.underlyingToken); underlyingToken.safeTransferFrom(_from, address(this), _amount); Types.Delta storage delta = deltas[_poolToken]; @@ -189,8 +194,12 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowIsPaused(); ERC20 underlyingToken = ERC20(market.underlyingToken); - if (!pool.getConfiguration(address(underlyingToken)).getBorrowingEnabled()) - revert BorrowingNotEnabled(); + DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration( + address(underlyingToken) + ); + + if (!reserveConfig.getBorrowingEnabled()) revert BorrowingNotEnabled(); + if (reserveConfig.getFrozen()) revert FrozenOnPool(); _updateIndexes(_poolToken); _setBorrowing(msg.sender, borrowMask[_poolToken], true); diff --git a/src/aave-v2/libraries/aave/ReserveConfiguration.sol b/src/aave-v2/libraries/aave/ReserveConfiguration.sol index 063a8740b..24397a349 100644 --- a/src/aave-v2/libraries/aave/ReserveConfiguration.sol +++ b/src/aave-v2/libraries/aave/ReserveConfiguration.sol @@ -182,11 +182,7 @@ library ReserveConfiguration { * @param self The reserve configuration * @return The frozen state **/ - function getFrozen(DataTypes.ReserveConfigurationMap storage self) - internal - view - returns (bool) - { + function getFrozen(DataTypes.ReserveConfigurationMap memory self) internal pure returns (bool) { return (self.data & ~FROZEN_MASK) != 0; } diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index fc56b15f3..c6a355773 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -247,6 +247,23 @@ contract TestBorrow is TestSetup { borrower1.borrow(aUsdc, amount); } + function testCannotBorrowOnFrozenPool() public { + uint256 amount = 10_000 ether; + + DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration(dai); + reserveConfig.setFrozen(true); + + borrower1.approve(dai, amount); + borrower1.supply(aDai, amount); + + // Lending pool configurator + vm.prank(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); + pool.setConfiguration(dai, reserveConfig.data); + + hevm.expectRevert(EntryPositionsManager.FrozenOnPool.selector); + borrower1.borrow(aDai, amount); + } + function testBorrowLargerThanDeltaShouldClearDelta() public { // Allows only 10 unmatch suppliers. diff --git a/test/aave-v2/TestSupply.t.sol b/test/aave-v2/TestSupply.t.sol index 4f5c61b7d..989464e05 100644 --- a/test/aave-v2/TestSupply.t.sol +++ b/test/aave-v2/TestSupply.t.sol @@ -5,6 +5,7 @@ import "./setup/TestSetup.sol"; contract TestSupply is TestSetup { using stdStorage for StdStorage; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using WadRayMath for uint256; // There are no available borrowers: all of the supplied amount is supplied to the pool and set `onPool`. @@ -264,6 +265,23 @@ contract TestSupply is TestSetup { supplier1.supply(aDai, amount); } + function testCannotSupplyOnFrozenPool() public { + uint256 amount = 10_000 ether; + + DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration(dai); + reserveConfig.setFrozen(true); + + // Lending pool configurator + vm.prank(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); + pool.setConfiguration(dai, reserveConfig.data); + + supplier1.approve(dai, amount); + + hevm.expectRevert(EntryPositionsManager.FrozenOnPool.selector); + + supplier1.supply(aDai, amount); + } + function testShouldMatchSupplyWithCorrectAmountOfGas() public { uint256 amount = 100 ether; createSigners(30); From d177f82a5c3f746cddb7d5c668be4c44f716b774 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 16 Dec 2022 10:13:42 -0500 Subject: [PATCH 044/105] Use config --- test/aave-v2/TestBorrow.t.sol | 3 +-- test/aave-v2/TestSupply.t.sol | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index c6a355773..f97c041e8 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -256,8 +256,7 @@ contract TestBorrow is TestSetup { borrower1.approve(dai, amount); borrower1.supply(aDai, amount); - // Lending pool configurator - vm.prank(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); + vm.prank(address(lendingPoolConfigurator)); pool.setConfiguration(dai, reserveConfig.data); hevm.expectRevert(EntryPositionsManager.FrozenOnPool.selector); diff --git a/test/aave-v2/TestSupply.t.sol b/test/aave-v2/TestSupply.t.sol index 989464e05..3977d8a1c 100644 --- a/test/aave-v2/TestSupply.t.sol +++ b/test/aave-v2/TestSupply.t.sol @@ -271,8 +271,7 @@ contract TestSupply is TestSetup { DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration(dai); reserveConfig.setFrozen(true); - // Lending pool configurator - vm.prank(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); + vm.prank(address(lendingPoolConfigurator)); pool.setConfiguration(dai, reserveConfig.data); supplier1.approve(dai, amount); From 142d2358c743031f4526f6ea5a1b9059870f8b84 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 16 Dec 2022 16:42:45 +0100 Subject: [PATCH 045/105] update units in comments --- src/aave-v2/MorphoStorage.sol | 2 +- src/aave-v2/lens/LensStorage.sol | 2 +- src/aave-v2/libraries/Types.sol | 16 ++++++++-------- src/compound/PositionsManager.sol | 4 ++-- src/compound/lens/UsersLens.sol | 10 +++++----- src/compound/libraries/Types.sol | 28 ++++++++++++++-------------- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/aave-v2/MorphoStorage.sol b/src/aave-v2/MorphoStorage.sol index c52711f53..6d7be2416 100644 --- a/src/aave-v2/MorphoStorage.sol +++ b/src/aave-v2/MorphoStorage.sol @@ -32,7 +32,7 @@ abstract contract MorphoStorage is OwnableUpgradeable, ReentrancyGuardUpgradeabl address public constant ST_ETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; // stETH is a rebasing token, so the rebase index's value when the astEth market was created is stored - // and used for internal calculations to convert `stEth.balanceOf` into an amount in scaled units. + // and used for internal calculations to convert `stEth.balanceOf` into an amount in pool supply unit. uint256 public constant ST_ETH_BASE_REBASE_INDEX = 1_086492192583716523804482274; bool public isClaimRewardsPaused; // Deprecated: whether claiming rewards is paused or not. diff --git a/src/aave-v2/lens/LensStorage.sol b/src/aave-v2/lens/LensStorage.sol index 9b060313a..eb826e778 100644 --- a/src/aave-v2/lens/LensStorage.sol +++ b/src/aave-v2/lens/LensStorage.sol @@ -29,7 +29,7 @@ abstract contract LensStorage is ILens { /// IMMUTABLES /// // stETH is a rebasing token, so the rebase index's value when the astEth market was created is stored - // and used for internal calculations to convert `stEth.balanceOf` into an amount in scaled units. + // and used for internal calculations to convert `stEth.balanceOf` into an amount in pool supply unit. uint256 public immutable ST_ETH_BASE_REBASE_INDEX; IMorpho public immutable morpho; diff --git a/src/aave-v2/libraries/Types.sol b/src/aave-v2/libraries/Types.sol index cb7b2672b..3c1edae6e 100644 --- a/src/aave-v2/libraries/Types.sol +++ b/src/aave-v2/libraries/Types.sol @@ -18,20 +18,20 @@ library Types { /// STRUCTS /// struct SupplyBalance { - uint256 inP2P; // In peer-to-peer supply scaled unit, a unit that grows in underlying value, to keep track of the interests earned by suppliers in peer-to-peer. Multiply by the peer-to-peer supply index to get the underlying amount. - uint256 onPool; // In pool supply scaled unit. Multiply by the pool supply index to get the underlying amount. + uint256 inP2P; // In peer-to-peer supply unit, a unit that grows in underlying value, to keep track of the interests earned by suppliers in peer-to-peer. Multiply by the peer-to-peer supply index to get the underlying amount. + uint256 onPool; // In pool supply unit. Multiply by the pool supply index to get the underlying amount. } struct BorrowBalance { - uint256 inP2P; // In peer-to-peer borrow scaled unit, a unit that grows in underlying value, to keep track of the interests paid by borrowers in peer-to-peer. Multiply by the peer-to-peer borrow index to get the underlying amount. - uint256 onPool; // In pool borrow scaled unit, a unit that grows in value, to keep track of the debt increase when borrowers are on Aave. Multiply by the pool borrow index to get the underlying amount. + uint256 inP2P; // In peer-to-peer borrow unit, a unit that grows in underlying value, to keep track of the interests paid by borrowers in peer-to-peer. Multiply by the peer-to-peer borrow index to get the underlying amount. + uint256 onPool; // In pool borrow unit, a unit that grows in value, to keep track of the debt increase when borrowers are on Aave. Multiply by the pool borrow index to get the underlying amount. } struct Indexes { - uint256 p2pSupplyIndex; // The peer-to-peer supply index (in ray), used to multiply the scaled peer-to-peer supply balance and get the peer-to-peer supply balance (in underlying). - uint256 p2pBorrowIndex; // The peer-to-peer borrow index (in ray), used to multiply the scaled peer-to-peer borrow balance and get the peer-to-peer borrow balance (in underlying). - uint256 poolSupplyIndex; // The pool supply index (in ray), used to multiply the scaled pool supply balance and get the pool supply balance (in underlying). - uint256 poolBorrowIndex; // The pool borrow index (in ray), used to multiply the scaled pool borrow balance and get the pool borrow balance (in underlying). + uint256 p2pSupplyIndex; // The peer-to-peer supply index (in ray), used to multiply the peer-to-peer supply scaled balance and get the peer-to-peer supply balance (in underlying). + uint256 p2pBorrowIndex; // The peer-to-peer borrow index (in ray), used to multiply the peer-to-peer borrow scaled balance and get the peer-to-peer borrow balance (in underlying). + uint256 poolSupplyIndex; // The pool supply index (in ray), used to multiply the pool supply scaled balance and get the pool supply balance (in underlying). + uint256 poolBorrowIndex; // The pool borrow index (in ray), used to multiply the pool borrow scaled balance and get the pool borrow balance (in underlying). } // Max gas to consume during the matching process for supply, borrow, withdraw and repay functions. diff --git a/src/compound/PositionsManager.sol b/src/compound/PositionsManager.sol index 0e4be1742..2d0ed1efa 100644 --- a/src/compound/PositionsManager.sol +++ b/src/compound/PositionsManager.sol @@ -408,7 +408,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { if (remainingToBorrow > 0) { borrowerBorrowBalance.onPool += remainingToBorrow.div( lastPoolIndexes[_poolToken].lastBorrowPoolIndex - ); // In cdUnit. + ); // In pool borrow unit. _borrowFromPool(_poolToken, remainingToBorrow); } @@ -779,7 +779,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { borrowerBorrowBalance.onPool -= CompoundMath.min( vars.borrowedOnPool, vars.toRepay.div(vars.poolBorrowIndex) - ); // In cdUnit. + ); // In pool borrow unit. _updateBorrowerInDS(_poolToken, _onBehalf); _repayToPool(_poolToken, underlyingToken, vars.toRepay); // Reverts on error. diff --git a/src/compound/lens/UsersLens.sol b/src/compound/lens/UsersLens.sol index 519221249..c959afca2 100644 --- a/src/compound/lens/UsersLens.sol +++ b/src/compound/lens/UsersLens.sol @@ -151,8 +151,8 @@ abstract contract UsersLens is IndexesLens { /// @param _poolToken The market to hypothetically withdraw/borrow in. /// @param _withdrawnAmount The amount to hypothetically withdraw from the given market (in underlying). /// @param _borrowedAmount The amount to hypothetically borrow from the given market (in underlying). - /// @return debtUsd The current debt value of the user. - /// @return maxDebtUsd The maximum debt value possible of the user. + /// @return debtUsd The current debt value of the user (in wad). + /// @return maxDebtUsd The maximum debt value possible of the user (in wad). function getUserHypotheticalBalanceStates( address _user, address _poolToken, @@ -187,9 +187,9 @@ abstract contract UsersLens is IndexesLens { /// @notice Returns the collateral value, debt value and max debt value of a given user. /// @param _user The user to determine liquidity for. /// @param _updatedMarkets The list of markets of which to compute virtually updated pool and peer-to-peer indexes. - /// @return collateralUsd The collateral value of the user. - /// @return debtUsd The current debt value of the user. - /// @return maxDebtUsd The maximum possible debt value of the user. + /// @return collateralUsd The collateral value of the user (in wad). + /// @return debtUsd The current debt value of the user (in wad). + /// @return maxDebtUsd The maximum possible debt value of the user (in wad). function getUserBalanceStates(address _user, address[] calldata _updatedMarkets) public view diff --git a/src/compound/libraries/Types.sol b/src/compound/libraries/Types.sol index c70e8e3e4..0cc3e6608 100644 --- a/src/compound/libraries/Types.sol +++ b/src/compound/libraries/Types.sol @@ -18,20 +18,20 @@ library Types { /// STRUCTS /// struct SupplyBalance { - uint256 inP2P; // In peer-to-peer supply scaled unit, a unit that grows in underlying value, to keep track of the interests earned by suppliers in peer-to-peer. Multiply by the peer-to-peer supply index to get the underlying amount. - uint256 onPool; // In pool supply scaled unit. Multiply by the pool supply index to get the underlying amount. + uint256 inP2P; // In peer-to-peer supply unit, a unit that grows in underlying value, to keep track of the interests earned by suppliers in peer-to-peer. Multiply by the peer-to-peer supply index to get the underlying amount. + uint256 onPool; // In pool supply unit. Multiply by the pool supply index to get the underlying amount. } struct BorrowBalance { - uint256 inP2P; // In peer-to-peer borrow scaled unit, a unit that grows in underlying value, to keep track of the interests paid by borrowers in peer-to-peer. Multiply by the peer-to-peer borrow index to get the underlying amount. + uint256 inP2P; // In peer-to-peer borrow unit, a unit that grows in underlying value, to keep track of the interests paid by borrowers in peer-to-peer. Multiply by the peer-to-peer borrow index to get the underlying amount. uint256 onPool; // In pool borrow unit, a unit that grows in value, to keep track of the debt increase when borrowers are on Compound. Multiply by the pool borrow index to get the underlying amount. } struct Indexes { - uint256 p2pSupplyIndex; // The peer-to-peer supply index (in wad), used to multiply the scaled peer-to-peer supply balance and get the peer-to-peer supply balance (in underlying). - uint256 p2pBorrowIndex; // The peer-to-peer borrow index (in wad), used to multiply the scaled peer-to-peer borrow balance and get the peer-to-peer borrow balance (in underlying). - uint256 poolSupplyIndex; // The pool supply index (in wad), used to multiply the scaled pool supply balance and get the pool supply balance (in underlying). - uint256 poolBorrowIndex; // The pool borrow index (in wad), used to multiply the scaled pool borrow balance and get the pool borrow balance (in underlying). + uint256 p2pSupplyIndex; // The peer-to-peer supply index (in wad), used to multiply the peer-to-peer supply scaled balance and get the peer-to-peer supply balance (in underlying). + uint256 p2pBorrowIndex; // The peer-to-peer borrow index (in wad), used to multiply the peer-to-peer borrow scaled balance and get the peer-to-peer borrow balance (in underlying). + uint256 poolSupplyIndex; // The pool supply index (in wad), used to multiply the pool supply scaled balance and get the pool supply balance (in underlying). + uint256 poolBorrowIndex; // The pool borrow index (in wad), used to multiply the pool borrow scaled balance and get the pool borrow balance (in underlying). } // Max gas to consume during the matching process for supply, borrow, withdraw and repay functions. @@ -50,17 +50,17 @@ library Types { } struct AssetLiquidityData { - uint256 collateralUsd; // The collateral value of the asset (in USD, wad). - uint256 maxDebtUsd; // The maximum possible debt value of the asset (in USD, wad). - uint256 debtUsd; // The debt value of the asset (in USD, wad). + uint256 collateralUsd; // The collateral value of the asset (in wad). + uint256 maxDebtUsd; // The maximum possible debt value of the asset (in wad). + uint256 debtUsd; // The debt value of the asset (in wad). uint256 underlyingPrice; // The price of the token. - uint256 collateralFactor; // The liquidation threshold applied on this token. + uint256 collateralFactor; // The liquidation threshold applied on this token (in wad). } struct LiquidityData { - uint256 collateralUsd; // The collateral value (in USD, wad). - uint256 maxDebtUsd; // The maximum debt value allowed before being liquidatable (in USD, wad). - uint256 debtUsd; // The debt value (in USD, wad). + uint256 collateralUsd; // The collateral value (in wad). + uint256 maxDebtUsd; // The maximum debt value allowed before being liquidatable (in wad). + uint256 debtUsd; // The debt value (in wad). } // Variables are packed together to save gas (will not exceed their limit during Morpho's lifetime). From 7c43753739a2cd53752ffda291a6b34f97f605ec Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 19 Dec 2022 16:12:35 +0100 Subject: [PATCH 046/105] Apply suggestions --- src/aave-v2/InterestRatesManager.sol | 22 +++++++++++----------- src/compound/InterestRatesManager.sol | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/aave-v2/InterestRatesManager.sol b/src/aave-v2/InterestRatesManager.sol index b19d26fee..69826d0c4 100644 --- a/src/aave-v2/InterestRatesManager.sol +++ b/src/aave-v2/InterestRatesManager.sol @@ -55,22 +55,22 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { if (block.timestamp == marketPoolIndexes.lastUpdateTimestamp) return; - Types.Market memory market = market[_poolToken]; + Types.Market storage market = market[_poolToken]; (uint256 newPoolSupplyIndex, uint256 newPoolBorrowIndex) = _getPoolIndexes( market.underlyingToken ); (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes( - Params( - p2pSupplyIndex[_poolToken], - p2pBorrowIndex[_poolToken], - newPoolSupplyIndex, - newPoolBorrowIndex, - marketPoolIndexes, - market.reserveFactor, - market.p2pIndexCursor, - deltas[_poolToken] - ) + Params({ + lastP2PSupplyIndex: p2pSupplyIndex[_poolToken], + lastP2PBorrowIndex: p2pBorrowIndex[_poolToken], + poolSupplyIndex: newPoolSupplyIndex, + poolBorrowIndex: newPoolBorrowIndex, + lastPoolIndexes: marketPoolIndexes, + reserveFactor: market.reserveFactor, + p2pIndexCursor: market.p2pIndexCursor, + delta: deltas[_poolToken] + }) ); p2pSupplyIndex[_poolToken] = newP2PSupplyIndex; diff --git a/src/compound/InterestRatesManager.sol b/src/compound/InterestRatesManager.sol index 78098a5ab..cf49ab03f 100644 --- a/src/compound/InterestRatesManager.sol +++ b/src/compound/InterestRatesManager.sol @@ -52,7 +52,7 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { function updateP2PIndexes(address _poolToken) external { Types.LastPoolIndexes storage poolIndexes = lastPoolIndexes[_poolToken]; - if (block.number <= poolIndexes.lastUpdateBlockNumber) return; + if (block.number == poolIndexes.lastUpdateBlockNumber) return; Types.MarketParameters memory marketParams = marketParameters[_poolToken]; @@ -60,16 +60,16 @@ contract InterestRatesManager is IInterestRatesManager, MorphoStorage { uint256 poolBorrowIndex = ICToken(_poolToken).borrowIndex(); (uint256 newP2PSupplyIndex, uint256 newP2PBorrowIndex) = _computeP2PIndexes( - Params( - p2pSupplyIndex[_poolToken], - p2pBorrowIndex[_poolToken], - poolSupplyIndex, - poolBorrowIndex, - poolIndexes, - marketParams.reserveFactor, - marketParams.p2pIndexCursor, - deltas[_poolToken] - ) + Params({ + lastP2PSupplyIndex: p2pSupplyIndex[_poolToken], + lastP2PBorrowIndex: p2pBorrowIndex[_poolToken], + poolSupplyIndex: poolSupplyIndex, + poolBorrowIndex: poolBorrowIndex, + lastPoolIndexes: poolIndexes, + reserveFactor: marketParams.reserveFactor, + p2pIndexCursor: marketParams.p2pIndexCursor, + delta: deltas[_poolToken] + }) ); p2pSupplyIndex[_poolToken] = newP2PSupplyIndex; From 3dd54181e05f9fe35cedbaf42090d553904b12ce Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 19 Dec 2022 16:31:11 -0500 Subject: [PATCH 047/105] fix: update aave test --- test/aave-v2/TestLens.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/aave-v2/TestLens.t.sol b/test/aave-v2/TestLens.t.sol index 97576161b..7ccae9158 100644 --- a/test/aave-v2/TestLens.t.sol +++ b/test/aave-v2/TestLens.t.sol @@ -1398,6 +1398,7 @@ contract TestLens is TestSetup { assertFalse(lens.isLiquidatable(address(borrower1), aUsdc)); + morpho.setIsBorrowPaused(aUsdc, true); morpho.setIsDeprecated(aUsdc, true); assertTrue(lens.isLiquidatable(address(borrower1), aUsdc)); From 418220e44d747080aa0de2de4b9458128c4c815b Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 20 Dec 2022 09:58:24 -0500 Subject: [PATCH 048/105] chore: remove compound lib --- src/compound/libraries/CompoundMath.sol | 52 ------------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/compound/libraries/CompoundMath.sol diff --git a/src/compound/libraries/CompoundMath.sol b/src/compound/libraries/CompoundMath.sol deleted file mode 100644 index 18a8bf7c9..000000000 --- a/src/compound/libraries/CompoundMath.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; - -/// @title CompoundMath. -/// @author Morpho Labs. -/// @custom:contact security@morpho.xyz -/// @dev Library emulating in solidity 8+ the behavior of Compound's mulScalarTruncate and divScalarByExpTruncate functions. -library CompoundMath { - /// ERRORS /// - - /// @notice Reverts when the number exceeds 224 bits. - error NumberExceeds224Bits(); - - /// @notice Reverts when the number exceeds 32 bits. - error NumberExceeds32Bits(); - - /// INTERNAL /// - - function mul(uint256 x, uint256 y) internal pure returns (uint256) { - return (x * y) / 1e18; - } - - function div(uint256 x, uint256 y) internal pure returns (uint256) { - return ((1e18 * x * 1e18) / y) / 1e18; - } - - function safe224(uint256 n) internal pure returns (uint224) { - if (n >= 2**224) revert NumberExceeds224Bits(); - return uint224(n); - } - - function safe32(uint256 n) internal pure returns (uint32) { - if (n >= 2**32) revert NumberExceeds32Bits(); - return uint32(n); - } - - function min( - uint256 a, - uint256 b, - uint256 c - ) internal pure returns (uint256) { - return a < b ? a < c ? a : c : b < c ? b : c; - } - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } - - function safeSub(uint256 a, uint256 b) internal pure returns (uint256) { - return a >= b ? a - b : 0; - } -} From c79737c263eb1f8ae1a59583a4dea92fedd6a66b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 27 Dec 2022 10:17:13 +0100 Subject: [PATCH 049/105] Update morpho-data-structures --- .gitmodules | 2 +- lib/morpho-data-structures | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 7347a404d..a31b3d9b0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,7 @@ [submodule "lib/morpho-data-structures"] path = lib/morpho-data-structures url = https://github.com/morpho-dao/morpho-data-structures - branch = morpho-v1 + branch = upgrade-morpho-1 [submodule "lib/morpho-utils"] path = lib/morpho-utils url = https://github.com/morpho-dao/morpho-utils diff --git a/lib/morpho-data-structures b/lib/morpho-data-structures index f19cd8176..aefcf40ec 160000 --- a/lib/morpho-data-structures +++ b/lib/morpho-data-structures @@ -1 +1 @@ -Subproject commit f19cd81768febf42bf6325b5fa288ed36d39c2ea +Subproject commit aefcf40ec879dfeb8275ab51c528676d9dbfa705 From 7c83fe65902e98bbbf574de0b247e745a88ec845 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 30 Dec 2022 14:19:55 +0100 Subject: [PATCH 050/105] =?UTF-8?q?=F0=9F=94=A5=20(#1565)=20Remove=20incen?= =?UTF-8?q?tives=20vault=20from=20Compound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/eth-mainnet/compound/Config.sol | 2 - scripts/aave-v2/Deploy.s.sol | 2 - scripts/compound/Deploy.s.sol | 3 - src/compound/IncentivesVault.sol | 155 ------------------- src/compound/Morpho.sol | 13 +- src/compound/MorphoGovernance.sol | 11 -- src/compound/MorphoStorage.sol | 3 +- src/compound/interfaces/IIncentivesVault.sol | 28 ---- src/compound/interfaces/IMorpho.sol | 4 +- test/compound/TestGovernance.t.sol | 17 -- test/compound/TestIncentivesVault.t.sol | 116 -------------- test/compound/TestRewards.t.sol | 29 ---- test/compound/helpers/IncentivesVault.sol | 36 ----- test/compound/setup/TestSetup.sol | 11 -- test/prod/compound/setup/TestSetup.sol | 2 - 15 files changed, 5 insertions(+), 427 deletions(-) delete mode 100644 src/compound/IncentivesVault.sol delete mode 100644 src/compound/interfaces/IIncentivesVault.sol delete mode 100644 test/compound/TestIncentivesVault.t.sol delete mode 100644 test/compound/helpers/IncentivesVault.sol diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index 6aec7318e..86249f860 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -2,7 +2,6 @@ pragma solidity >=0.8.0; import "src/compound/interfaces/compound/ICompound.sol"; -import {IIncentivesVault} from "src/compound/interfaces/IIncentivesVault.sol"; import {IPositionsManager} from "src/compound/interfaces/IPositionsManager.sol"; import {IInterestRatesManager} from "src/compound/interfaces/IInterestRatesManager.sol"; import {IMorpho} from "src/compound/interfaces/IMorpho.sol"; @@ -53,7 +52,6 @@ contract Config is BaseConfig { Lens public lens; Morpho public morpho; RewardsManager public rewardsManager; - IIncentivesVault public incentivesVault; IPositionsManager public positionsManager; IInterestRatesManager public interestRatesManager; } diff --git a/scripts/aave-v2/Deploy.s.sol b/scripts/aave-v2/Deploy.s.sol index 552a9aac3..2480f434c 100644 --- a/scripts/aave-v2/Deploy.s.sol +++ b/scripts/aave-v2/Deploy.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity 0.8.13; -import "src/aave-v2/interfaces/IIncentivesVault.sol"; import "src/aave-v2/interfaces/IInterestRatesManager.sol"; import "src/aave-v2/interfaces/IExitPositionsManager.sol"; import "src/aave-v2/interfaces/IEntryPositionsManager.sol"; @@ -27,7 +26,6 @@ contract Deploy is Script, Config { IEntryPositionsManager public entryPositionsManager; IExitPositionsManager public exitPositionsManager; IInterestRatesManager public interestRatesManager; - IIncentivesVault public incentivesVault; function run() external { vm.startBroadcast(); diff --git a/scripts/compound/Deploy.s.sol b/scripts/compound/Deploy.s.sol index 6f33f71d3..30352e2ca 100644 --- a/scripts/compound/Deploy.s.sol +++ b/scripts/compound/Deploy.s.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.13; import "src/compound/interfaces/IRewardsManager.sol"; -import "src/compound/interfaces/IIncentivesVault.sol"; import "src/compound/interfaces/IInterestRatesManager.sol"; import "src/compound/interfaces/IPositionsManager.sol"; import "src/compound/interfaces/compound/ICompound.sol"; @@ -10,7 +9,6 @@ import "src/compound/interfaces/compound/ICompound.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import {IncentivesVault} from "src/compound/IncentivesVault.sol"; import {RewardsManager} from "src/compound/RewardsManager.sol"; import {InterestRatesManager} from "src/compound/InterestRatesManager.sol"; import {PositionsManager} from "src/compound/PositionsManager.sol"; @@ -27,7 +25,6 @@ contract Deploy is Script, Config { Morpho public morpho; IPositionsManager public positionsManager; IInterestRatesManager public interestRatesManager; - IIncentivesVault public incentivesVault; RewardsManager public rewardsManager; function run() external { diff --git a/src/compound/IncentivesVault.sol b/src/compound/IncentivesVault.sol deleted file mode 100644 index 7638bd23e..000000000 --- a/src/compound/IncentivesVault.sol +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity 0.8.13; - -import "./interfaces/IIncentivesVault.sol"; -import "./interfaces/IOracle.sol"; -import "./interfaces/IMorpho.sol"; - -import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; -import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; - -import "@openzeppelin/contracts/access/Ownable.sol"; - -/// @title IncentivesVault. -/// @author Morpho Labs. -/// @custom:contact security@morpho.xyz -/// @notice Contract handling Morpho incentives. -contract IncentivesVault is IIncentivesVault, Ownable { - using SafeTransferLib for ERC20; - using PercentageMath for uint256; - - /// STORAGE /// - - uint256 public constant MAX_BASIS_POINTS = 100_00; - - IMorpho public immutable morpho; // The address of the main Morpho contract. - IComptroller public immutable comptroller; // Compound's comptroller proxy. - ERC20 public immutable morphoToken; // The MORPHO token. - - IOracle public oracle; // The oracle used to get the price of MORPHO tokens against COMP tokens. - address public incentivesTreasuryVault; // The address of the incentives treasury vault. - uint256 public bonus; // The bonus percentage of MORPHO tokens to give to the user. - bool public isPaused; // Whether the trade of COMP rewards for MORPHO rewards is paused or not. - - /// EVENTS /// - - /// @notice Emitted when the oracle is set. - /// @param newOracle The new oracle set. - event OracleSet(address newOracle); - - /// @notice Emitted when the incentives treasury vault is set. - /// @param newIncentivesTreasuryVault The address of the incentives treasury vault. - event IncentivesTreasuryVaultSet(address newIncentivesTreasuryVault); - - /// @notice Emitted when the reward bonus is set. - /// @param newBonus The new bonus set. - event BonusSet(uint256 newBonus); - - /// @notice Emitted when the pause status is changed. - /// @param newStatus The new newStatus set. - event PauseStatusSet(bool newStatus); - - /// @notice Emitted when tokens are transferred to the DAO. - /// @param token The address of the token transferred. - /// @param amount The amount of token transferred to the DAO. - event TokensTransferred(address indexed token, uint256 amount); - - /// @notice Emitted when COMP tokens are traded for MORPHO tokens. - /// @param receiver The address of the receiver. - /// @param compAmount The amount of COMP traded. - /// @param morphoAmount The amount of MORPHO sent. - event CompTokensTraded(address indexed receiver, uint256 compAmount, uint256 morphoAmount); - - /// ERRORS /// - - /// @notice Thrown when an other address than Morpho triggers the function. - error OnlyMorpho(); - - /// @notice Thrown when the vault is paused. - error VaultIsPaused(); - - /// @notice Thrown when the input is above the max basis points value (100%). - error ExceedsMaxBasisPoints(); - - /// CONSTRUCTOR /// - - /// @notice Constructs the IncentivesVault contract. - /// @param _comptroller The Compound comptroller. - /// @param _morpho The main Morpho contract. - /// @param _morphoToken The MORPHO token. - /// @param _incentivesTreasuryVault The address of the incentives treasury vault. - /// @param _oracle The oracle. - constructor( - IComptroller _comptroller, - IMorpho _morpho, - ERC20 _morphoToken, - address _incentivesTreasuryVault, - IOracle _oracle - ) { - morpho = _morpho; - comptroller = _comptroller; - morphoToken = _morphoToken; - incentivesTreasuryVault = _incentivesTreasuryVault; - oracle = _oracle; - } - - /// EXTERNAL /// - - /// @notice Sets the oracle. - /// @param _newOracle The address of the new oracle. - function setOracle(IOracle _newOracle) external onlyOwner { - oracle = _newOracle; - emit OracleSet(address(_newOracle)); - } - - /// @notice Sets the incentives treasury vault. - /// @param _newIncentivesTreasuryVault The address of the incentives treasury vault. - function setIncentivesTreasuryVault(address _newIncentivesTreasuryVault) external onlyOwner { - incentivesTreasuryVault = _newIncentivesTreasuryVault; - emit IncentivesTreasuryVaultSet(_newIncentivesTreasuryVault); - } - - /// @notice Sets the reward bonus. - /// @param _newBonus The new reward bonus. - function setBonus(uint256 _newBonus) external onlyOwner { - if (_newBonus > MAX_BASIS_POINTS) revert ExceedsMaxBasisPoints(); - - bonus = _newBonus; - emit BonusSet(_newBonus); - } - - /// @notice Sets the pause status. - /// @param _newStatus The new pause status. - function setPauseStatus(bool _newStatus) external onlyOwner { - isPaused = _newStatus; - emit PauseStatusSet(_newStatus); - } - - /// @notice Transfers the specified token to the DAO. - /// @param _token The address of the token to transfer. - /// @param _amount The amount of token to transfer to the DAO. - function transferTokensToDao(address _token, uint256 _amount) external onlyOwner { - ERC20(_token).safeTransfer(incentivesTreasuryVault, _amount); - emit TokensTransferred(_token, _amount); - } - - /// @notice Trades COMP tokens for MORPHO tokens and sends them to the receiver. - /// @param _receiver The address of the receiver. - /// @param _amount The amount to transfer to the receiver. - function tradeCompForMorphoTokens(address _receiver, uint256 _amount) external { - if (msg.sender != address(morpho)) revert OnlyMorpho(); - if (isPaused) revert VaultIsPaused(); - // Transfer COMP to the DAO. - ERC20(comptroller.getCompAddress()).safeTransferFrom( - msg.sender, - incentivesTreasuryVault, - _amount - ); - - // Add a bonus on MORPHO rewards. - uint256 amountOut = oracle.consult(_amount).percentAdd(bonus); - morphoToken.safeTransfer(_receiver, amountOut); - - emit CompTokensTraded(_receiver, _amount, amountOut); - } -} diff --git a/src/compound/Morpho.sol b/src/compound/Morpho.sol index 21824e2ef..722948c8a 100644 --- a/src/compound/Morpho.sol +++ b/src/compound/Morpho.sol @@ -146,9 +146,8 @@ contract Morpho is MorphoGovernance { /// @notice Claims rewards for the given assets. /// @param _cTokenAddresses The cToken addresses to claim rewards from. - /// @param _tradeForMorphoToken Whether or not to trade COMP tokens for MORPHO tokens. /// @return amountOfRewards The amount of rewards claimed (in COMP). - function claimRewards(address[] calldata _cTokenAddresses, bool _tradeForMorphoToken) + function claimRewards(address[] calldata _cTokenAddresses, bool) external nonReentrant returns (uint256 amountOfRewards) @@ -157,16 +156,10 @@ contract Morpho is MorphoGovernance { amountOfRewards = rewardsManager.claimRewards(_cTokenAddresses, msg.sender); if (amountOfRewards > 0) { - ERC20 comp = ERC20(comptroller.getCompAddress()); - comptroller.claimComp(address(this), _cTokenAddresses); + ERC20(comptroller.getCompAddress()).safeTransfer(msg.sender, amountOfRewards); - if (_tradeForMorphoToken) { - comp.safeApprove(address(incentivesVault), amountOfRewards); - incentivesVault.tradeCompForMorphoTokens(msg.sender, amountOfRewards); - } else comp.safeTransfer(msg.sender, amountOfRewards); - - emit RewardsClaimed(msg.sender, amountOfRewards, _tradeForMorphoToken); + emit RewardsClaimed(msg.sender, amountOfRewards, false); } } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index cb80d6020..adfbb5cbf 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -25,10 +25,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @param _newTreasuryVaultAddress The new address of the `treasuryVault`. event TreasuryVaultSet(address indexed _newTreasuryVaultAddress); - /// @notice Emitted when the address of the `incentivesVault` is set. - /// @param _newIncentivesVaultAddress The new address of the `incentivesVault`. - event IncentivesVaultSet(address indexed _newIncentivesVaultAddress); - /// @notice Emitted when the `positionsManager` is set. /// @param _positionsManager The new address of the `positionsManager`. event PositionsManagerSet(address indexed _positionsManager); @@ -221,13 +217,6 @@ abstract contract MorphoGovernance is MorphoUtils { emit TreasuryVaultSet(_treasuryVault); } - /// @notice Sets the `incentivesVault`. - /// @param _incentivesVault The new `incentivesVault`. - function setIncentivesVault(IIncentivesVault _incentivesVault) external onlyOwner { - incentivesVault = _incentivesVault; - emit IncentivesVaultSet(address(_incentivesVault)); - } - /// @dev Sets `dustThreshold`. /// @param _dustThreshold The new `dustThreshold`. function setDustThreshold(uint256 _dustThreshold) external onlyOwner { diff --git a/src/compound/MorphoStorage.sol b/src/compound/MorphoStorage.sol index 066676113..b613a5765 100644 --- a/src/compound/MorphoStorage.sol +++ b/src/compound/MorphoStorage.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.13; import "./interfaces/compound/ICompound.sol"; import "./interfaces/IPositionsManager.sol"; -import "./interfaces/IIncentivesVault.sol"; import "./interfaces/IRewardsManager.sol"; import "./interfaces/IInterestRatesManager.sol"; @@ -53,7 +52,7 @@ abstract contract MorphoStorage is OwnableUpgradeable, ReentrancyGuardUpgradeabl /// CONTRACTS AND ADDRESSES /// IPositionsManager public positionsManager; - IIncentivesVault public incentivesVault; + address public incentivesVault; // Deprecated. IRewardsManager public rewardsManager; IInterestRatesManager public interestRatesManager; IComptroller public comptroller; diff --git a/src/compound/interfaces/IIncentivesVault.sol b/src/compound/interfaces/IIncentivesVault.sol deleted file mode 100644 index b29a8eaf8..000000000 --- a/src/compound/interfaces/IIncentivesVault.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity >=0.5.0; - -import "./IOracle.sol"; - -interface IIncentivesVault { - function isPaused() external view returns (bool); - - function bonus() external view returns (uint256); - - function MAX_BASIS_POINTS() external view returns (uint256); - - function incentivesTreasuryVault() external view returns (address); - - function oracle() external view returns (IOracle); - - function setOracle(IOracle _newOracle) external; - - function setIncentivesTreasuryVault(address _newIncentivesTreasuryVault) external; - - function setBonus(uint256 _newBonus) external; - - function setPauseStatus(bool _newStatus) external; - - function transferTokensToDao(address _token, uint256 _amount) external; - - function tradeCompForMorphoTokens(address _to, uint256 _amount) external; -} diff --git a/src/compound/interfaces/IMorpho.sol b/src/compound/interfaces/IMorpho.sol index 97ae8737b..2bdd371e3 100644 --- a/src/compound/interfaces/IMorpho.sol +++ b/src/compound/interfaces/IMorpho.sol @@ -4,7 +4,6 @@ pragma solidity >=0.5.0; import "./IInterestRatesManager.sol"; import "./IPositionsManager.sol"; import "./IRewardsManager.sol"; -import "./IIncentivesVault.sol"; import "../libraries/Types.sol"; @@ -35,7 +34,7 @@ interface IMorpho { function interestRatesManager() external view returns (IInterestRatesManager); function rewardsManager() external view returns (IRewardsManager); function positionsManager() external view returns (IPositionsManager); - function incentiveVault() external view returns (IIncentivesVault); + function incentiveVault() external view returns (address); function treasuryVault() external view returns (address); function cEth() external view returns (address); function wEth() external view returns (address); @@ -51,7 +50,6 @@ interface IMorpho { function setMaxSortedUsers(uint256 _newMaxSortedUsers) external; function setDefaultMaxGasForMatching(Types.MaxGasForMatching memory _maxGasForMatching) external; - function setIncentivesVault(address _newIncentivesVault) external; function setRewardsManager(address _rewardsManagerAddress) external; function setPositionsManager(IPositionsManager _positionsManager) external; function setInterestRatesManager(IInterestRatesManager _interestRatesManager) external; diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index 0be71e746..7ee2505b0 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -170,23 +170,6 @@ contract TestGovernance is TestSetup { assertEq(address(morpho.interestRatesManager()), address(interestRatesV2)); } - function testOnlyOwnerShouldSetIncentivesVault() public { - IIncentivesVault incentivesVaultV2 = new IncentivesVault( - comptroller, - IMorpho(address(morpho)), - morphoToken, - address(1), - dumbOracle - ); - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - morpho.setIncentivesVault(incentivesVaultV2); - - morpho.setIncentivesVault(incentivesVaultV2); - assertEq(address(morpho.incentivesVault()), address(incentivesVaultV2)); - } - function testOnlyOwnerShouldSetDustThreshold() public { hevm.prank(address(0)); hevm.expectRevert("Ownable: caller is not the owner"); diff --git a/test/compound/TestIncentivesVault.t.sol b/test/compound/TestIncentivesVault.t.sol deleted file mode 100644 index 534c6bb01..000000000 --- a/test/compound/TestIncentivesVault.t.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; - -import "./setup/TestSetup.sol"; -import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; - -contract TestIncentivesVault is TestSetup { - using SafeTransferLib for ERC20; - - function testShouldNotSetBonusAboveMaxBasisPoints() public { - uint256 moreThanMaxBasisPoints = PercentageMath.PERCENTAGE_FACTOR + 1; - hevm.expectRevert(abi.encodeWithSelector(IncentivesVault.ExceedsMaxBasisPoints.selector)); - incentivesVault.setBonus(moreThanMaxBasisPoints); - } - - function testOnlyOwnerShouldSetBonus() public { - uint256 bonusToSet = 1; - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setBonus(bonusToSet); - - incentivesVault.setBonus(bonusToSet); - assertEq(incentivesVault.bonus(), bonusToSet); - } - - function testOnlyOwnerShouldSetIncentivesTreasuryVault() public { - address incentivesTreasuryVault = address(1); - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setIncentivesTreasuryVault(incentivesTreasuryVault); - - incentivesVault.setIncentivesTreasuryVault(incentivesTreasuryVault); - assertEq(incentivesVault.incentivesTreasuryVault(), incentivesTreasuryVault); - } - - function testOnlyOwnerShouldSetOracle() public { - IOracle oracle = IOracle(address(1)); - - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setOracle(oracle); - - incentivesVault.setOracle(oracle); - assertEq(address(incentivesVault.oracle()), address(oracle)); - } - - function testOnlyOwnerShouldSetPauseStatus() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.setPauseStatus(true); - - incentivesVault.setPauseStatus(true); - assertTrue(incentivesVault.isPaused()); - - incentivesVault.setPauseStatus(false); - assertFalse(incentivesVault.isPaused()); - } - - function testOnlyOwnerShouldTransferTokensToDao() public { - hevm.prank(address(0)); - hevm.expectRevert("Ownable: caller is not the owner"); - incentivesVault.transferTokensToDao(address(morphoToken), 1); - - incentivesVault.transferTokensToDao(address(morphoToken), 1); - assertEq(ERC20(morphoToken).balanceOf(address(treasuryVault)), 1); - } - - function testFailWhenContractNotActive() public { - incentivesVault.setPauseStatus(true); - - hevm.prank(address(morpho)); - incentivesVault.tradeCompForMorphoTokens(address(1), 0); - } - - function testOnlyMorphoShouldTriggerCompConvertFunction() public { - incentivesVault.setIncentivesTreasuryVault(address(1)); - uint256 amount = 100; - deal(comp, address(morpho), amount); - - hevm.prank(address(morpho)); - ERC20(comp).safeApprove(address(incentivesVault), amount); - - hevm.expectRevert(abi.encodeWithSignature("OnlyMorpho()")); - incentivesVault.tradeCompForMorphoTokens(address(2), amount); - - hevm.prank(address(morpho)); - incentivesVault.tradeCompForMorphoTokens(address(2), amount); - } - - function testShouldGiveTheRightAmountOfRewards() public { - incentivesVault.setIncentivesTreasuryVault(address(1)); - uint256 toApprove = 1_000 ether; - deal(comp, address(morpho), toApprove); - - hevm.prank(address(morpho)); - ERC20(comp).safeApprove(address(incentivesVault), toApprove); - uint256 amount = 100; - - // O% bonus. - uint256 balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(address(morpho)); - incentivesVault.tradeCompForMorphoTokens(address(2), amount); - uint256 balanceAfter = ERC20(morphoToken).balanceOf(address(2)); - assertEq(balanceAfter - balanceBefore, 100); - - // 10% bonus. - incentivesVault.setBonus(1_000); - balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(address(morpho)); - incentivesVault.tradeCompForMorphoTokens(address(2), amount); - balanceAfter = ERC20(morphoToken).balanceOf(address(2)); - assertEq(balanceAfter - balanceBefore, 110); - } -} diff --git a/test/compound/TestRewards.t.sol b/test/compound/TestRewards.t.sol index c540a28c9..a8faca3e1 100644 --- a/test/compound/TestRewards.t.sol +++ b/test/compound/TestRewards.t.sol @@ -314,35 +314,6 @@ contract TestRewards is TestSetup { supplier3.borrow(cUsdc, toBorrow); } - function testShouldClaimRewardsAndTradeForMorpkoTokens() public { - // 10% bonus. - incentivesVault.setBonus(1_000); - - uint256 toSupply = 100 ether; - supplier1.approve(dai, toSupply); - supplier1.supply(cDai, toSupply); - - (, uint256 onPool) = morpho.supplyBalanceInOf(cDai, address(supplier1)); - uint256 userIndex = rewardsManager.compSupplierIndex(cDai, address(supplier1)); - uint256 rewardBalanceBefore = supplier1.balanceOf(comp); - - address[] memory cTokens = new address[](1); - cTokens[0] = cDai; - - hevm.roll(block.number + 1_000); - uint256 claimedAmount = supplier1.claimRewards(cTokens, true); - - uint256 index = comptroller.compSupplyState(cDai).index; - uint256 expectedClaimed = (onPool * (index - userIndex)) / 1e36; - uint256 expectedMorphoTokens = (expectedClaimed * 11_000) / 10_000; // 10% bonus with a dumb oracle 1:1 exchange from COMP to MORPHO. - - uint256 morphoBalance = supplier1.balanceOf(address(morphoToken)); - uint256 rewardBalanceAfter = supplier1.balanceOf(comp); - assertEq(claimedAmount, expectedClaimed); - testEquality(morphoBalance, expectedMorphoTokens); - testEquality(rewardBalanceBefore, rewardBalanceAfter); - } - function testShouldClaimTheSameAmountOfRewards() public { uint256 smallAmount = 1 ether; uint256 bigAmount = 10_000 ether; diff --git a/test/compound/helpers/IncentivesVault.sol b/test/compound/helpers/IncentivesVault.sol deleted file mode 100644 index ca3b51a04..000000000 --- a/test/compound/helpers/IncentivesVault.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GNU AGPLv3 -pragma solidity ^0.8.0; - -import "./DumbOracle.sol"; - -import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; - -contract IncentivesVault { - using SafeTransferLib for ERC20; - - address public immutable morphoToken; - address public immutable morpho; - address public immutable oracle; - address public constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - - uint256 public constant MAX_BASIS_POINTS = 10_000; - uint256 public constant BONUS = 1_000; - - constructor( - address _morpho, - address _morphoToken, - address _oracle - ) { - morpho = _morpho; - morphoToken = _morphoToken; - oracle = _oracle; - } - - function convertCompToMorphoTokens(address _to, uint256 _amount) external { - require(msg.sender == morpho, "!morpho"); - ERC20(COMP).safeTransferFrom(msg.sender, address(this), _amount); - uint256 amountOut = (IOracle(oracle).consult(_amount) * (MAX_BASIS_POINTS + BONUS)) / - MAX_BASIS_POINTS; - ERC20(morphoToken).transfer(_to, amountOut); - } -} diff --git a/test/compound/setup/TestSetup.sol b/test/compound/setup/TestSetup.sol index caf60c14d..841a5ec55 100644 --- a/test/compound/setup/TestSetup.sol +++ b/test/compound/setup/TestSetup.sol @@ -7,7 +7,6 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; -import {IncentivesVault} from "src/compound/IncentivesVault.sol"; import {PositionsManager} from "src/compound/PositionsManager.sol"; import {InterestRatesManager} from "src/compound/InterestRatesManager.sol"; import "../../common/helpers/MorphoToken.sol"; @@ -96,15 +95,6 @@ contract TestSetup is Config, Utils { morphoToken = new MorphoToken(address(this)); dumbOracle = new DumbOracle(); - incentivesVault = new IncentivesVault( - comptroller, - IMorpho(address(morpho)), - morphoToken, - address(treasuryVault), - dumbOracle - ); - morphoToken.transfer(address(incentivesVault), 1_000_000 ether); - morpho.setIncentivesVault(incentivesVault); rewardsManagerImplV1 = new RewardsManager(); rewardsManagerProxy = new TransparentUpgradeableProxy( @@ -182,7 +172,6 @@ contract TestSetup is Config, Utils { hevm.label(address(comptroller), "Comptroller"); hevm.label(address(oracle), "CompoundOracle"); hevm.label(address(dumbOracle), "DumbOracle"); - hevm.label(address(incentivesVault), "IncentivesVault"); hevm.label(address(treasuryVault), "TreasuryVault"); hevm.label(address(lens), "Lens"); } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index afa64e896..cc2e1d388 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -61,7 +61,6 @@ contract TestSetup is Config, Test { lens = Lens(address(lensProxy)); morpho = Morpho(payable(morphoProxy)); rewardsManager = RewardsManager(address(morpho.rewardsManager())); - incentivesVault = morpho.incentivesVault(); positionsManager = morpho.positionsManager(); interestRatesManager = morpho.interestRatesManager(); @@ -100,7 +99,6 @@ contract TestSetup is Config, Test { vm.label(address(rewardsManager), "RewardsManager"); vm.label(address(comptroller), "Comptroller"); vm.label(address(oracle), "Oracle"); - vm.label(address(incentivesVault), "IncentivesVault"); vm.label(address(lens), "Lens"); vm.label(address(aave), "AAVE"); From 3429e147ae9966dc4b4b27e6f019dd6effdefee1 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 30 Dec 2022 14:53:46 +0100 Subject: [PATCH 051/105] =?UTF-8?q?=F0=9F=93=B8=20Update=20compound=20stor?= =?UTF-8?q?age=20snapshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snapshots/.storage-layout-compound | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshots/.storage-layout-compound b/snapshots/.storage-layout-compound index b0351c45f..6b720f448 100644 --- a/snapshots/.storage-layout-compound +++ b/snapshots/.storage-layout-compound @@ -32,7 +32,7 @@ | marketStatus | mapping(address => struct Types.MarketStatus) | 168 | 0 | 32 | src/compound/Morpho.sol:Morpho | | deltas | mapping(address => struct Types.Delta) | 169 | 0 | 32 | src/compound/Morpho.sol:Morpho | | positionsManager | contract IPositionsManager | 170 | 0 | 20 | src/compound/Morpho.sol:Morpho | -| incentivesVault | contract IIncentivesVault | 171 | 0 | 20 | src/compound/Morpho.sol:Morpho | +| incentivesVault | address | 171 | 0 | 20 | src/compound/Morpho.sol:Morpho | | rewardsManager | contract IRewardsManager | 172 | 0 | 20 | src/compound/Morpho.sol:Morpho | | interestRatesManager | contract IInterestRatesManager | 173 | 0 | 20 | src/compound/Morpho.sol:Morpho | | comptroller | contract IComptroller | 174 | 0 | 20 | src/compound/Morpho.sol:Morpho | From 0ebfa00755aeace2fece992b1b3ebb975359e265 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 2 Jan 2023 11:14:21 +0100 Subject: [PATCH 052/105] Updated lib/morpho-data-structures --- lib/morpho-data-structures | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/morpho-data-structures b/lib/morpho-data-structures index aefcf40ec..7e43ca750 160000 --- a/lib/morpho-data-structures +++ b/lib/morpho-data-structures @@ -1 +1 @@ -Subproject commit aefcf40ec879dfeb8275ab51c528676d9dbfa705 +Subproject commit 7e43ca750054d89f4ce365ca07e2a4ace1340ad1 From 7c9de736275fd1dad87298c1117502bb2fc7f7ac Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 2 Jan 2023 14:52:06 +0100 Subject: [PATCH 053/105] Fix production test --- test/prod/compound/TestDeltas.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prod/compound/TestDeltas.t.sol b/test/prod/compound/TestDeltas.t.sol index 661f386e4..bb190bafe 100644 --- a/test/prod/compound/TestDeltas.t.sol +++ b/test/prod/compound/TestDeltas.t.sol @@ -104,7 +104,7 @@ contract TestDeltas is TestSetup { assertApproxEqAbs( test.avgBorrowRatePerBlock, ICToken(test.market.poolToken).borrowRatePerBlock(), - 1, + 2, "avg borrow rate per year" ); From e85b08a8abd57ad16929893c4fd23da944e25b36 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 2 Jan 2023 11:53:23 -0500 Subject: [PATCH 054/105] refactor: remove supply and borrow guard --- src/aave-v2/EntryPositionsManager.sol | 12 ++---------- .../libraries/aave/ReserveConfiguration.sol | 6 +++++- test/aave-v2/TestBorrow.t.sol | 16 ---------------- test/aave-v2/TestSupply.t.sol | 16 ---------------- 4 files changed, 7 insertions(+), 43 deletions(-) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index 3adf793d5..f94b80faa 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -63,9 +63,6 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils /// @notice Thrown when someone tries to borrow but the borrow is paused. error BorrowIsPaused(); - /// @notice Thrown when underlying pool is frozen. - error FrozenOnPool(); - /// STRUCTS /// // Struct to avoid stack too deep. @@ -97,7 +94,6 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (!market.isCreated) revert MarketNotCreated(); if (marketPauseStatus[_poolToken].isSupplyPaused) revert SupplyIsPaused(); - if (pool.getConfiguration(address(underlyingToken)).getFrozen()) revert FrozenOnPool(); _updateIndexes(_poolToken); _setSupplying(_onBehalf, borrowMask[_poolToken], true); @@ -194,12 +190,8 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowIsPaused(); ERC20 underlyingToken = ERC20(market.underlyingToken); - DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration( - address(underlyingToken) - ); - - if (!reserveConfig.getBorrowingEnabled()) revert BorrowingNotEnabled(); - if (reserveConfig.getFrozen()) revert FrozenOnPool(); + if (!pool.getConfiguration(address(underlyingToken)).getBorrowingEnabled()) + revert BorrowingNotEnabled(); _updateIndexes(_poolToken); _setBorrowing(msg.sender, borrowMask[_poolToken], true); diff --git a/src/aave-v2/libraries/aave/ReserveConfiguration.sol b/src/aave-v2/libraries/aave/ReserveConfiguration.sol index 24397a349..063a8740b 100644 --- a/src/aave-v2/libraries/aave/ReserveConfiguration.sol +++ b/src/aave-v2/libraries/aave/ReserveConfiguration.sol @@ -182,7 +182,11 @@ library ReserveConfiguration { * @param self The reserve configuration * @return The frozen state **/ - function getFrozen(DataTypes.ReserveConfigurationMap memory self) internal pure returns (bool) { + function getFrozen(DataTypes.ReserveConfigurationMap storage self) + internal + view + returns (bool) + { return (self.data & ~FROZEN_MASK) != 0; } diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index f97c041e8..fc56b15f3 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -247,22 +247,6 @@ contract TestBorrow is TestSetup { borrower1.borrow(aUsdc, amount); } - function testCannotBorrowOnFrozenPool() public { - uint256 amount = 10_000 ether; - - DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration(dai); - reserveConfig.setFrozen(true); - - borrower1.approve(dai, amount); - borrower1.supply(aDai, amount); - - vm.prank(address(lendingPoolConfigurator)); - pool.setConfiguration(dai, reserveConfig.data); - - hevm.expectRevert(EntryPositionsManager.FrozenOnPool.selector); - borrower1.borrow(aDai, amount); - } - function testBorrowLargerThanDeltaShouldClearDelta() public { // Allows only 10 unmatch suppliers. diff --git a/test/aave-v2/TestSupply.t.sol b/test/aave-v2/TestSupply.t.sol index 3977d8a1c..7d60d22d8 100644 --- a/test/aave-v2/TestSupply.t.sol +++ b/test/aave-v2/TestSupply.t.sol @@ -265,22 +265,6 @@ contract TestSupply is TestSetup { supplier1.supply(aDai, amount); } - function testCannotSupplyOnFrozenPool() public { - uint256 amount = 10_000 ether; - - DataTypes.ReserveConfigurationMap memory reserveConfig = pool.getConfiguration(dai); - reserveConfig.setFrozen(true); - - vm.prank(address(lendingPoolConfigurator)); - pool.setConfiguration(dai, reserveConfig.data); - - supplier1.approve(dai, amount); - - hevm.expectRevert(EntryPositionsManager.FrozenOnPool.selector); - - supplier1.supply(aDai, amount); - } - function testShouldMatchSupplyWithCorrectAmountOfGas() public { uint256 amount = 100 ether; createSigners(30); From 4933dd3bcc10a9798572ffd0c9a11eadf35c8a31 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 31 Dec 2022 16:17:53 +0100 Subject: [PATCH 055/105] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compound/interfaces/IMorpho.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compound/interfaces/IMorpho.sol b/src/compound/interfaces/IMorpho.sol index 8b5df0b98..d9240d5db 100644 --- a/src/compound/interfaces/IMorpho.sol +++ b/src/compound/interfaces/IMorpho.sol @@ -34,7 +34,7 @@ interface IMorpho { function interestRatesManager() external view returns (IInterestRatesManager); function rewardsManager() external view returns (IRewardsManager); function positionsManager() external view returns (IPositionsManager); - function incentiveVault() external view returns (address); + function incentivesVault() external view returns (address); function treasuryVault() external view returns (address); function cEth() external view returns (address); function wEth() external view returns (address); From 3b25c86be6210b6068531591aa1cf7b96cf14ab0 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 31 Dec 2022 16:22:12 +0100 Subject: [PATCH 056/105] =?UTF-8?q?=F0=9F=93=9D=20Update=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compound/Morpho.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compound/Morpho.sol b/src/compound/Morpho.sol index 8be09582b..a994c0fe8 100644 --- a/src/compound/Morpho.sol +++ b/src/compound/Morpho.sol @@ -145,6 +145,7 @@ contract Morpho is MorphoGovernance { } /// @notice Claims rewards for the given assets. + /// @dev The incentives vault will never be implemented. Thus the second parameter of this function becomes useless. /// @param _cTokenAddresses The cToken addresses to claim rewards from. /// @return amountOfRewards The amount of rewards claimed (in COMP). function claimRewards(address[] calldata _cTokenAddresses, bool) From 7d3205fc7e05d7bb1a16767538a4f3cc72e9a3c4 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 3 Jan 2023 11:53:11 -0500 Subject: [PATCH 057/105] fix: remove getBorrowingEnabled check --- src/aave-v2/EntryPositionsManager.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index f94b80faa..d379a33eb 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -190,8 +190,6 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowIsPaused(); ERC20 underlyingToken = ERC20(market.underlyingToken); - if (!pool.getConfiguration(address(underlyingToken)).getBorrowingEnabled()) - revert BorrowingNotEnabled(); _updateIndexes(_poolToken); _setBorrowing(msg.sender, borrowMask[_poolToken], true); From d4fe1055af38f68c6d2921a17b97b21864b864aa Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 3 Jan 2023 11:57:30 -0500 Subject: [PATCH 058/105] fix: remove defunct test --- test/aave-v2/TestBorrow.t.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index fc56b15f3..82a0a7d97 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -210,16 +210,6 @@ contract TestBorrow is TestSetup { morpho.borrow(aDai, 0, type(uint256).max); } - function testShouldNotBorrowAssetNotBorrowable() public { - uint256 amount = 100 ether; - - borrower1.approve(dai, type(uint256).max); - borrower1.supply(aDai, amount); - - hevm.expectRevert(EntryPositionsManager.BorrowingNotEnabled.selector); - borrower1.borrow(aAave, amount / 2); - } - function testShouldNotAllowSmallBorrow() public { (uint256 ltv, , , , ) = pool.getConfiguration(dai).getParamsMemory(); From 6986f205aa4fea7fde7c1f53adbee50a30b1ea3f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 4 Jan 2023 13:47:33 +0100 Subject: [PATCH 059/105] Only match delta if p2p enabled --- src/aave-v2/lens/RatesLens.sol | 100 +++++++++++++++++--------------- src/compound/lens/RatesLens.sol | 92 +++++++++++++++-------------- 2 files changed, 100 insertions(+), 92 deletions(-) diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index f3e956680..08fb41566 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -46,33 +46,35 @@ abstract contract RatesLens is UsersLens { ) = _getIndexes(_poolToken); Types.SupplyBalance memory supplyBalance = morpho.supplyBalanceInOf(_poolToken, _user); - if (_amount > 0 && delta.p2pBorrowDelta > 0) { - uint256 matchedDelta = Math.min( - delta.p2pBorrowDelta.rayMul(indexes.poolBorrowIndex), - _amount - ); - - supplyBalance.inP2P += matchedDelta.rayDiv(indexes.p2pSupplyIndex); - _amount -= matchedDelta; - } - - if (_amount > 0 && !market.isP2PDisabled) { - address firstPoolBorrower = morpho.getHead( - _poolToken, - Types.PositionType.BORROWERS_ON_POOL - ); - uint256 firstPoolBorrowerBalance = morpho - .borrowBalanceInOf(_poolToken, firstPoolBorrower) - .onPool; - - if (firstPoolBorrowerBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolBorrowerBalance.rayMul(indexes.poolBorrowIndex), + if (!market.isP2PDisabled) { + if (_amount > 0 && delta.p2pBorrowDelta > 0) { + uint256 matchedDelta = Math.min( + delta.p2pBorrowDelta.rayMul(indexes.poolBorrowIndex), _amount ); - supplyBalance.inP2P += matchedP2P.rayDiv(indexes.p2pSupplyIndex); - _amount -= matchedP2P; + supplyBalance.inP2P += matchedDelta.rayDiv(indexes.p2pSupplyIndex); + _amount -= matchedDelta; + } + + if (_amount > 0 && !market.isP2PDisabled) { + address firstPoolBorrower = morpho.getHead( + _poolToken, + Types.PositionType.BORROWERS_ON_POOL + ); + uint256 firstPoolBorrowerBalance = morpho + .borrowBalanceInOf(_poolToken, firstPoolBorrower) + .onPool; + + if (firstPoolBorrowerBalance > 0) { + uint256 matchedP2P = Math.min( + firstPoolBorrowerBalance.rayMul(indexes.poolBorrowIndex), + _amount + ); + + supplyBalance.inP2P += matchedP2P.rayDiv(indexes.p2pSupplyIndex); + _amount -= matchedP2P; + } } } @@ -119,33 +121,35 @@ abstract contract RatesLens is UsersLens { ) = _getIndexes(_poolToken); Types.BorrowBalance memory borrowBalance = morpho.borrowBalanceInOf(_poolToken, _user); - if (_amount > 0 && delta.p2pSupplyDelta > 0) { - uint256 matchedDelta = Math.min( - delta.p2pSupplyDelta.rayMul(indexes.poolSupplyIndex), - _amount - ); - - borrowBalance.inP2P += matchedDelta.rayDiv(indexes.p2pBorrowIndex); - _amount -= matchedDelta; - } - - if (_amount > 0 && !market.isP2PDisabled) { - address firstPoolSupplier = morpho.getHead( - _poolToken, - Types.PositionType.SUPPLIERS_ON_POOL - ); - uint256 firstPoolSupplierBalance = morpho - .supplyBalanceInOf(_poolToken, firstPoolSupplier) - .onPool; - - if (firstPoolSupplierBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), + if (!market.isP2PDisabled) { + if (_amount > 0 && delta.p2pSupplyDelta > 0) { + uint256 matchedDelta = Math.min( + delta.p2pSupplyDelta.rayMul(indexes.poolSupplyIndex), _amount ); - borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); - _amount -= matchedP2P; + borrowBalance.inP2P += matchedDelta.rayDiv(indexes.p2pBorrowIndex); + _amount -= matchedDelta; + } + + if (_amount > 0) { + address firstPoolSupplier = morpho.getHead( + _poolToken, + Types.PositionType.SUPPLIERS_ON_POOL + ); + uint256 firstPoolSupplierBalance = morpho + .supplyBalanceInOf(_poolToken, firstPoolSupplier) + .onPool; + + if (firstPoolSupplierBalance > 0) { + uint256 matchedP2P = Math.min( + firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), + _amount + ); + + borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); + _amount -= matchedP2P; + } } } diff --git a/src/compound/lens/RatesLens.sol b/src/compound/lens/RatesLens.sol index d04dc8530..50a7cc806 100644 --- a/src/compound/lens/RatesLens.sol +++ b/src/compound/lens/RatesLens.sol @@ -40,31 +40,33 @@ abstract contract RatesLens is UsersLens { (Types.Delta memory delta, Types.Indexes memory indexes) = _getIndexes(_poolToken, true); Types.SupplyBalance memory supplyBalance = morpho.supplyBalanceInOf(_poolToken, _user); - if (_amount > 0 && delta.p2pBorrowDelta > 0) { - uint256 matchedDelta = Math.min( - delta.p2pBorrowDelta.mul(indexes.poolBorrowIndex), - _amount - ); - - supplyBalance.inP2P += matchedDelta.div(indexes.p2pSupplyIndex); - _amount -= matchedDelta; - } - - if (_amount > 0 && !morpho.p2pDisabled(_poolToken)) { - uint256 firstPoolBorrowerBalance = morpho - .borrowBalanceInOf( - _poolToken, - morpho.getHead(_poolToken, Types.PositionType.BORROWERS_ON_POOL) - ).onPool; - - if (firstPoolBorrowerBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolBorrowerBalance.mul(indexes.poolBorrowIndex), + if (!morpho.p2pDisabled(_poolToken)) { + if (_amount > 0 && delta.p2pBorrowDelta > 0) { + uint256 matchedDelta = Math.min( + delta.p2pBorrowDelta.mul(indexes.poolBorrowIndex), _amount ); - supplyBalance.inP2P += matchedP2P.div(indexes.p2pSupplyIndex); - _amount -= matchedP2P; + supplyBalance.inP2P += matchedDelta.div(indexes.p2pSupplyIndex); + _amount -= matchedDelta; + } + + if (_amount > 0) { + uint256 firstPoolBorrowerBalance = morpho + .borrowBalanceInOf( + _poolToken, + morpho.getHead(_poolToken, Types.PositionType.BORROWERS_ON_POOL) + ).onPool; + + if (firstPoolBorrowerBalance > 0) { + uint256 matchedP2P = Math.min( + firstPoolBorrowerBalance.mul(indexes.poolBorrowIndex), + _amount + ); + + supplyBalance.inP2P += matchedP2P.div(indexes.p2pSupplyIndex); + _amount -= matchedP2P; + } } } @@ -107,31 +109,33 @@ abstract contract RatesLens is UsersLens { (Types.Delta memory delta, Types.Indexes memory indexes) = _getIndexes(_poolToken, true); Types.BorrowBalance memory borrowBalance = morpho.borrowBalanceInOf(_poolToken, _user); - if (_amount > 0 && delta.p2pSupplyDelta > 0) { - uint256 matchedDelta = Math.min( - delta.p2pSupplyDelta.mul(indexes.poolSupplyIndex), - _amount - ); - - borrowBalance.inP2P += matchedDelta.div(indexes.p2pBorrowIndex); - _amount -= matchedDelta; - } - - if (_amount > 0 && !morpho.p2pDisabled(_poolToken)) { - uint256 firstPoolSupplierBalance = morpho - .supplyBalanceInOf( - _poolToken, - morpho.getHead(_poolToken, Types.PositionType.SUPPLIERS_ON_POOL) - ).onPool; - - if (firstPoolSupplierBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolSupplierBalance.mul(indexes.poolSupplyIndex), + if (!morpho.p2pDisabled(_poolToken)) { + if (_amount > 0 && delta.p2pSupplyDelta > 0) { + uint256 matchedDelta = Math.min( + delta.p2pSupplyDelta.mul(indexes.poolSupplyIndex), _amount ); - borrowBalance.inP2P += matchedP2P.div(indexes.p2pBorrowIndex); - _amount -= matchedP2P; + borrowBalance.inP2P += matchedDelta.div(indexes.p2pBorrowIndex); + _amount -= matchedDelta; + } + + if (_amount > 0) { + uint256 firstPoolSupplierBalance = morpho + .supplyBalanceInOf( + _poolToken, + morpho.getHead(_poolToken, Types.PositionType.SUPPLIERS_ON_POOL) + ).onPool; + + if (firstPoolSupplierBalance > 0) { + uint256 matchedP2P = Math.min( + firstPoolSupplierBalance.mul(indexes.poolSupplyIndex), + _amount + ); + + borrowBalance.inP2P += matchedP2P.div(indexes.p2pBorrowIndex); + _amount -= matchedP2P; + } } } From 36a47840e1b9e17378529c2d516407f33bb1fa2a Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 4 Jan 2023 15:37:20 -0500 Subject: [PATCH 060/105] feat: re-add borrowing enabled check --- src/aave-v2/EntryPositionsManager.sol | 2 ++ test/aave-v2/TestBorrow.t.sol | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/aave-v2/EntryPositionsManager.sol b/src/aave-v2/EntryPositionsManager.sol index d379a33eb..f94b80faa 100644 --- a/src/aave-v2/EntryPositionsManager.sol +++ b/src/aave-v2/EntryPositionsManager.sol @@ -190,6 +190,8 @@ contract EntryPositionsManager is IEntryPositionsManager, PositionsManagerUtils if (marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowIsPaused(); ERC20 underlyingToken = ERC20(market.underlyingToken); + if (!pool.getConfiguration(address(underlyingToken)).getBorrowingEnabled()) + revert BorrowingNotEnabled(); _updateIndexes(_poolToken); _setBorrowing(msg.sender, borrowMask[_poolToken], true); diff --git a/test/aave-v2/TestBorrow.t.sol b/test/aave-v2/TestBorrow.t.sol index 82a0a7d97..fc56b15f3 100644 --- a/test/aave-v2/TestBorrow.t.sol +++ b/test/aave-v2/TestBorrow.t.sol @@ -210,6 +210,16 @@ contract TestBorrow is TestSetup { morpho.borrow(aDai, 0, type(uint256).max); } + function testShouldNotBorrowAssetNotBorrowable() public { + uint256 amount = 100 ether; + + borrower1.approve(dai, type(uint256).max); + borrower1.supply(aDai, amount); + + hevm.expectRevert(EntryPositionsManager.BorrowingNotEnabled.selector); + borrower1.borrow(aAave, amount / 2); + } + function testShouldNotAllowSmallBorrow() public { (uint256 ltv, , , , ) = pool.getConfiguration(dai).getParamsMemory(); From 700c900f7357df2f281ff20e14fa13601b6a2f74 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 9 Jan 2023 15:07:14 +0100 Subject: [PATCH 061/105] Consider impact on pool rates for future rates --- .../interfaces/aave/IProtocolDataProvider.sol | 73 ++++++++ .../aave/IReserveInterestRateStrategy.sol | 24 +++ src/aave-v2/lens/LensStorage.sol | 9 +- src/aave-v2/lens/RatesLens.sol | 156 ++++++++++++++++-- 4 files changed, 242 insertions(+), 20 deletions(-) create mode 100644 src/aave-v2/interfaces/aave/IProtocolDataProvider.sol create mode 100644 src/aave-v2/interfaces/aave/IReserveInterestRateStrategy.sol diff --git a/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol b/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol new file mode 100644 index 000000000..69a0a1c83 --- /dev/null +++ b/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.5.0; + +import {ILendingPoolAddressesProvider} from "./ILendingPoolAddressesProvider.sol"; + +interface IProtocolDataProvider { + struct TokenData { + string symbol; + address tokenAddress; + } + + function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); + + function getAllReservesTokens() external view returns (TokenData[] memory); + + function getAllATokens() external view returns (TokenData[] memory); + + function getReserveConfigurationData(address asset) + external + view + returns ( + uint256 decimals, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 reserveFactor, + bool usageAsCollateralEnabled, + bool borrowingEnabled, + bool stableBorrowRateEnabled, + bool isActive, + bool isFrozen + ); + + function getReserveData(address asset) + external + view + returns ( + uint256 availableLiquidity, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 liquidityRate, + uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex, + uint40 lastUpdateTimestamp + ); + + function getUserReserveData(address asset, address user) + external + view + returns ( + uint256 currentATokenBalance, + uint256 currentStableDebt, + uint256 currentVariableDebt, + uint256 principalStableDebt, + uint256 scaledVariableDebt, + uint256 stableBorrowRate, + uint256 liquidityRate, + uint40 stableRateLastUpdated, + bool usageAsCollateralEnabled + ); + + function getReserveTokensAddresses(address asset) + external + view + returns ( + address aTokenAddress, + address stableDebtTokenAddress, + address variableDebtTokenAddress + ); +} diff --git a/src/aave-v2/interfaces/aave/IReserveInterestRateStrategy.sol b/src/aave-v2/interfaces/aave/IReserveInterestRateStrategy.sol new file mode 100644 index 000000000..4ce1e7a72 --- /dev/null +++ b/src/aave-v2/interfaces/aave/IReserveInterestRateStrategy.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.5.0; + +interface IReserveInterestRateStrategy { + function baseVariableBorrowRate() external view returns (uint256); + + function getMaxVariableBorrowRate() external view returns (uint256); + + function calculateInterestRates( + address reserve, + uint256 utilizationRate, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 averageStableBorrowRate, + uint256 reserveFactor + ) + external + view + returns ( + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate + ); +} diff --git a/src/aave-v2/lens/LensStorage.sol b/src/aave-v2/lens/LensStorage.sol index c9e627bee..f74145bd2 100644 --- a/src/aave-v2/lens/LensStorage.sol +++ b/src/aave-v2/lens/LensStorage.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.13; +import "../interfaces/aave/IProtocolDataProvider.sol"; import "../interfaces/aave/IPriceOracleGetter.sol"; import "../interfaces/aave/ILendingPool.sol"; import "../interfaces/aave/IAToken.sol"; import "../interfaces/IMorpho.sol"; import "./interfaces/ILens.sol"; +import "../libraries/aave/DataTypes.sol"; +import "../libraries/InterestRatesModel.sol"; import "../libraries/aave/ReserveConfiguration.sol"; import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; import "@morpho-dao/morpho-utils/math/WadRayMath.sol"; import "@morpho-dao/morpho-utils/math/Math.sol"; -import "../libraries/aave/DataTypes.sol"; -import "../libraries/InterestRatesModel.sol"; /// @title LensStorage. /// @author Morpho Labs. @@ -21,6 +22,8 @@ import "../libraries/InterestRatesModel.sol"; abstract contract LensStorage is ILens { /// CONSTANTS /// + bytes32 internal constant DATA_PROVIDER_ID = + 0x0100000000000000000000000000000000000000000000000000000000000000; uint16 public constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 50_00; // 50% in basis points. uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; // Health factor below which the positions can be liquidated. @@ -34,6 +37,7 @@ abstract contract LensStorage is ILens { IMorpho public immutable morpho; ILendingPoolAddressesProvider public immutable addressesProvider; + IProtocolDataProvider internal immutable dataProvider; ILendingPool public immutable pool; /// CONSTRUCTOR /// @@ -44,6 +48,7 @@ abstract contract LensStorage is ILens { morpho = IMorpho(_morpho); pool = ILendingPool(morpho.pool()); addressesProvider = ILendingPoolAddressesProvider(morpho.addressesProvider()); + dataProvider = IProtocolDataProvider(addressesProvider.getAddress(DATA_PROVIDER_ID)); ST_ETH_BASE_REBASE_INDEX = morpho.ST_ETH_BASE_REBASE_INDEX(); } } diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index 505d4c290..b31eba212 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.13; import "../interfaces/aave/IVariableDebtToken.sol"; +import "../interfaces/aave/IReserveInterestRateStrategy.sol"; import "./UsersLens.sol"; @@ -10,6 +11,7 @@ import "./UsersLens.sol"; /// @custom:contact security@morpho.xyz /// @notice Intermediary layer exposing endpoints to query live data related to the Morpho Protocol users and their positions. abstract contract RatesLens is UsersLens { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using WadRayMath for uint256; using Math for uint256; @@ -46,6 +48,8 @@ abstract contract RatesLens is UsersLens { ) = _getIndexes(_poolToken); Types.SupplyBalance memory supplyBalance = morpho.supplyBalanceInOf(_poolToken, _user); + + uint256 repaidToPool; if (!market.isP2PDisabled) { if (_amount > 0 && delta.p2pBorrowDelta > 0) { uint256 matchedDelta = Math.min( @@ -73,6 +77,7 @@ abstract contract RatesLens is UsersLens { ); supplyBalance.inP2P += matchedP2P.rayDiv(indexes.p2pSupplyIndex); + repaidToPool += matchedP2P; _amount -= matchedP2P; } } @@ -86,7 +91,9 @@ abstract contract RatesLens is UsersLens { (nextSupplyRatePerYear, totalBalance) = _getUserSupplyRatePerYear( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + _amount, + repaidToPool ); } @@ -121,8 +128,10 @@ abstract contract RatesLens is UsersLens { ) = _getIndexes(_poolToken); Types.BorrowBalance memory borrowBalance = morpho.borrowBalanceInOf(_poolToken, _user); + + uint256 withdrawnFromPool; if (!market.isP2PDisabled) { - if (_amount > 0 && delta.p2pSupplyDelta > 0) { + if (delta.p2pSupplyDelta > 0) { uint256 matchedDelta = Math.min( delta.p2pSupplyDelta.rayMul(indexes.poolSupplyIndex), _amount @@ -142,13 +151,13 @@ abstract contract RatesLens is UsersLens { .onPool; if (firstPoolSupplierBalance > 0) { - uint256 matchedP2P = Math.min( + withdrawnFromPool = Math.min( firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), _amount ); - borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); - _amount -= matchedP2P; + borrowBalance.inP2P += withdrawnFromPool.rayDiv(indexes.p2pBorrowIndex); + _amount -= withdrawnFromPool; } } } @@ -161,7 +170,9 @@ abstract contract RatesLens is UsersLens { (nextBorrowRatePerYear, totalBalance) = _getUserBorrowRatePerYear( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + _amount, + withdrawnFromPool ); } @@ -179,7 +190,13 @@ abstract contract RatesLens is UsersLens { _user ); - (supplyRatePerYear, ) = _getUserSupplyRatePerYear(_poolToken, balanceInP2P, balanceOnPool); + (supplyRatePerYear, ) = _getUserSupplyRatePerYear( + _poolToken, + balanceInP2P, + balanceOnPool, + 0, + 0 + ); } /// @notice Returns the borrow rate per year a given user is currently experiencing on a given market. @@ -196,7 +213,13 @@ abstract contract RatesLens is UsersLens { _user ); - (borrowRatePerYear, ) = _getUserBorrowRatePerYear(_poolToken, balanceInP2P, balanceOnPool); + (borrowRatePerYear, ) = _getUserBorrowRatePerYear( + _poolToken, + balanceInP2P, + balanceOnPool, + 0, + 0 + ); } /// PUBLIC /// @@ -313,6 +336,45 @@ abstract contract RatesLens is UsersLens { /// @return poolSupplyRate The market's pool supply rate per year (in ray). /// @return poolBorrowRate The market's pool borrow rate per year (in ray). function getRatesPerYear(address _poolToken) + public + view + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return _getRatesPerYear(_poolToken, 0, 0, 0, 0); + } + + /// INTERNAL /// + + struct PoolRatesVars { + uint256 availableLiquidity; + uint256 totalStableDebt; + uint256 totalVariableDebt; + uint256 avgStableRate; + uint256 reserveFactor; + } + + /// @notice Computes and returns peer-to-peer and pool rates for a specific market. + /// @param _poolToken The market address. + /// @param _suppliedOnPool The amount hypothetically supplied to the underlying's pool. + /// @param _borrowedFromPool The amount hypothetically borrowed from the underlying's pool. + /// @param _repaidOnPool The amount hypothetically repaid to the underlying's pool. + /// @param _withdrawnFromPool The amount hypothetically withdrawn from the underlying's pool. + /// @return p2pSupplyRate The market's peer-to-peer supply rate per year (in ray). + /// @return p2pBorrowRate The market's peer-to-peer borrow rate per year (in ray). + /// @return poolSupplyRate The market's pool supply rate per year (in ray). + /// @return poolBorrowRate The market's pool borrow rate per year (in ray). + function _getRatesPerYear( + address _poolToken, + uint256 _suppliedOnPool, + uint256 _borrowedFromPool, + uint256 _repaidOnPool, + uint256 _withdrawnFromPool + ) public view returns ( @@ -328,9 +390,13 @@ abstract contract RatesLens is UsersLens { Types.Indexes memory indexes ) = _getIndexes(_poolToken); - DataTypes.ReserveData memory reserve = pool.getReserveData(market.underlyingToken); - poolSupplyRate = reserve.currentLiquidityRate; - poolBorrowRate = reserve.currentVariableBorrowRate; + (poolSupplyRate, poolBorrowRate) = _getPoolRatesPerYear( + market.underlyingToken, + _suppliedOnPool, + _borrowedFromPool, + _repaidOnPool, + _withdrawnFromPool + ); p2pSupplyRate = InterestRatesModel.computeP2PSupplyRatePerYear( InterestRatesModel.P2PRateComputeParams({ @@ -359,7 +425,49 @@ abstract contract RatesLens is UsersLens { ); } - /// INTERNAL /// + /// @notice Computes and returns the underlying pool rates for a specific market. + /// @param _underlying The underlying pool market address. + /// @param _supplied The amount hypothetically supplied. + /// @param _borrowed The amount hypothetically borrowed. + /// @param _repaid The amount hypothetically repaid. + /// @param _withdrawn The amount hypothetically withdrawn. + /// @return poolSupplyRate The market's pool supply rate per year (in ray). + /// @return poolBorrowRate The market's pool borrow rate per year (in ray). + function _getPoolRatesPerYear( + address _underlying, + uint256 _supplied, + uint256 _borrowed, + uint256 _repaid, + uint256 _withdrawn + ) internal view returns (uint256 poolSupplyRate, uint256 poolBorrowRate) { + DataTypes.ReserveData memory reserve = pool.getReserveData(_underlying); + + PoolRatesVars memory vars; + ( + vars.availableLiquidity, + vars.totalStableDebt, + vars.totalVariableDebt, + , + , + , + vars.avgStableRate, + , + , + + ) = dataProvider.getReserveData(_underlying); + (, , , , vars.reserveFactor) = reserve.configuration.getParamsMemory(); + + (poolSupplyRate, , poolBorrowRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + _underlying, + vars.availableLiquidity + _supplied + _repaid - _borrowed - _withdrawn, + vars.totalStableDebt, + vars.totalVariableDebt + _borrowed - _repaid, + vars.avgStableRate, + vars.reserveFactor + ); + } /// @notice Computes and returns the total distribution of supply for a given market, using virtually updated indexes. /// @param _poolToken The address of the market to check. @@ -410,10 +518,16 @@ abstract contract RatesLens is UsersLens { function _getUserSupplyRatePerYear( address _poolToken, uint256 _balanceInP2P, - uint256 _balanceOnPool + uint256 _balanceOnPool, + uint256 _suppliedOnPool, + uint256 _repaidToPool ) internal view returns (uint256, uint256) { - (uint256 p2pSupplyRatePerYear, , uint256 poolSupplyRatePerYear, ) = getRatesPerYear( - _poolToken + (uint256 p2pSupplyRatePerYear, , uint256 poolSupplyRatePerYear, ) = _getRatesPerYear( + _poolToken, + _suppliedOnPool, + 0, + _repaidToPool, + 0 ); return @@ -434,10 +548,16 @@ abstract contract RatesLens is UsersLens { function _getUserBorrowRatePerYear( address _poolToken, uint256 _balanceInP2P, - uint256 _balanceOnPool + uint256 _balanceOnPool, + uint256 _borrowedFromPool, + uint256 _withdrawnFromPool ) internal view returns (uint256, uint256) { - (, uint256 p2pBorrowRatePerYear, , uint256 poolBorrowRatePerYear) = getRatesPerYear( - _poolToken + (, uint256 p2pBorrowRatePerYear, , uint256 poolBorrowRatePerYear) = _getRatesPerYear( + _poolToken, + 0, + _borrowedFromPool, + 0, + _withdrawnFromPool ); return From 90a00511d52d313ccdb67128c2ac10bcac1a2c0b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 9 Jan 2023 17:18:35 +0100 Subject: [PATCH 062/105] Added upgrade tests --- test/prod/aave-v2/TestLifecycle.t.sol | 12 ++-- test/prod/aave-v2/TestUpgradeLens.t.sol | 76 +++++++++++++++++++++++++ test/prod/aave-v2/setup/TestSetup.sol | 7 ++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/test/prod/aave-v2/TestLifecycle.t.sol b/test/prod/aave-v2/TestLifecycle.t.sol index 0cdf07093..3e04b91d1 100644 --- a/test/prod/aave-v2/TestLifecycle.t.sol +++ b/test/prod/aave-v2/TestLifecycle.t.sol @@ -322,14 +322,16 @@ contract TestLifecycle is TestSetup { if (supplyMarket.status.isSupplyPaused) continue; + uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); uint256 borrowAmount = _boundBorrowAmount( borrowMarket, _amount, - oracle.getAssetPrice(borrowMarket.underlying) + borrowedPrice, + PercentageMath.PERCENTAGE_FACTOR ); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, - oracle.getAssetPrice(borrowMarket.underlying), + borrowedPrice, borrowMarket.decimals, oracle.getAssetPrice(supplyMarket.underlying), supplyMarket.decimals, @@ -376,14 +378,16 @@ contract TestLifecycle is TestSetup { if (supplyMarket.status.isSupplyPaused || borrowMarket.status.isBorrowPaused) continue; + uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); uint256 borrowAmount = _boundBorrowAmount( borrowMarket, _amount, - oracle.getAssetPrice(borrowMarket.underlying) + borrowedPrice, + PercentageMath.PERCENTAGE_FACTOR ); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, - oracle.getAssetPrice(borrowMarket.underlying), + borrowedPrice, borrowMarket.decimals, oracle.getAssetPrice(supplyMarket.underlying), supplyMarket.decimals, diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index 240d01a31..da784f703 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import "./setup/TestSetup.sol"; contract TestUpgradeLens is TestSetup { + using WadRayMath for uint256; + function testShouldPreserveIndexes() public { Types.Indexes[] memory expectedIndexes = new Types.Indexes[](markets.length); @@ -42,4 +44,78 @@ contract TestUpgradeLens is TestSetup { ); } } + + function testNextRateShouldMatchRateAfterInteraction(uint96 _amount) public { + _upgrade(); + + for ( + uint256 supplyMarketIndex; + supplyMarketIndex < collateralMarkets.length; + ++supplyMarketIndex + ) { + for ( + uint256 borrowMarketIndex; + borrowMarketIndex < borrowableMarkets.length; + ++borrowMarketIndex + ) { + _revert(); + + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + + uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); + uint256 borrowAmount = _boundBorrowAmount( + borrowMarket, + _amount, + borrowedPrice, + 60_00 + ); + uint256 supplyAmount = _getMinimumCollateralAmount( + borrowAmount, + borrowedPrice, + borrowMarket.decimals, + oracle.getAssetPrice(supplyMarket.underlying), + supplyMarket.decimals, + supplyMarket.ltv + ).wadMul(1.001 ether); + + (uint256 expectedSupplyRate, , , ) = lens.getNextUserSupplyRatePerYear( + supplyMarket.poolToken, + address(user), + supplyAmount + ); + + _tip(supplyMarket.underlying, address(user), supplyAmount); + + user.approve(supplyMarket.underlying, supplyAmount); + user.supply(supplyMarket.poolToken, address(user), supplyAmount); + + assertApproxEqAbs( + lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), + expectedSupplyRate, + 1e24, + string.concat(supplyMarket.symbol, " supply rate") + ); + + if (borrowMarket.status.isBorrowPaused) continue; + + (uint256 expectedBorrowRate, , , ) = lens.getNextUserBorrowRatePerYear( + borrowMarket.poolToken, + address(user), + borrowAmount + ); + + user.borrow(borrowMarket.poolToken, borrowAmount); + + assertApproxEqAbs( + lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), + expectedBorrowRate, + 1e24, + string.concat(borrowMarket.symbol, " borrow rate") + ); + } + } + } } diff --git a/test/prod/aave-v2/setup/TestSetup.sol b/test/prod/aave-v2/setup/TestSetup.sol index 50195d22f..17db3c2c5 100644 --- a/test/prod/aave-v2/setup/TestSetup.sol +++ b/test/prod/aave-v2/setup/TestSetup.sol @@ -227,7 +227,8 @@ contract TestSetup is Config, Test { function _boundBorrowAmount( TestMarket memory _market, uint96 _amount, - uint256 _price + uint256 _price, + uint256 _maxUtilizationBps ) internal view returns (uint256) { return bound( @@ -235,7 +236,9 @@ contract TestSetup is Config, Test { (MIN_ETH_AMOUNT * 10**_market.decimals) / _price, Math.min( Math.min( - ERC20(_market.underlying).balanceOf(_market.poolToken), + ERC20(_market.underlying).balanceOf(_market.poolToken).percentMul( + _maxUtilizationBps + ), (MAX_ETH_AMOUNT * 10**_market.decimals) / _price ), type(uint96).max / 2 // so that collateral amount < type(uint96).max From bda8ff1cb0db2b823945853145ea7f70014151f9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 9 Jan 2023 19:02:02 +0100 Subject: [PATCH 063/105] Update tests --- config/eth-mainnet/aave-v2/Config.sol | 7 ++ test/aave-v2/TestRatesLens.t.sol | 110 ++++++++++++++------------ test/aave-v2/setup/TestSetup.sol | 1 + 3 files changed, 69 insertions(+), 49 deletions(-) diff --git a/config/eth-mainnet/aave-v2/Config.sol b/config/eth-mainnet/aave-v2/Config.sol index 080dd90c5..54c87baf5 100644 --- a/config/eth-mainnet/aave-v2/Config.sol +++ b/config/eth-mainnet/aave-v2/Config.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0; import {ILendingPool} from "src/aave-v2/interfaces/aave/ILendingPool.sol"; import {IPriceOracleGetter} from "src/aave-v2/interfaces/aave/IPriceOracleGetter.sol"; +import {IProtocolDataProvider} from "src/aave-v2/interfaces/aave/IProtocolDataProvider.sol"; import {ILendingPoolAddressesProvider} from "src/aave-v2/interfaces/aave/ILendingPoolAddressesProvider.sol"; import {IEntryPositionsManager} from "src/aave-v2/interfaces/IEntryPositionsManager.sol"; import {IExitPositionsManager} from "src/aave-v2/interfaces/IExitPositionsManager.sol"; @@ -42,6 +43,12 @@ contract Config is BaseConfig { ILendingPoolAddressesProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); ILendingPoolConfigurator public lendingPoolConfigurator = ILendingPoolConfigurator(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); + IProtocolDataProvider public dataProvider = + IProtocolDataProvider( + poolAddressesProvider.getAddress( + 0x0100000000000000000000000000000000000000000000000000000000000000 + ) + ); IPriceOracleGetter public oracle = IPriceOracleGetter(poolAddressesProvider.getPriceOracle()); ILendingPool public pool = ILendingPool(poolAddressesProvider.getLendingPool()); diff --git a/test/aave-v2/TestRatesLens.t.sol b/test/aave-v2/TestRatesLens.t.sol index f42b32bee..abdb7a341 100644 --- a/test/aave-v2/TestRatesLens.t.sol +++ b/test/aave-v2/TestRatesLens.t.sol @@ -8,6 +8,8 @@ contract TestRatesLens is TestSetup { using SafeTransferLib for ERC20; function testGetRatesPerYear() public { + supplier1.aaveSupply(dai, 1 ether); // Update pool rates. + hevm.roll(block.number + 1_000); ( uint256 p2pSupplyRate, @@ -174,7 +176,7 @@ contract TestRatesLens is TestSetup { uint256 totalBalance ) = lens.getNextUserSupplyRatePerYear(aDai, address(supplier1), 0); - assertEq(supplyRatePerYear, 0, "non zero supply rate per block"); + assertEq(supplyRatePerYear, 0, "non zero supply rate per year"); assertEq(balanceOnPool, 0, "non zero pool balance"); assertEq(balanceInP2P, 0, "non zero p2p balance"); assertEq(totalBalance, 0, "non zero total balance"); @@ -188,7 +190,7 @@ contract TestRatesLens is TestSetup { uint256 totalBalance ) = lens.getNextUserBorrowRatePerYear(aDai, address(borrower1), 0); - assertEq(borrowRatePerYear, 0, "non zero borrow rate per block"); + assertEq(borrowRatePerYear, 0, "non zero borrow rate per year"); assertEq(balanceOnPool, 0, "non zero pool balance"); assertEq(balanceInP2P, 0, "non zero p2p balance"); assertEq(totalBalance, 0, "non zero total balance"); @@ -219,8 +221,8 @@ contract TestRatesLens is TestSetup { uint256 expectedTotalBalance ) = lens.getCurrentSupplyBalanceInOf(aDai, address(supplier1)); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); - assertEq(supplyRatePerYear, expectedSupplyRatePerYear, "unexpected supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); + assertEq(supplyRatePerYear, expectedSupplyRatePerYear, "unexpected supply rate per year"); assertEq(balanceOnPool, expectedBalanceOnPool, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); assertEq(totalBalance, expectedTotalBalance, "unexpected total balance"); @@ -252,8 +254,8 @@ contract TestRatesLens is TestSetup { uint256 expectedTotalBalance ) = lens.getCurrentBorrowBalanceInOf(aDai, address(borrower1)); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); - assertEq(borrowRatePerYear, expectedBorrowRatePerYear, "unexpected borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); + assertEq(borrowRatePerYear, expectedBorrowRatePerYear, "unexpected borrow rate per year"); assertEq(balanceOnPool, expectedBalanceOnPool, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); assertEq(totalBalance, expectedTotalBalance, "unexpected total balance"); @@ -273,12 +275,12 @@ contract TestRatesLens is TestSetup { uint256 expectedSupplyRatePerYear = reserve.currentLiquidityRate; uint256 poolSupplyIndex = pool.getReserveNormalizedIncome(dai); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, expectedSupplyRatePerYear, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertEq( balanceOnPool, @@ -306,12 +308,12 @@ contract TestRatesLens is TestSetup { DataTypes.ReserveData memory reserve = pool.getReserveData(dai); uint256 expectedBorrowRatePerYear = reserve.currentVariableBorrowRate; - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, expectedBorrowRatePerYear, - 1, - "unexpected borrow rate per block" + 1e22, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, amount, 1, "unexpected pool balance"); assertEq(balanceInP2P, 0, "unexpected p2p balance"); @@ -341,12 +343,12 @@ contract TestRatesLens is TestSetup { uint256 expectedBalanceInP2P = amount.rayDiv(p2pSupplyIndex).rayMul(p2pSupplyIndex); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, p2pSupplyRatePerYear, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); @@ -375,12 +377,12 @@ contract TestRatesLens is TestSetup { uint256 expectedBalanceInP2P = amount.rayDiv(p2pBorrowIndex).rayMul(p2pBorrowIndex); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, p2pBorrowRatePerYear, - 1, - "unexpected borrow rate per block" + 1e22, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, 0, 1, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); @@ -413,12 +415,12 @@ contract TestRatesLens is TestSetup { ); uint256 expectedBalanceInP2P = (amount / 2).rayDiv(p2pSupplyIndex).rayMul(p2pSupplyIndex); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, (p2pSupplyRatePerYear + poolSupplyRatePerYear) / 2, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertEq(balanceOnPool, expectedBalanceOnPool, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); @@ -454,12 +456,12 @@ contract TestRatesLens is TestSetup { ); uint256 expectedBalanceInP2P = (amount / 2).rayDiv(p2pBorrowIndex).rayMul(p2pBorrowIndex); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, (p2pBorrowRatePerYear + poolBorrowRatePerYear) / 2, - 1, - "unexpected borrow rate per block" + 1e22, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, expectedBalanceOnPool, 1, "unexpected pool balance"); assertApproxEqAbs(balanceInP2P, expectedBalanceInP2P, 1, "unexpected p2p balance"); @@ -496,8 +498,8 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerYear, expectedPoolSupplyRate, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertEq( balanceOnPool, @@ -535,8 +537,8 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerYear, expectedBorrowRatePerYear, - 1, - "unexpected borrow rate per block" + 1e22, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, amount, 1, "unexpected pool balance"); assertEq(balanceInP2P, 0, "unexpected p2p balance"); @@ -565,12 +567,12 @@ contract TestRatesLens is TestSetup { uint256 p2pSupplyIndex = morpho.p2pSupplyIndex(aDai); uint256 expectedBalanceInP2P = amount.rayDiv(p2pSupplyIndex).rayMul(p2pSupplyIndex); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, p2pSupplyRatePerYear, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); assertApproxEqAbs(balanceInP2P, expectedBalanceInP2P, 1, "unexpected p2p balance"); @@ -599,12 +601,12 @@ contract TestRatesLens is TestSetup { uint256 p2pBorrowIndex = morpho.p2pBorrowIndex(aDai); uint256 expectedBalanceInP2P = amount.rayDiv(p2pBorrowIndex).rayMul(p2pBorrowIndex); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, p2pBorrowRatePerYear, - 1, - "unexpected borrow rate per block" + 1e21, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, 0, 1, "unexpected pool balance"); assertApproxEqAbs(balanceInP2P, expectedBalanceInP2P, 1, "unexpected p2p balance"); @@ -639,12 +641,12 @@ contract TestRatesLens is TestSetup { uint256 p2pSupplyIndex = morpho.p2pSupplyIndex(aDai); uint256 expectedBalanceInP2P = amount.rayDiv(p2pSupplyIndex).rayMul(p2pSupplyIndex); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, p2pSupplyRatePerYear, 1, - "unexpected supply rate per block" + "unexpected supply rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); @@ -680,12 +682,12 @@ contract TestRatesLens is TestSetup { uint256 p2pBorrowIndex = morpho.p2pBorrowIndex(aDai); uint256 expectedBalanceInP2P = amount.rayDiv(p2pBorrowIndex).rayMul(p2pBorrowIndex); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, p2pBorrowRatePerYear, 1, - "unexpected borrow rate per block" + "unexpected borrow rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); assertEq(balanceInP2P, expectedBalanceInP2P, "unexpected p2p balance"); @@ -727,12 +729,12 @@ contract TestRatesLens is TestSetup { ); uint256 expectedBalanceInP2P = (amount / 2).rayDiv(p2pSupplyIndex).rayMul(p2pSupplyIndex); - assertGt(supplyRatePerYear, 0, "zero supply rate per block"); + assertGt(supplyRatePerYear, 0, "zero supply rate per year"); assertApproxEqAbs( supplyRatePerYear, (p2pSupplyRatePerYear + poolSupplyRatePerYear) / 2, - 1, - "unexpected supply rate per block" + 1e22, + "unexpected supply rate per year" ); assertApproxEqAbs(balanceOnPool, expectedBalanceOnPool, 1, "unexpected pool balance"); assertApproxEqAbs(balanceInP2P, expectedBalanceInP2P, 1, "unexpected p2p balance"); @@ -779,12 +781,12 @@ contract TestRatesLens is TestSetup { ); uint256 expectedBalanceInP2P = (amount / 2).rayDiv(p2pBorrowIndex).rayMul(p2pBorrowIndex); - assertGt(borrowRatePerYear, 0, "zero borrow rate per block"); + assertGt(borrowRatePerYear, 0, "zero borrow rate per year"); assertApproxEqAbs( borrowRatePerYear, (p2pBorrowRatePerYear + poolBorrowRatePerYear) / 2, - 1, - "unexpected borrow rate per block" + 1e21, + "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, expectedBalanceOnPool, 1, "unexpected pool balance"); assertApproxEqAbs(balanceInP2P, expectedBalanceInP2P, 1, "unexpected p2p balance"); @@ -1056,7 +1058,7 @@ contract TestRatesLens is TestSetup { (uint256 p2pSupplyDelta, , , ) = morpho.deltas(aDai); assertEq(p2pSupplyDelta, repayAmount.rayDiv(pool.getReserveNormalizedIncome(dai))); - // Invert spreads on DAI. + // TODO: mock the call to reserve strategy to return inverted spread (uint256 poolSupplyRate, uint256 poolBorrowRate) = _invertPoolSpreadWithStorageManipulation( dai ); @@ -1065,9 +1067,19 @@ contract TestRatesLens is TestSetup { (uint256 avgBorrowRate, , ) = lens.getAverageBorrowRatePerYear(aDai); (uint256 p2pSupplyRate, uint256 p2pBorrowRate, , ) = lens.getRatesPerYear(aDai); - assertApproxEqAbs(avgSupplyRate, (poolBorrowRate + poolSupplyRate) / 2, 1e7); - assertEq(avgBorrowRate, poolBorrowRate); - assertApproxEqAbs(p2pSupplyRate, (poolBorrowRate + poolSupplyRate) / 2, 1e7); - assertEq(p2pBorrowRate, poolBorrowRate); + assertApproxEqAbs( + avgSupplyRate, + (poolBorrowRate + poolSupplyRate) / 2, + 1e7, + "avg supply rate" + ); + assertEq(avgBorrowRate, poolBorrowRate, "avg borrow rate"); + assertApproxEqAbs( + p2pSupplyRate, + (poolBorrowRate + poolSupplyRate) / 2, + 1e7, + "p2p supply rate" + ); + assertEq(p2pBorrowRate, poolBorrowRate, "p2p borrow rate"); } } diff --git a/test/aave-v2/setup/TestSetup.sol b/test/aave-v2/setup/TestSetup.sol index a9a96791d..1519c4d79 100644 --- a/test/aave-v2/setup/TestSetup.sol +++ b/test/aave-v2/setup/TestSetup.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; +import "src/aave-v2/interfaces/aave/IReserveInterestRateStrategy.sol"; import "src/aave-v2/interfaces/aave/IVariableDebtToken.sol"; import "src/aave-v2/interfaces/aave/IAToken.sol"; import "src/aave-v2/interfaces/IMorpho.sol"; From b884b2cc9f00e73eef86863474da40843d4515f4 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 10 Jan 2023 10:55:50 +0100 Subject: [PATCH 064/105] Update inverted spread test --- test/aave-v2/TestRatesLens.t.sol | 41 ++++++++++++++++--------- test/prod/aave-v2/TestLifecycle.t.sol | 14 ++------- test/prod/aave-v2/TestUpgradeLens.t.sol | 11 ++----- test/prod/aave-v2/setup/TestSetup.sol | 7 ++--- 4 files changed, 34 insertions(+), 39 deletions(-) diff --git a/test/aave-v2/TestRatesLens.t.sol b/test/aave-v2/TestRatesLens.t.sol index abdb7a341..4e0eef939 100644 --- a/test/aave-v2/TestRatesLens.t.sol +++ b/test/aave-v2/TestRatesLens.t.sol @@ -1046,40 +1046,53 @@ contract TestRatesLens is TestSetup { function testRatesWithInvertedSpreadAndHalfSupplyDelta() public { // Full p2p on the DAI market, with a 50% supply delta. uint256 supplyAmount = 1 ether; - uint256 repayAmount = 0.5 ether; + uint256 repayAmount = supplyAmount / 2; + supplier1.approve(dai, supplyAmount); supplier1.supply(aDai, supplyAmount); + borrower1.approve(aave, supplyAmount); borrower1.supply(aAave, supplyAmount); borrower1.borrow(aDai, supplyAmount); + setDefaultMaxGasForMatchingHelper(0, 0, 0, 0); borrower1.approve(dai, repayAmount); borrower1.repay(aDai, repayAmount); - (uint256 p2pSupplyDelta, , , ) = morpho.deltas(aDai); - assertEq(p2pSupplyDelta, repayAmount.rayDiv(pool.getReserveNormalizedIncome(dai))); - // TODO: mock the call to reserve strategy to return inverted spread - (uint256 poolSupplyRate, uint256 poolBorrowRate) = _invertPoolSpreadWithStorageManipulation( - dai + DataTypes.ReserveData memory reserve = pool.getReserveData(dai); + reserve.currentLiquidityRate = 0.03e27; + reserve.currentVariableBorrowRate = 0.02e27; + reserve.currentStableBorrowRate = 0.04e27; + vm.mockCall( + reserve.interestRateStrategyAddress, + abi.encodeWithSelector(IReserveInterestRateStrategy.calculateInterestRates.selector), + abi.encode( + reserve.currentLiquidityRate, + reserve.currentStableBorrowRate, + reserve.currentVariableBorrowRate + ) + ); + vm.mockCall( + address(pool), + abi.encodeWithSelector(ILendingPool.getReserveData.selector), + abi.encode(reserve) ); (uint256 avgSupplyRate, , ) = lens.getAverageSupplyRatePerYear(aDai); (uint256 avgBorrowRate, , ) = lens.getAverageBorrowRatePerYear(aDai); (uint256 p2pSupplyRate, uint256 p2pBorrowRate, , ) = lens.getRatesPerYear(aDai); - assertApproxEqAbs( + assertEq( avgSupplyRate, - (poolBorrowRate + poolSupplyRate) / 2, - 1e7, + (reserve.currentVariableBorrowRate + reserve.currentLiquidityRate) / 2, "avg supply rate" ); - assertEq(avgBorrowRate, poolBorrowRate, "avg borrow rate"); - assertApproxEqAbs( + assertEq(avgBorrowRate, reserve.currentVariableBorrowRate, "avg borrow rate"); + assertEq( p2pSupplyRate, - (poolBorrowRate + poolSupplyRate) / 2, - 1e7, + (reserve.currentVariableBorrowRate + reserve.currentLiquidityRate) / 2, "p2p supply rate" ); - assertEq(p2pBorrowRate, poolBorrowRate, "p2p borrow rate"); + assertEq(p2pBorrowRate, reserve.currentVariableBorrowRate, "p2p borrow rate"); } } diff --git a/test/prod/aave-v2/TestLifecycle.t.sol b/test/prod/aave-v2/TestLifecycle.t.sol index 3e04b91d1..67613bfff 100644 --- a/test/prod/aave-v2/TestLifecycle.t.sol +++ b/test/prod/aave-v2/TestLifecycle.t.sol @@ -323,12 +323,7 @@ contract TestLifecycle is TestSetup { if (supplyMarket.status.isSupplyPaused) continue; uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); - uint256 borrowAmount = _boundBorrowAmount( - borrowMarket, - _amount, - borrowedPrice, - PercentageMath.PERCENTAGE_FACTOR - ); + uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, borrowedPrice, @@ -379,12 +374,7 @@ contract TestLifecycle is TestSetup { continue; uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); - uint256 borrowAmount = _boundBorrowAmount( - borrowMarket, - _amount, - borrowedPrice, - PercentageMath.PERCENTAGE_FACTOR - ); + uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, borrowedPrice, diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index da784f703..d46813998 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -66,12 +66,7 @@ contract TestUpgradeLens is TestSetup { if (supplyMarket.status.isSupplyPaused) continue; uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); - uint256 borrowAmount = _boundBorrowAmount( - borrowMarket, - _amount, - borrowedPrice, - 60_00 - ); + uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, borrowedPrice, @@ -95,7 +90,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e24, + 1e21, string.concat(supplyMarket.symbol, " supply rate") ); @@ -112,7 +107,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e24, + 1e21, string.concat(borrowMarket.symbol, " borrow rate") ); } diff --git a/test/prod/aave-v2/setup/TestSetup.sol b/test/prod/aave-v2/setup/TestSetup.sol index 17db3c2c5..50195d22f 100644 --- a/test/prod/aave-v2/setup/TestSetup.sol +++ b/test/prod/aave-v2/setup/TestSetup.sol @@ -227,8 +227,7 @@ contract TestSetup is Config, Test { function _boundBorrowAmount( TestMarket memory _market, uint96 _amount, - uint256 _price, - uint256 _maxUtilizationBps + uint256 _price ) internal view returns (uint256) { return bound( @@ -236,9 +235,7 @@ contract TestSetup is Config, Test { (MIN_ETH_AMOUNT * 10**_market.decimals) / _price, Math.min( Math.min( - ERC20(_market.underlying).balanceOf(_market.poolToken).percentMul( - _maxUtilizationBps - ), + ERC20(_market.underlying).balanceOf(_market.poolToken), (MAX_ETH_AMOUNT * 10**_market.decimals) / _price ), type(uint96).max / 2 // so that collateral amount < type(uint96).max From 0ebdb6ef95de474d1ccfe4a17e5a4d69e5e0234b Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 10 Jan 2023 15:39:36 +0100 Subject: [PATCH 065/105] Apply suggestions --- src/aave-v2/lens/RatesLens.sol | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index b31eba212..6bbce731e 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -51,17 +51,18 @@ abstract contract RatesLens is UsersLens { uint256 repaidToPool; if (!market.isP2PDisabled) { - if (_amount > 0 && delta.p2pBorrowDelta > 0) { + if (delta.p2pBorrowDelta > 0) { uint256 matchedDelta = Math.min( delta.p2pBorrowDelta.rayMul(indexes.poolBorrowIndex), _amount ); supplyBalance.inP2P += matchedDelta.rayDiv(indexes.p2pSupplyIndex); + repaidToPool += matchedDelta; _amount -= matchedDelta; } - if (_amount > 0 && !market.isP2PDisabled) { + if (_amount > 0) { address firstPoolBorrower = morpho.getHead( _poolToken, Types.PositionType.BORROWERS_ON_POOL @@ -138,6 +139,7 @@ abstract contract RatesLens is UsersLens { ); borrowBalance.inP2P += matchedDelta.rayDiv(indexes.p2pBorrowIndex); + withdrawnFromPool += matchedDelta; _amount -= matchedDelta; } @@ -151,13 +153,14 @@ abstract contract RatesLens is UsersLens { .onPool; if (firstPoolSupplierBalance > 0) { - withdrawnFromPool = Math.min( + uint256 matchedP2P = Math.min( firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), _amount ); - borrowBalance.inP2P += withdrawnFromPool.rayDiv(indexes.p2pBorrowIndex); - _amount -= withdrawnFromPool; + borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); + withdrawnFromPool += matchedP2P; + _amount -= matchedP2P; } } } @@ -394,8 +397,8 @@ abstract contract RatesLens is UsersLens { market.underlyingToken, _suppliedOnPool, _borrowedFromPool, - _repaidOnPool, - _withdrawnFromPool + _withdrawnFromPool, + _repaidOnPool ); p2pSupplyRate = InterestRatesModel.computeP2PSupplyRatePerYear( @@ -437,8 +440,8 @@ abstract contract RatesLens is UsersLens { address _underlying, uint256 _supplied, uint256 _borrowed, - uint256 _repaid, - uint256 _withdrawn + uint256 _withdrawn, + uint256 _repaid ) internal view returns (uint256 poolSupplyRate, uint256 poolBorrowRate) { DataTypes.ReserveData memory reserve = pool.getReserveData(_underlying); From e4588ada1094eceb817a22816c8b27e3686182bd Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 10 Jan 2023 16:59:26 +0100 Subject: [PATCH 066/105] Fix tests --- test/aave-v2/TestRatesLens.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/aave-v2/TestRatesLens.t.sol b/test/aave-v2/TestRatesLens.t.sol index 4e0eef939..f92365f16 100644 --- a/test/aave-v2/TestRatesLens.t.sol +++ b/test/aave-v2/TestRatesLens.t.sol @@ -645,7 +645,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerYear, p2pSupplyRatePerYear, - 1, + 1e22, "unexpected supply rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); @@ -686,7 +686,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerYear, p2pBorrowRatePerYear, - 1, + 1e22, "unexpected borrow rate per year" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); @@ -785,7 +785,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerYear, (p2pBorrowRatePerYear + poolBorrowRatePerYear) / 2, - 1e21, + 1e22, "unexpected borrow rate per year" ); assertApproxEqAbs(balanceOnPool, expectedBalanceOnPool, 1, "unexpected pool balance"); From e61d813f323c687d76293bfbb554bd637f7c62e9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 10:37:15 +0100 Subject: [PATCH 067/105] Propagate to Morpho-Compound --- src/aave-v2/lens/RatesLens.sol | 66 ++++----- src/compound/lens/IndexesLens.sol | 186 +++++++++++++++--------- src/compound/lens/RatesLens.sol | 232 ++++++++++++++++++++++-------- src/compound/lens/UsersLens.sol | 2 +- 4 files changed, 328 insertions(+), 158 deletions(-) diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index 6bbce731e..7b607d0be 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -71,16 +71,14 @@ abstract contract RatesLens is UsersLens { .borrowBalanceInOf(_poolToken, firstPoolBorrower) .onPool; - if (firstPoolBorrowerBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolBorrowerBalance.rayMul(indexes.poolBorrowIndex), - _amount - ); - - supplyBalance.inP2P += matchedP2P.rayDiv(indexes.p2pSupplyIndex); - repaidToPool += matchedP2P; - _amount -= matchedP2P; - } + uint256 matchedP2P = Math.min( + firstPoolBorrowerBalance.rayMul(indexes.poolBorrowIndex), + _amount + ); + + supplyBalance.inP2P += matchedP2P.rayDiv(indexes.p2pSupplyIndex); + repaidToPool += matchedP2P; + _amount -= matchedP2P; } } @@ -152,16 +150,14 @@ abstract contract RatesLens is UsersLens { .supplyBalanceInOf(_poolToken, firstPoolSupplier) .onPool; - if (firstPoolSupplierBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), - _amount - ); + uint256 matchedP2P = Math.min( + firstPoolSupplierBalance.rayMul(indexes.poolSupplyIndex), + _amount + ); - borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); - withdrawnFromPool += matchedP2P; - _amount -= matchedP2P; - } + borrowBalance.inP2P += matchedP2P.rayDiv(indexes.p2pBorrowIndex); + withdrawnFromPool += matchedP2P; + _amount -= matchedP2P; } } @@ -361,12 +357,12 @@ abstract contract RatesLens is UsersLens { uint256 reserveFactor; } - /// @notice Computes and returns peer-to-peer and pool rates for a specific market. + /// @dev Computes and returns peer-to-peer and pool rates for a specific market. /// @param _poolToken The market address. - /// @param _suppliedOnPool The amount hypothetically supplied to the underlying's pool. - /// @param _borrowedFromPool The amount hypothetically borrowed from the underlying's pool. - /// @param _repaidOnPool The amount hypothetically repaid to the underlying's pool. - /// @param _withdrawnFromPool The amount hypothetically withdrawn from the underlying's pool. + /// @param _suppliedOnPool The amount hypothetically supplied to the underlying's pool (in underlying). + /// @param _borrowedFromPool The amount hypothetically borrowed from the underlying's pool (in underlying). + /// @param _repaidOnPool The amount hypothetically repaid to the underlying's pool (in underlying). + /// @param _withdrawnFromPool The amount hypothetically withdrawn from the underlying's pool (in underlying). /// @return p2pSupplyRate The market's peer-to-peer supply rate per year (in ray). /// @return p2pBorrowRate The market's peer-to-peer borrow rate per year (in ray). /// @return poolSupplyRate The market's pool supply rate per year (in ray). @@ -378,7 +374,7 @@ abstract contract RatesLens is UsersLens { uint256 _repaidOnPool, uint256 _withdrawnFromPool ) - public + internal view returns ( uint256 p2pSupplyRate, @@ -428,12 +424,12 @@ abstract contract RatesLens is UsersLens { ); } - /// @notice Computes and returns the underlying pool rates for a specific market. + /// @dev Computes and returns the underlying pool rates for a specific market. /// @param _underlying The underlying pool market address. - /// @param _supplied The amount hypothetically supplied. - /// @param _borrowed The amount hypothetically borrowed. - /// @param _repaid The amount hypothetically repaid. - /// @param _withdrawn The amount hypothetically withdrawn. + /// @param _supplied The amount hypothetically supplied (in underlying). + /// @param _borrowed The amount hypothetically borrowed (in underlying). + /// @param _repaid The amount hypothetically repaid (in underlying). + /// @param _withdrawn The amount hypothetically withdrawn (in underlying). /// @return poolSupplyRate The market's pool supply rate per year (in ray). /// @return poolBorrowRate The market's pool borrow rate per year (in ray). function _getPoolRatesPerYear( @@ -472,7 +468,7 @@ abstract contract RatesLens is UsersLens { ); } - /// @notice Computes and returns the total distribution of supply for a given market, using virtually updated indexes. + /// @dev Computes and returns the total distribution of supply for a given market, using virtually updated indexes. /// @param _poolToken The address of the market to check. /// @param _p2pSupplyIndex The given market's peer-to-peer supply index. /// @param _poolSupplyIndex The given market's pool supply index. @@ -491,7 +487,7 @@ abstract contract RatesLens is UsersLens { poolSupplyAmount = IAToken(_poolToken).balanceOf(address(morpho)); } - /// @notice Computes and returns the total distribution of borrows for a given market, using virtually updated indexes. + /// @dev Computes and returns the total distribution of borrows for a given market, using virtually updated indexes. /// @param reserve The reserve data of the underlying pool. /// @param _p2pBorrowIndex The given market's peer-to-peer borrow index. /// @param _poolBorrowIndex The given market's pool borrow index. @@ -513,9 +509,12 @@ abstract contract RatesLens is UsersLens { } /// @dev Returns the supply rate per year experienced on a market based on a given position distribution. + /// The calculation takes into account the change in pool rates implied by an hypothetical supply and/or repay. /// @param _poolToken The address of the market. /// @param _balanceOnPool The amount of balance supplied on pool (in a unit common to `_balanceInP2P`). /// @param _balanceInP2P The amount of balance matched peer-to-peer (in a unit common to `_balanceOnPool`). + /// @param _suppliedOnPool The amount hypothetically supplied on pool (in underlying). + /// @param _repaidToPool The amount hypothetically repaid to the pool (in underlying). /// @return The supply rate per year experienced by the given position (in ray). /// @return The sum of peer-to-peer & pool balances. function _getUserSupplyRatePerYear( @@ -543,9 +542,12 @@ abstract contract RatesLens is UsersLens { } /// @dev Returns the borrow rate per year experienced on a market based on a given position distribution. + /// The calculation takes into account the change in pool rates implied by an hypothetical borrow and/or withdraw. /// @param _poolToken The address of the market. /// @param _balanceOnPool The amount of balance supplied on pool (in a unit common to `_balanceInP2P`). /// @param _balanceInP2P The amount of balance matched peer-to-peer (in a unit common to `_balanceOnPool`). + /// @param _borrowedFromPool The amount hypothetically borrowed from the pool (in underlying). + /// @param _withdrawnFromPool The amount hypothetically withdrawn from the pool (in underlying). /// @return The borrow rate per year experienced by the given position (in ray). /// @return The sum of peer-to-peer & pool balances. function _getUserBorrowRatePerYear( diff --git a/src/compound/lens/IndexesLens.sol b/src/compound/lens/IndexesLens.sol index b0bcddcc9..bf366470f 100644 --- a/src/compound/lens/IndexesLens.sol +++ b/src/compound/lens/IndexesLens.sol @@ -40,15 +40,13 @@ abstract contract IndexesLens is LensStorage { p2pBorrowIndex = indexes.p2pBorrowIndex; } - /// PUBLIC /// - /// @notice Returns the most up-to-date or virtually updated peer-to-peer and pool indexes. /// @dev If not virtually updated, the indexes returned are those used by Morpho for non-updated markets during the liquidity check. /// @param _poolToken The address of the market. /// @param _updated Whether to compute virtually updated pool and peer-to-peer indexes. /// @return indexes The given market's virtually updated indexes. function getIndexes(address _poolToken, bool _updated) - public + external view returns (Types.Indexes memory indexes) { @@ -61,42 +59,22 @@ abstract contract IndexesLens is LensStorage { /// @return poolSupplyIndex The supply index. /// @return poolBorrowIndex The borrow index. function getCurrentPoolIndexes(address _poolToken) - public + external view returns (uint256 poolSupplyIndex, uint256 poolBorrowIndex) { - ICToken cToken = ICToken(_poolToken); - - uint256 accrualBlockNumberPrior = cToken.accrualBlockNumber(); - if (block.number == accrualBlockNumberPrior) - return (cToken.exchangeRateStored(), cToken.borrowIndex()); - - // Read the previous values out of storage - uint256 cashPrior = cToken.getCash(); - uint256 totalSupply = cToken.totalSupply(); - uint256 borrowsPrior = cToken.totalBorrows(); - uint256 reservesPrior = cToken.totalReserves(); - uint256 borrowIndexPrior = cToken.borrowIndex(); - - // Calculate the current borrow interest rate - uint256 borrowRateMantissa = cToken.borrowRatePerBlock(); - require(borrowRateMantissa <= 0.0005e16, "borrow rate is absurdly high"); - - uint256 blockDelta = block.number - accrualBlockNumberPrior; - - // Calculate the interest accumulated into borrows and reserves and the current index. - uint256 simpleInterestFactor = borrowRateMantissa * blockDelta; - uint256 interestAccumulated = simpleInterestFactor.mul(borrowsPrior); - uint256 totalBorrowsNew = interestAccumulated + borrowsPrior; - uint256 totalReservesNew = cToken.reserveFactorMantissa().mul(interestAccumulated) + - reservesPrior; - - poolSupplyIndex = (cashPrior + totalBorrowsNew - totalReservesNew).div(totalSupply); - poolBorrowIndex = simpleInterestFactor.mul(borrowIndexPrior) + borrowIndexPrior; + (poolSupplyIndex, poolBorrowIndex, ) = _accruePoolInterests(ICToken(_poolToken)); } /// INTERNAL /// + struct PoolInterestsVars { + uint256 cash; + uint256 totalBorrows; + uint256 totalReserves; + uint256 reserveFactorMantissa; + } + /// @notice Returns the most up-to-date or virtually updated peer-to-peer and pool indexes. /// @dev If not virtually updated, the indexes returned are those used by Morpho for non-updated markets during the liquidity check. /// @param _poolToken The address of the market. @@ -115,44 +93,122 @@ abstract contract IndexesLens is LensStorage { indexes.poolSupplyIndex = ICToken(_poolToken).exchangeRateStored(); indexes.poolBorrowIndex = ICToken(_poolToken).borrowIndex(); } else { - (indexes.poolSupplyIndex, indexes.poolBorrowIndex) = getCurrentPoolIndexes(_poolToken); + (indexes.poolSupplyIndex, indexes.poolBorrowIndex, ) = _accruePoolInterests( + ICToken(_poolToken) + ); } - if (!_updated || block.number == lastPoolIndexes.lastUpdateBlockNumber) { - indexes.p2pSupplyIndex = morpho.p2pSupplyIndex(_poolToken); - indexes.p2pBorrowIndex = morpho.p2pBorrowIndex(_poolToken); + (indexes.p2pSupplyIndex, indexes.p2pBorrowIndex, ) = _computeP2PIndexes( + _poolToken, + _updated, + indexes.poolSupplyIndex, + indexes.poolBorrowIndex, + delta, + lastPoolIndexes + ); + } + + /// @notice Returns the virtually updated pool indexes of a given market. + /// @dev Mimicks `CToken.accrueInterest`'s calculations, without writing to the storage. + /// @param _poolToken The address of the market. + /// @return poolSupplyIndex The supply index. + /// @return poolBorrowIndex The borrow index. + function _accruePoolInterests(ICToken _poolToken) + internal + view + returns ( + uint256 poolSupplyIndex, + uint256 poolBorrowIndex, + PoolInterestsVars memory vars + ) + { + poolBorrowIndex = _poolToken.borrowIndex(); + vars.cash = _poolToken.getCash(); + vars.totalBorrows = _poolToken.totalBorrows(); + vars.totalReserves = _poolToken.totalReserves(); + vars.reserveFactorMantissa = _poolToken.reserveFactorMantissa(); + + uint256 accrualBlockNumberPrior = _poolToken.accrualBlockNumber(); + if (block.number == accrualBlockNumberPrior) { + poolSupplyIndex = _poolToken.exchangeRateStored(); } else { - Types.MarketParameters memory marketParams = morpho.marketParameters(_poolToken); - - InterestRatesModel.GrowthFactors memory growthFactors = InterestRatesModel - .computeGrowthFactors( - indexes.poolSupplyIndex, - indexes.poolBorrowIndex, - lastPoolIndexes, - marketParams.p2pIndexCursor, - marketParams.reserveFactor - ); + uint256 borrowRateMantissa = _poolToken.borrowRatePerBlock(); + require(borrowRateMantissa <= 0.0005e16, "borrow rate is absurdly high"); - indexes.p2pSupplyIndex = InterestRatesModel.computeP2PIndex( - InterestRatesModel.P2PIndexComputeParams({ - poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, - p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, - lastPoolIndex: lastPoolIndexes.lastSupplyPoolIndex, - lastP2PIndex: morpho.p2pSupplyIndex(_poolToken), - p2pDelta: delta.p2pSupplyDelta, - p2pAmount: delta.p2pSupplyAmount - }) - ); - indexes.p2pBorrowIndex = InterestRatesModel.computeP2PIndex( - InterestRatesModel.P2PIndexComputeParams({ - poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, - p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, - lastPoolIndex: lastPoolIndexes.lastBorrowPoolIndex, - lastP2PIndex: morpho.p2pBorrowIndex(_poolToken), - p2pDelta: delta.p2pBorrowDelta, - p2pAmount: delta.p2pBorrowAmount - }) + uint256 simpleInterestFactor = borrowRateMantissa * + (block.number - accrualBlockNumberPrior); + uint256 interestAccumulated = simpleInterestFactor.mul(vars.totalBorrows); + + vars.totalBorrows += interestAccumulated; + vars.totalReserves += vars.reserveFactorMantissa.mul(interestAccumulated); + + poolSupplyIndex = (vars.cash + vars.totalBorrows - vars.totalReserves).div( + _poolToken.totalSupply() ); + poolBorrowIndex += simpleInterestFactor.mul(poolBorrowIndex); } } + + /// @notice Returns the most up-to-date or virtually updated peer-to-peer indexes. + /// @dev If not virtually updated, the indexes returned are those used by Morpho for non-updated markets during the liquidity check. + /// @param _poolToken The address of the market. + /// @param _updated Whether to compute virtually updated peer-to-peer indexes. + /// @param _poolSupplyIndex The underlying pool supply index. + /// @param _poolBorrowIndex The underlying pool borrow index. + /// @param _delta The given market's deltas. + /// @param _lastPoolIndexes The last pool indexes stored on Morpho. + /// @return _p2pSupplyIndex The given market's peer-to-peer supply index. + /// @return _p2pBorrowIndex The given market's peer-to-peer borrow index. + function _computeP2PIndexes( + address _poolToken, + bool _updated, + uint256 _poolSupplyIndex, + uint256 _poolBorrowIndex, + Types.Delta memory _delta, + Types.LastPoolIndexes memory _lastPoolIndexes + ) + internal + view + returns ( + uint256 _p2pSupplyIndex, + uint256 _p2pBorrowIndex, + Types.MarketParameters memory params + ) + { + params = morpho.marketParameters(_poolToken); + + if (!_updated || block.number == _lastPoolIndexes.lastUpdateBlockNumber) { + return (morpho.p2pSupplyIndex(_poolToken), morpho.p2pBorrowIndex(_poolToken), params); + } + + InterestRatesModel.GrowthFactors memory growthFactors = InterestRatesModel + .computeGrowthFactors( + _poolSupplyIndex, + _poolBorrowIndex, + _lastPoolIndexes, + params.p2pIndexCursor, + params.reserveFactor + ); + + _p2pSupplyIndex = InterestRatesModel.computeP2PIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolSupplyGrowthFactor, + p2pGrowthFactor: growthFactors.p2pSupplyGrowthFactor, + lastPoolIndex: _lastPoolIndexes.lastSupplyPoolIndex, + lastP2PIndex: morpho.p2pSupplyIndex(_poolToken), + p2pDelta: _delta.p2pSupplyDelta, + p2pAmount: _delta.p2pSupplyAmount + }) + ); + _p2pBorrowIndex = InterestRatesModel.computeP2PIndex( + InterestRatesModel.P2PIndexComputeParams({ + poolGrowthFactor: growthFactors.poolBorrowGrowthFactor, + p2pGrowthFactor: growthFactors.p2pBorrowGrowthFactor, + lastPoolIndex: _lastPoolIndexes.lastBorrowPoolIndex, + lastP2PIndex: morpho.p2pBorrowIndex(_poolToken), + p2pDelta: _delta.p2pBorrowDelta, + p2pAmount: _delta.p2pBorrowAmount + }) + ); + } } diff --git a/src/compound/lens/RatesLens.sol b/src/compound/lens/RatesLens.sol index 50a7cc806..76e517c1d 100644 --- a/src/compound/lens/RatesLens.sol +++ b/src/compound/lens/RatesLens.sol @@ -40,33 +40,37 @@ abstract contract RatesLens is UsersLens { (Types.Delta memory delta, Types.Indexes memory indexes) = _getIndexes(_poolToken, true); Types.SupplyBalance memory supplyBalance = morpho.supplyBalanceInOf(_poolToken, _user); + + uint256 repaidToPool; if (!morpho.p2pDisabled(_poolToken)) { - if (_amount > 0 && delta.p2pBorrowDelta > 0) { + if (delta.p2pBorrowDelta > 0) { uint256 matchedDelta = Math.min( delta.p2pBorrowDelta.mul(indexes.poolBorrowIndex), _amount ); supplyBalance.inP2P += matchedDelta.div(indexes.p2pSupplyIndex); + repaidToPool += matchedDelta; _amount -= matchedDelta; } if (_amount > 0) { - uint256 firstPoolBorrowerBalance = morpho - .borrowBalanceInOf( + address firstPoolBorrower = morpho.getHead( _poolToken, - morpho.getHead(_poolToken, Types.PositionType.BORROWERS_ON_POOL) - ).onPool; - - if (firstPoolBorrowerBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolBorrowerBalance.mul(indexes.poolBorrowIndex), - _amount - ); - - supplyBalance.inP2P += matchedP2P.div(indexes.p2pSupplyIndex); - _amount -= matchedP2P; - } + Types.PositionType.BORROWERS_ON_POOL + ); + uint256 firstPoolBorrowerBalance = morpho + .borrowBalanceInOf(_poolToken, firstPoolBorrower) + .onPool; + + uint256 matchedP2P = Math.min( + firstPoolBorrowerBalance.mul(indexes.poolBorrowIndex), + _amount + ); + + supplyBalance.inP2P += matchedP2P.div(indexes.p2pSupplyIndex); + repaidToPool += matchedP2P; + _amount -= matchedP2P; } } @@ -78,7 +82,9 @@ abstract contract RatesLens is UsersLens { (nextSupplyRatePerBlock, totalBalance) = _getUserSupplyRatePerBlock( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + _amount, + repaidToPool ); } @@ -109,33 +115,37 @@ abstract contract RatesLens is UsersLens { (Types.Delta memory delta, Types.Indexes memory indexes) = _getIndexes(_poolToken, true); Types.BorrowBalance memory borrowBalance = morpho.borrowBalanceInOf(_poolToken, _user); + + uint256 withdrawnFromPool; if (!morpho.p2pDisabled(_poolToken)) { - if (_amount > 0 && delta.p2pSupplyDelta > 0) { + if (delta.p2pSupplyDelta > 0) { uint256 matchedDelta = Math.min( delta.p2pSupplyDelta.mul(indexes.poolSupplyIndex), _amount ); borrowBalance.inP2P += matchedDelta.div(indexes.p2pBorrowIndex); + withdrawnFromPool += matchedDelta; _amount -= matchedDelta; } if (_amount > 0) { - uint256 firstPoolSupplierBalance = morpho - .supplyBalanceInOf( + address firstPoolSupplier = morpho.getHead( _poolToken, - morpho.getHead(_poolToken, Types.PositionType.SUPPLIERS_ON_POOL) - ).onPool; - - if (firstPoolSupplierBalance > 0) { - uint256 matchedP2P = Math.min( - firstPoolSupplierBalance.mul(indexes.poolSupplyIndex), - _amount - ); - - borrowBalance.inP2P += matchedP2P.div(indexes.p2pBorrowIndex); - _amount -= matchedP2P; - } + Types.PositionType.SUPPLIERS_ON_POOL + ); + uint256 firstPoolSupplierBalance = morpho + .supplyBalanceInOf(_poolToken, firstPoolSupplier) + .onPool; + + uint256 matchedP2P = Math.min( + firstPoolSupplierBalance.mul(indexes.poolSupplyIndex), + _amount + ); + + borrowBalance.inP2P += matchedP2P.div(indexes.p2pBorrowIndex); + withdrawnFromPool += matchedP2P; + _amount -= matchedP2P; } } @@ -147,7 +157,9 @@ abstract contract RatesLens is UsersLens { (nextBorrowRatePerBlock, totalBalance) = _getUserBorrowRatePerBlock( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + _amount, + withdrawnFromPool ); } @@ -168,7 +180,9 @@ abstract contract RatesLens is UsersLens { (supplyRatePerBlock, ) = _getUserSupplyRatePerBlock( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + 0, + 0 ); } @@ -189,7 +203,9 @@ abstract contract RatesLens is UsersLens { (borrowRatePerBlock, ) = _getUserBorrowRatePerBlock( _poolToken, balanceInP2P, - balanceOnPool + balanceOnPool, + 0, + 0 ); } @@ -311,7 +327,45 @@ abstract contract RatesLens is UsersLens { /// @return poolSupplyRate The market's pool supply rate per block (in wad). /// @return poolBorrowRate The market's pool borrow rate per block (in wad). function getRatesPerBlock(address _poolToken) - public + external + view + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return _getRatesPerBlock(_poolToken, 0, 0, 0, 0); + } + + /// INTERNAL /// + + struct PoolRatesVars { + Types.Delta delta; + Types.LastPoolIndexes lastPoolIndexes; + Types.Indexes indexes; + Types.MarketParameters params; + } + + /// @dev Computes and returns peer-to-peer and pool rates for a specific market. + /// @param _poolToken The market address. + /// @param _suppliedOnPool The amount hypothetically supplied to the underlying's pool (in underlying). + /// @param _borrowedFromPool The amount hypothetically borrowed from the underlying's pool (in underlying). + /// @param _repaidOnPool The amount hypothetically repaid to the underlying's pool (in underlying). + /// @param _withdrawnFromPool The amount hypothetically withdrawn from the underlying's pool (in underlying). + /// @return p2pSupplyRate The market's peer-to-peer supply rate per block (in wad). + /// @return p2pBorrowRate The market's peer-to-peer borrow rate per block (in wad). + /// @return poolSupplyRate The market's pool supply rate per block (in wad). + /// @return poolBorrowRate The market's pool borrow rate per block (in wad). + function _getRatesPerBlock( + address _poolToken, + uint256 _suppliedOnPool, + uint256 _borrowedFromPool, + uint256 _repaidOnPool, + uint256 _withdrawnFromPool + ) + internal view returns ( uint256 p2pSupplyRate, @@ -320,29 +374,67 @@ abstract contract RatesLens is UsersLens { uint256 poolBorrowRate ) { - ICToken cToken = ICToken(_poolToken); + PoolRatesVars memory ratesVars; + PoolInterestsVars memory interestsVars; + + ratesVars.delta = morpho.deltas(_poolToken); + ratesVars.lastPoolIndexes = morpho.lastPoolIndexes(_poolToken); + + ( + ratesVars.indexes.poolSupplyIndex, + ratesVars.indexes.poolBorrowIndex, + interestsVars + ) = _accruePoolInterests(ICToken(_poolToken)); + + interestsVars.cash = + interestsVars.cash + + _suppliedOnPool + + _repaidOnPool - + _borrowedFromPool - + _withdrawnFromPool; + interestsVars.totalBorrows = interestsVars.totalBorrows + _borrowedFromPool - _repaidOnPool; + + IInterestRateModel interestRateModel = ICToken(_poolToken).interestRateModel(); + poolSupplyRate = interestRateModel.getSupplyRate( + interestsVars.cash, + interestsVars.totalBorrows, + interestsVars.totalReserves, + interestsVars.reserveFactorMantissa + ); + poolBorrowRate = interestRateModel.getBorrowRate( + interestsVars.cash, + interestsVars.totalBorrows, + interestsVars.totalReserves + ); - poolSupplyRate = cToken.supplyRatePerBlock(); - poolBorrowRate = cToken.borrowRatePerBlock(); + ( + ratesVars.indexes.p2pSupplyIndex, + ratesVars.indexes.p2pBorrowIndex, + ratesVars.params + ) = _computeP2PIndexes( + _poolToken, + true, + ratesVars.indexes.poolSupplyIndex, + ratesVars.indexes.poolBorrowIndex, + ratesVars.delta, + ratesVars.lastPoolIndexes + ); - Types.MarketParameters memory marketParams = morpho.marketParameters(_poolToken); uint256 p2pRate = PercentageMath.weightedAvg( poolSupplyRate, poolBorrowRate, - marketParams.p2pIndexCursor + ratesVars.params.p2pIndexCursor ); - (Types.Delta memory delta, Types.Indexes memory indexes) = _getIndexes(_poolToken, false); - p2pSupplyRate = InterestRatesModel.computeP2PSupplyRatePerBlock( InterestRatesModel.P2PRateComputeParams({ p2pRate: p2pRate, poolRate: poolSupplyRate, - poolIndex: indexes.poolSupplyIndex, - p2pIndex: indexes.p2pSupplyIndex, - p2pDelta: delta.p2pSupplyDelta, - p2pAmount: delta.p2pSupplyAmount, - reserveFactor: marketParams.reserveFactor + poolIndex: ratesVars.indexes.poolSupplyIndex, + p2pIndex: ratesVars.indexes.p2pSupplyIndex, + p2pDelta: ratesVars.delta.p2pSupplyDelta, + p2pAmount: ratesVars.delta.p2pSupplyAmount, + reserveFactor: ratesVars.params.reserveFactor }) ); @@ -350,18 +442,16 @@ abstract contract RatesLens is UsersLens { InterestRatesModel.P2PRateComputeParams({ p2pRate: p2pRate, poolRate: poolBorrowRate, - poolIndex: indexes.poolBorrowIndex, - p2pIndex: indexes.p2pBorrowIndex, - p2pDelta: delta.p2pBorrowDelta, - p2pAmount: delta.p2pBorrowAmount, - reserveFactor: marketParams.reserveFactor + poolIndex: ratesVars.indexes.poolBorrowIndex, + p2pIndex: ratesVars.indexes.p2pBorrowIndex, + p2pDelta: ratesVars.delta.p2pBorrowDelta, + p2pAmount: ratesVars.delta.p2pBorrowAmount, + reserveFactor: ratesVars.params.reserveFactor }) ); } - /// INTERNAL /// - - /// @notice Computes and returns the total distribution of supply for a given market, using virtually updated indexes. + /// @dev Computes and returns the total distribution of supply for a given market, using virtually updated indexes. /// @param _poolToken The address of the market to check. /// @param _p2pSupplyIndex The given market's peer-to-peer supply index. /// @param _poolSupplyIndex The given market's pool supply index. @@ -380,7 +470,7 @@ abstract contract RatesLens is UsersLens { poolSupplyAmount = ICToken(_poolToken).balanceOf(address(morpho)).mul(_poolSupplyIndex); } - /// @notice Computes and returns the total distribution of borrows for a given market, using virtually updated indexes. + /// @dev Computes and returns the total distribution of borrows for a given market, using virtually updated indexes. /// @param _poolToken The address of the market to check. /// @param _p2pBorrowIndex The given market's peer-to-peer borrow index. /// @param _poolBorrowIndex The given market's borrow index. @@ -403,33 +493,55 @@ abstract contract RatesLens is UsersLens { } /// @dev Returns the supply rate per block experienced on a market based on a given position distribution. + /// The calculation takes into account the change in pool rates implied by an hypothetical supply and/or repay. /// @param _poolToken The address of the market. /// @param _balanceOnPool The amount of balance supplied on pool (in a unit common to `_balanceInP2P`). /// @param _balanceInP2P The amount of balance matched peer-to-peer (in a unit common to `_balanceOnPool`). + /// @param _suppliedOnPool The amount hypothetically supplied on pool (in underlying). + /// @param _repaidToPool The amount hypothetically repaid to the pool (in underlying). /// @return The supply rate per block experienced by the given position (in wad). /// @return The sum of peer-to-peer & pool balances. function _getUserSupplyRatePerBlock( address _poolToken, uint256 _balanceInP2P, - uint256 _balanceOnPool + uint256 _balanceOnPool, + uint256 _suppliedOnPool, + uint256 _repaidToPool ) internal view returns (uint256, uint256) { - (uint256 p2pSupplyRate, , uint256 poolSupplyRate, ) = getRatesPerBlock(_poolToken); + (uint256 p2pSupplyRate, , uint256 poolSupplyRate, ) = _getRatesPerBlock( + _poolToken, + _suppliedOnPool, + 0, + 0, + _repaidToPool + ); return _getWeightedRate(p2pSupplyRate, poolSupplyRate, _balanceInP2P, _balanceOnPool); } /// @dev Returns the borrow rate per block experienced on a market based on a given position distribution. + /// The calculation takes into account the change in pool rates implied by an hypothetical borrow and/or withdraw. /// @param _poolToken The address of the market. /// @param _balanceOnPool The amount of balance supplied on pool (in a unit common to `_balanceInP2P`). /// @param _balanceInP2P The amount of balance matched peer-to-peer (in a unit common to `_balanceOnPool`). + /// @param _borrowedFromPool The amount hypothetically borrowed from the pool (in underlying). + /// @param _withdrawnFromPool The amount hypothetically withdrawn from the pool (in underlying). /// @return The borrow rate per block experienced by the given position (in wad). /// @return The sum of peer-to-peer & pool balances. function _getUserBorrowRatePerBlock( address _poolToken, uint256 _balanceInP2P, - uint256 _balanceOnPool + uint256 _balanceOnPool, + uint256 _borrowedFromPool, + uint256 _withdrawnFromPool ) internal view returns (uint256, uint256) { - (, uint256 p2pBorrowRate, , uint256 poolBorrowRate) = getRatesPerBlock(_poolToken); + (, uint256 p2pBorrowRate, , uint256 poolBorrowRate) = _getRatesPerBlock( + _poolToken, + 0, + _borrowedFromPool, + _withdrawnFromPool, + 0 + ); return _getWeightedRate(p2pBorrowRate, poolBorrowRate, _balanceInP2P, _balanceOnPool); } diff --git a/src/compound/lens/UsersLens.sol b/src/compound/lens/UsersLens.sol index 275a35deb..c19738376 100644 --- a/src/compound/lens/UsersLens.sol +++ b/src/compound/lens/UsersLens.sol @@ -394,7 +394,7 @@ abstract contract UsersLens is IndexesLens { (, assetData.collateralFactor, ) = comptroller.markets(_poolToken); - Types.Indexes memory indexes = getIndexes(_poolToken, _getUpdatedIndexes); + (, Types.Indexes memory indexes) = _getIndexes(_poolToken, _getUpdatedIndexes); assetData.collateralUsd = _getUserSupplyBalanceInOf( _poolToken, From 8e5277f2d168459a3b7f145560c8dc629e767a0e Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 11:34:42 +0100 Subject: [PATCH 068/105] Added upgrade test for morpho-compound --- .../interfaces/compound/ICompound.sol | 10 +-- src/compound/lens/RatesLens.sol | 20 ++++-- test/compound/TestRatesLens.t.sol | 2 + test/prod/compound/TestLifecycle.t.sol | 9 +-- test/prod/compound/TestUpgradeLens.t.sol | 69 +++++++++++++++++++ test/prod/compound/setup/TestSetup.sol | 2 +- 6 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/compound/interfaces/compound/ICompound.sol b/src/compound/interfaces/compound/ICompound.sol index 32b78af6a..2e2c15ecc 100644 --- a/src/compound/interfaces/compound/ICompound.sol +++ b/src/compound/interfaces/compound/ICompound.sol @@ -248,17 +248,17 @@ interface IComptroller { } interface IInterestRateModel { - function getBorrowRate( + function getSupplyRate( uint256 cash, uint256 borrows, - uint256 reserves + uint256 reserves, + uint256 reserveFactorMantissa ) external view returns (uint256); - function getSupplyRate( + function getBorrowRate( uint256 cash, uint256 borrows, - uint256 reserves, - uint256 reserveFactorMantissa + uint256 reserves ) external view returns (uint256); } diff --git a/src/compound/lens/RatesLens.sol b/src/compound/lens/RatesLens.sol index 76e517c1d..505c33ea7 100644 --- a/src/compound/lens/RatesLens.sol +++ b/src/compound/lens/RatesLens.sol @@ -11,6 +11,10 @@ abstract contract RatesLens is UsersLens { using CompoundMath for uint256; using Math for uint256; + /// ERRORS /// + + error BorrowRateFailed(); + /// EXTERNAL /// /// @notice Returns the supply rate per block experienced on a market after having supplied the given amount on behalf of the given user. @@ -395,17 +399,23 @@ abstract contract RatesLens is UsersLens { interestsVars.totalBorrows = interestsVars.totalBorrows + _borrowedFromPool - _repaidOnPool; IInterestRateModel interestRateModel = ICToken(_poolToken).interestRateModel(); - poolSupplyRate = interestRateModel.getSupplyRate( + poolSupplyRate = IInterestRateModel(interestRateModel).getSupplyRate( interestsVars.cash, interestsVars.totalBorrows, interestsVars.totalReserves, interestsVars.reserveFactorMantissa ); - poolBorrowRate = interestRateModel.getBorrowRate( - interestsVars.cash, - interestsVars.totalBorrows, - interestsVars.totalReserves + (bool success, bytes memory result) = address(interestRateModel).staticcall( + abi.encodeWithSelector( + IInterestRateModel.getBorrowRate.selector, + interestsVars.cash, + interestsVars.totalBorrows, + interestsVars.totalReserves + ) ); + if (!success) revert BorrowRateFailed(); + if (result.length > 32) (, poolBorrowRate) = abi.decode(result, (uint256, uint256)); + else poolBorrowRate = abi.decode(result, (uint256)); ( ratesVars.indexes.p2pSupplyIndex, diff --git a/test/compound/TestRatesLens.t.sol b/test/compound/TestRatesLens.t.sol index 47823daf8..318b4eb87 100644 --- a/test/compound/TestRatesLens.t.sol +++ b/test/compound/TestRatesLens.t.sol @@ -7,6 +7,8 @@ contract TestRatesLens is TestSetup { using CompoundMath for uint256; function testGetRatesPerBlock() public { + supplier1.compoundSupply(cDai, 1 ether); // Update pool rates. + hevm.roll(block.number + 1_000); ( uint256 p2pSupplyRate, diff --git a/test/prod/compound/TestLifecycle.t.sol b/test/prod/compound/TestLifecycle.t.sol index e75e73c0b..76f778fca 100644 --- a/test/prod/compound/TestLifecycle.t.sol +++ b/test/prod/compound/TestLifecycle.t.sol @@ -393,14 +393,11 @@ contract TestLifecycle is TestSetup { if (supplyMarket.status.isSupplyPaused) continue; - uint256 borrowAmount = _boundBorrowAmount( - borrowMarket, - _amount, - oracle.getUnderlyingPrice(borrowMarket.poolToken) - ); + uint256 borrowedPrice = oracle.getUnderlyingPrice(borrowMarket.poolToken); + uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( borrowAmount, - oracle.getUnderlyingPrice(borrowMarket.poolToken), + borrowedPrice, oracle.getUnderlyingPrice(supplyMarket.poolToken), supplyMarket.collateralFactor ).mul(1.001 ether); diff --git a/test/prod/compound/TestUpgradeLens.t.sol b/test/prod/compound/TestUpgradeLens.t.sol index ba22a933d..f64909a10 100644 --- a/test/prod/compound/TestUpgradeLens.t.sol +++ b/test/prod/compound/TestUpgradeLens.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import "./setup/TestSetup.sol"; contract TestUpgradeLens is TestSetup { + using CompoundMath for uint256; + function testShouldPreserveOutdatedIndexes() public { Types.Indexes[] memory expectedIndexes = new Types.Indexes[](markets.length); @@ -77,4 +79,71 @@ contract TestUpgradeLens is TestSetup { ); } } + + function testNextRateShouldMatchRateAfterInteraction(uint96 _amount) public { + _upgrade(); + + for ( + uint256 supplyMarketIndex; + supplyMarketIndex < collateralMarkets.length; + ++supplyMarketIndex + ) { + for ( + uint256 borrowMarketIndex; + borrowMarketIndex < borrowableMarkets.length; + ++borrowMarketIndex + ) { + _revert(); + + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + + uint256 borrowedPrice = oracle.getUnderlyingPrice(borrowMarket.poolToken); + uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); + uint256 supplyAmount = _getMinimumCollateralAmount( + borrowAmount, + borrowedPrice, + oracle.getUnderlyingPrice(supplyMarket.poolToken), + supplyMarket.collateralFactor + ).mul(1.001 ether); + + (uint256 expectedSupplyRate, , , ) = lens.getNextUserSupplyRatePerBlock( + supplyMarket.poolToken, + address(user), + supplyAmount + ); + + _tip(supplyMarket.underlying, address(user), supplyAmount); + + user.approve(supplyMarket.underlying, supplyAmount); + user.supply(supplyMarket.poolToken, address(user), supplyAmount); + + assertApproxEqAbs( + lens.getCurrentUserSupplyRatePerBlock(supplyMarket.poolToken, address(user)), + expectedSupplyRate, + 1e21, + string.concat(supplyMarket.symbol, " supply rate") + ); + + if (borrowMarket.status.isBorrowPaused) continue; + + (uint256 expectedBorrowRate, , , ) = lens.getNextUserBorrowRatePerBlock( + borrowMarket.poolToken, + address(user), + borrowAmount + ); + + user.borrow(borrowMarket.poolToken, borrowAmount); + + assertApproxEqAbs( + lens.getCurrentUserBorrowRatePerBlock(borrowMarket.poolToken, address(user)), + expectedBorrowRate, + 1e21, + string.concat(borrowMarket.symbol, " borrow rate") + ); + } + } + } } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index eaface0e4..7041cb4d7 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -124,7 +124,7 @@ contract TestSetup is Config, Test { vm.label(address(cUsdc), "cUSDC"); vm.label(address(cUsdt), "cUSDT"); vm.label(address(cWbtc2), "cWBTC"); - vm.label(address(cEth), "cWETH"); + vm.label(address(cEth), "cETH"); vm.label(address(cComp), "cCOMP"); vm.label(address(cBat), "cBAT"); vm.label(address(cTusd), "cTUSD"); From 89df889438f8b1c3bbc106cb46274c49a604cd9f Mon Sep 17 00:00:00 2001 From: spalen0 Date: Wed, 11 Jan 2023 13:17:34 +0100 Subject: [PATCH 069/105] Gas optimisation --- src/aave-v2/libraries/InterestRatesModel.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/libraries/InterestRatesModel.sol b/src/aave-v2/libraries/InterestRatesModel.sol index 4ed5c333f..dd00d2b67 100644 --- a/src/aave-v2/libraries/InterestRatesModel.sol +++ b/src/aave-v2/libraries/InterestRatesModel.sol @@ -119,15 +119,15 @@ library InterestRatesModel { if (_params.poolSupplyRatePerYear > _params.poolBorrowRatePerYear) { p2pSupplyRate = _params.poolBorrowRatePerYear; // The p2pSupplyRate is set to the poolBorrowRatePerYear because there is no rate spread. } else { - uint256 p2pRate = PercentageMath.weightedAvg( + p2pSupplyRate = PercentageMath.weightedAvg( _params.poolSupplyRatePerYear, _params.poolBorrowRatePerYear, _params.p2pIndexCursor ); p2pSupplyRate = - p2pRate - - (p2pRate - _params.poolSupplyRatePerYear).percentMul(_params.reserveFactor); + p2pSupplyRate - + (p2pSupplyRate - _params.poolSupplyRatePerYear).percentMul(_params.reserveFactor); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { @@ -155,15 +155,15 @@ library InterestRatesModel { if (_params.poolSupplyRatePerYear > _params.poolBorrowRatePerYear) { p2pBorrowRate = _params.poolBorrowRatePerYear; // The p2pBorrowRate is set to the poolBorrowRatePerYear because there is no rate spread. } else { - uint256 p2pRate = PercentageMath.weightedAvg( + p2pBorrowRate = PercentageMath.weightedAvg( _params.poolSupplyRatePerYear, _params.poolBorrowRatePerYear, _params.p2pIndexCursor ); p2pBorrowRate = - p2pRate + - (_params.poolBorrowRatePerYear - p2pRate).percentMul(_params.reserveFactor); + p2pBorrowRate + + (_params.poolBorrowRatePerYear - p2pBorrowRate).percentMul(_params.reserveFactor); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { From c0c6a37bb254aa4c1adcb71807a765a980da0966 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 14:15:47 +0100 Subject: [PATCH 070/105] Fixed rates tests --- src/compound/lens/RatesLens.sol | 82 +++++++++++++++---------- test/compound/TestRatesLens.t.sol | 22 +++---- test/prod/aave-v2/TestUpgradeLens.t.sol | 4 +- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/compound/lens/RatesLens.sol b/src/compound/lens/RatesLens.sol index 505c33ea7..3db892bd6 100644 --- a/src/compound/lens/RatesLens.sol +++ b/src/compound/lens/RatesLens.sol @@ -379,43 +379,61 @@ abstract contract RatesLens is UsersLens { ) { PoolRatesVars memory ratesVars; - PoolInterestsVars memory interestsVars; ratesVars.delta = morpho.deltas(_poolToken); ratesVars.lastPoolIndexes = morpho.lastPoolIndexes(_poolToken); - ( - ratesVars.indexes.poolSupplyIndex, - ratesVars.indexes.poolBorrowIndex, - interestsVars - ) = _accruePoolInterests(ICToken(_poolToken)); - - interestsVars.cash = - interestsVars.cash + - _suppliedOnPool + - _repaidOnPool - - _borrowedFromPool - - _withdrawnFromPool; - interestsVars.totalBorrows = interestsVars.totalBorrows + _borrowedFromPool - _repaidOnPool; - - IInterestRateModel interestRateModel = ICToken(_poolToken).interestRateModel(); - poolSupplyRate = IInterestRateModel(interestRateModel).getSupplyRate( - interestsVars.cash, - interestsVars.totalBorrows, - interestsVars.totalReserves, - interestsVars.reserveFactorMantissa - ); - (bool success, bytes memory result) = address(interestRateModel).staticcall( - abi.encodeWithSelector( - IInterestRateModel.getBorrowRate.selector, + bool updated = _suppliedOnPool > 0 || + _borrowedFromPool > 0 || + _repaidOnPool > 0 || + _withdrawnFromPool > 0; + if (updated) { + PoolInterestsVars memory interestsVars; + ( + ratesVars.indexes.poolSupplyIndex, + ratesVars.indexes.poolBorrowIndex, + interestsVars + ) = _accruePoolInterests(ICToken(_poolToken)); + + interestsVars.cash = + interestsVars.cash + + _suppliedOnPool + + _repaidOnPool - + _borrowedFromPool - + _withdrawnFromPool; + interestsVars.totalBorrows = + interestsVars.totalBorrows + + _borrowedFromPool - + _repaidOnPool; + + IInterestRateModel interestRateModel = ICToken(_poolToken).interestRateModel(); + + poolSupplyRate = IInterestRateModel(interestRateModel).getSupplyRate( interestsVars.cash, interestsVars.totalBorrows, - interestsVars.totalReserves - ) - ); - if (!success) revert BorrowRateFailed(); - if (result.length > 32) (, poolBorrowRate) = abi.decode(result, (uint256, uint256)); - else poolBorrowRate = abi.decode(result, (uint256)); + interestsVars.totalReserves, + interestsVars.reserveFactorMantissa + ); + + (bool success, bytes memory result) = address(interestRateModel).staticcall( + abi.encodeWithSelector( + IInterestRateModel.getBorrowRate.selector, + interestsVars.cash, + interestsVars.totalBorrows, + interestsVars.totalReserves + ) + ); + if (!success) revert BorrowRateFailed(); + + if (result.length > 32) (, poolBorrowRate) = abi.decode(result, (uint256, uint256)); + else poolBorrowRate = abi.decode(result, (uint256)); + } else { + ratesVars.indexes.poolSupplyIndex = ICToken(_poolToken).exchangeRateStored(); + ratesVars.indexes.poolBorrowIndex = ICToken(_poolToken).borrowIndex(); + + poolSupplyRate = ICToken(_poolToken).supplyRatePerBlock(); + poolBorrowRate = ICToken(_poolToken).borrowRatePerBlock(); + } ( ratesVars.indexes.p2pSupplyIndex, @@ -423,7 +441,7 @@ abstract contract RatesLens is UsersLens { ratesVars.params ) = _computeP2PIndexes( _poolToken, - true, + updated, ratesVars.indexes.poolSupplyIndex, ratesVars.indexes.poolBorrowIndex, ratesVars.delta, diff --git a/test/compound/TestRatesLens.t.sol b/test/compound/TestRatesLens.t.sol index 318b4eb87..d79d55a21 100644 --- a/test/compound/TestRatesLens.t.sol +++ b/test/compound/TestRatesLens.t.sol @@ -305,7 +305,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, expectedSupplyRatePerBlock, - 1, + 1e6, "unexpected supply rate per block" ); assertEq( @@ -337,7 +337,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerBlock, expectedBorrowRatePerBlock, - 1, + 1e6, "unexpected borrow rate per block" ); assertApproxEqAbs(balanceOnPool, amount, 1, "unexpected pool balance"); @@ -372,7 +372,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, p2pSupplyRatePerBlock, - 1, + 1e6, "unexpected supply rate per block" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); @@ -406,7 +406,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerBlock, p2pBorrowRatePerBlock, - 1, + 1e6, "unexpected borrow rate per block" ); assertApproxEqAbs(balanceOnPool, 0, 1e6, "unexpected pool balance"); // compound rounding error at supply @@ -442,7 +442,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, (p2pSupplyRatePerBlock + poolSupplyRatePerBlock) / 2, - 1, + 1e6, "unexpected supply rate per block" ); assertEq(balanceOnPool, expectedBalanceOnPool, "unexpected pool balance"); @@ -518,7 +518,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, expectedSupplyRatePerBlock, - 1, + 1e6, "unexpected supply rate per block" ); assertEq( @@ -556,7 +556,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerBlock, expectedBorrowRatePerBlock, - 1, + 1e6, "unexpected borrow rate per block" ); assertApproxEqAbs(balanceOnPool, amount, 1, "unexpected pool balance"); @@ -590,7 +590,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, p2pSupplyRatePerBlock, - 1, + 1e6, "unexpected supply rate per block" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); @@ -624,7 +624,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerBlock, p2pBorrowRatePerBlock, - 1, + 1e6, "unexpected borrow rate per block" ); assertApproxEqAbs(balanceOnPool, 0, 1e9, "unexpected pool balance"); // compound rounding errors @@ -664,7 +664,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( supplyRatePerBlock, p2pSupplyRatePerBlock, - 1, + 1e6, "unexpected supply rate per block" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); @@ -705,7 +705,7 @@ contract TestRatesLens is TestSetup { assertApproxEqAbs( borrowRatePerBlock, p2pBorrowRatePerBlock, - 1, + 1e6, "unexpected borrow rate per block" ); assertEq(balanceOnPool, 0, "unexpected pool balance"); diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index d46813998..03ae0c8bb 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -90,7 +90,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e21, + 1e22, string.concat(supplyMarket.symbol, " supply rate") ); @@ -107,7 +107,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e21, + 1e22, string.concat(borrowMarket.symbol, " borrow rate") ); } From c52442b27556faa228eda3149cc33c9c74783f6a Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 14:20:40 +0100 Subject: [PATCH 071/105] Improve IRM further --- src/aave-v2/libraries/InterestRatesModel.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aave-v2/libraries/InterestRatesModel.sol b/src/aave-v2/libraries/InterestRatesModel.sol index dd00d2b67..10212a5f2 100644 --- a/src/aave-v2/libraries/InterestRatesModel.sol +++ b/src/aave-v2/libraries/InterestRatesModel.sol @@ -125,9 +125,9 @@ library InterestRatesModel { _params.p2pIndexCursor ); - p2pSupplyRate = - p2pSupplyRate - - (p2pSupplyRate - _params.poolSupplyRatePerYear).percentMul(_params.reserveFactor); + p2pSupplyRate -= (p2pSupplyRate - _params.poolSupplyRatePerYear).percentMul( + _params.reserveFactor + ); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { @@ -161,9 +161,9 @@ library InterestRatesModel { _params.p2pIndexCursor ); - p2pBorrowRate = - p2pBorrowRate + - (_params.poolBorrowRatePerYear - p2pBorrowRate).percentMul(_params.reserveFactor); + p2pBorrowRate += (_params.poolBorrowRatePerYear - p2pBorrowRate).percentMul( + _params.reserveFactor + ); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { From d2f27da06501de9168b23bcbd74baebd9a245779 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 15:03:40 +0100 Subject: [PATCH 072/105] Externalize rewards lens logic --- config/eth-mainnet/compound/Config.sol | 2 + src/compound/lens/Lens.sol | 4 +- src/compound/lens/LensStorage.sol | 9 +- src/compound/lens/RewardsLens.sol | 119 ++------------- src/compound/lens/RewardsLensLogic.sol | 199 +++++++++++++++++++++++++ src/compound/lens/UsersLens.sol | 2 +- src/compound/lens/interfaces/ILens.sol | 1 - test/compound/TestUpgradeable.t.sol | 9 +- test/compound/setup/TestSetup.sol | 3 +- test/prod/compound/setup/TestSetup.sol | 1 + 10 files changed, 232 insertions(+), 117 deletions(-) create mode 100644 src/compound/lens/RewardsLensLogic.sol diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index 070a51e94..ad49f806b 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -9,6 +9,7 @@ import {IMorpho} from "src/compound/interfaces/IMorpho.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {RewardsLensLogic} from "src/compound/lens/RewardsLensLogic.sol"; import {RewardsManager} from "src/compound/RewardsManager.sol"; import {Lens} from "src/compound/lens/Lens.sol"; import {Morpho} from "src/compound/Morpho.sol"; @@ -54,4 +55,5 @@ contract Config is BaseConfig { RewardsManager public rewardsManager; IPositionsManager public positionsManager; IInterestRatesManager public interestRatesManager; + RewardsLensLogic public rewardsLensLogic; } diff --git a/src/compound/lens/Lens.sol b/src/compound/lens/Lens.sol index 712f51f39..20de83b91 100644 --- a/src/compound/lens/Lens.sol +++ b/src/compound/lens/Lens.sol @@ -13,8 +13,8 @@ contract Lens is RewardsLens { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _morpho The address of the main Morpho contract. - constructor(address _morpho) LensStorage(_morpho) {} + /// @param _rewardsLensLogic The address of the rewards lens logic. + constructor(address _rewardsLensLogic) LensStorage(_rewardsLensLogic) {} /// EXTERNAL /// diff --git a/src/compound/lens/LensStorage.sol b/src/compound/lens/LensStorage.sol index eda6428cd..4240b1a19 100644 --- a/src/compound/lens/LensStorage.sol +++ b/src/compound/lens/LensStorage.sol @@ -12,6 +12,7 @@ import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {ERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol"; +import {RewardsLensLogic} from "./RewardsLensLogic.sol"; /// @title LensStorage. /// @author Morpho Labs. @@ -28,6 +29,7 @@ abstract contract LensStorage is ILens, Initializable { IMorpho public immutable morpho; IComptroller public immutable comptroller; IRewardsManager public immutable rewardsManager; + RewardsLensLogic public immutable rewardsLensLogic; /// STORAGE /// @@ -38,9 +40,10 @@ abstract contract LensStorage is ILens, Initializable { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _morpho The address of the main Morpho contract. - constructor(address _morpho) { - morpho = IMorpho(_morpho); + /// @param _rewardsLensLogic The address of the rewards lens logic. + constructor(address _rewardsLensLogic) { + rewardsLensLogic = RewardsLensLogic(_rewardsLensLogic); + morpho = IMorpho(rewardsLensLogic.morpho()); comptroller = IComptroller(morpho.comptroller()); rewardsManager = IRewardsManager(morpho.rewardsManager()); } diff --git a/src/compound/lens/RewardsLens.sol b/src/compound/lens/RewardsLens.sol index 5b5bfe49c..78412cc97 100644 --- a/src/compound/lens/RewardsLens.sol +++ b/src/compound/lens/RewardsLens.sol @@ -6,15 +6,8 @@ import "./MarketsLens.sol"; /// @title RewardsLens. /// @author Morpho Labs. /// @custom:contact security@morpho.xyz -/// @notice Intermediary layer exposing endpoints to query live data related to the Morpho Protocol rewards distribution. +/// @notice Intermediary layer serving as proxy to lighten the bytecode weight of the Lens. abstract contract RewardsLens is MarketsLens { - using CompoundMath for uint256; - - /// ERRORS /// - - /// @notice Thrown when an invalid cToken address is passed to compute accrued rewards. - error InvalidPoolToken(); - /// EXTERNAL /// /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. @@ -23,36 +16,11 @@ abstract contract RewardsLens is MarketsLens { function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) external view - returns (uint256 unclaimedRewards) + returns (uint256) { - unclaimedRewards = rewardsManager.userUnclaimedCompRewards(_user); - - for (uint256 i; i < _poolTokens.length; ) { - address poolToken = _poolTokens[i]; - - (bool isListed, , ) = comptroller.markets(poolToken); - if (!isListed) revert InvalidPoolToken(); - - unclaimedRewards += - getAccruedSupplierComp( - _user, - poolToken, - morpho.supplyBalanceInOf(poolToken, _user).onPool - ) + - getAccruedBorrowerComp( - _user, - poolToken, - morpho.borrowBalanceInOf(poolToken, _user).onPool - ); - - unchecked { - ++i; - } - } + return rewardsLensLogic.getUserUnclaimedRewards(_poolTokens, _user); } - /// PUBLIC /// - /// @notice Returns the accrued COMP rewards of a user since the last update. /// @param _supplier The address of the supplier. /// @param _poolToken The cToken address. @@ -62,11 +30,8 @@ abstract contract RewardsLens is MarketsLens { address _supplier, address _poolToken, uint256 _balance - ) public view returns (uint256) { - uint256 supplierIndex = rewardsManager.compSupplierIndex(_poolToken, _supplier); - - if (supplierIndex == 0) return 0; - return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; + ) external view returns (uint256) { + return rewardsLensLogic.getAccruedSupplierComp(_supplier, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -78,12 +43,7 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return - getAccruedSupplierComp( - _supplier, - _poolToken, - morpho.supplyBalanceInOf(_poolToken, _supplier).onPool - ); + return rewardsLensLogic.getAccruedSupplierComp(_supplier, _poolToken); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -95,11 +55,8 @@ abstract contract RewardsLens is MarketsLens { address _borrower, address _poolToken, uint256 _balance - ) public view returns (uint256) { - uint256 borrowerIndex = rewardsManager.compBorrowerIndex(_poolToken, _borrower); - - if (borrowerIndex == 0) return 0; - return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; + ) external view returns (uint256) { + return rewardsLensLogic.getAccruedBorrowerComp(_borrower, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -111,70 +68,20 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return - getAccruedBorrowerComp( - _borrower, - _poolToken, - morpho.borrowBalanceInOf(_poolToken, _borrower).onPool - ); + return rewardsLensLogic.getAccruedBorrowerComp(_borrower, _poolToken); } /// @notice Returns the updated COMP supply index. /// @param _poolToken The cToken address. /// @return The updated COMP supply index. - function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localSupplyState = rewardsManager - .getLocalCompSupplyState(_poolToken); - - if (localSupplyState.block == block.number) return localSupplyState.index; - else { - IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( - _poolToken - ); - - uint256 deltaBlocks = block.number - supplyState.block; - uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); - - if (deltaBlocks > 0 && supplySpeed > 0) { - uint256 supplyTokens = ICToken(_poolToken).totalSupply(); - uint256 ratio = supplyTokens > 0 - ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens - : 0; - - return supplyState.index + ratio; - } - - return supplyState.index; - } + function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256) { + return rewardsLensLogic.getCurrentCompSupplyIndex(_poolToken); } /// @notice Returns the updated COMP borrow index. /// @param _poolToken The cToken address. /// @return The updated COMP borrow index. - function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localBorrowState = rewardsManager - .getLocalCompBorrowState(_poolToken); - - if (localBorrowState.block == block.number) return localBorrowState.index; - else { - IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( - _poolToken - ); - uint256 deltaBlocks = block.number - borrowState.block; - uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); - - if (deltaBlocks > 0 && borrowSpeed > 0) { - uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( - ICToken(_poolToken).borrowIndex() - ); - uint256 ratio = borrowAmount > 0 - ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount - : 0; - - return borrowState.index + ratio; - } - - return borrowState.index; - } + function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256) { + return rewardsLensLogic.getCurrentCompBorrowIndex(_poolToken); } } diff --git a/src/compound/lens/RewardsLensLogic.sol b/src/compound/lens/RewardsLensLogic.sol new file mode 100644 index 000000000..1c577a398 --- /dev/null +++ b/src/compound/lens/RewardsLensLogic.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.13; + +import "../interfaces/compound/ICompound.sol"; +import "../interfaces/IMorpho.sol"; + +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; + +/// @title RewardsLensLogic. +/// @author Morpho Labs. +/// @custom:contact security@morpho.xyz +/// @notice External logic layer called by the Lens to calculate rewards-related data. +contract RewardsLensLogic { + using CompoundMath for uint256; + + /// IMMUTABLES /// + + IMorpho public immutable morpho; + IComptroller internal immutable comptroller; + IRewardsManager internal immutable rewardsManager; + + /// ERRORS /// + + /// @notice Thrown when an invalid cToken address is passed to compute accrued rewards. + error InvalidPoolToken(); + + /// CONSTRUCTOR /// + + /// @notice Constructs the contract. + /// @param _morpho The address of the main Morpho contract. + constructor(address _morpho) { + morpho = IMorpho(_morpho); + comptroller = IComptroller(morpho.comptroller()); + rewardsManager = IRewardsManager(morpho.rewardsManager()); + } + + /// EXTERNAL /// + + /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. + /// @param _poolTokens The cToken addresses for which to compute the rewards. + /// @param _user The address of the user. + function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) + external + view + returns (uint256 unclaimedRewards) + { + unclaimedRewards = rewardsManager.userUnclaimedCompRewards(_user); + + for (uint256 i; i < _poolTokens.length; ) { + address poolToken = _poolTokens[i]; + + (bool isListed, , ) = comptroller.markets(poolToken); + if (!isListed) revert InvalidPoolToken(); + + unclaimedRewards += + getAccruedSupplierComp( + _user, + poolToken, + morpho.supplyBalanceInOf(poolToken, _user).onPool + ) + + getAccruedBorrowerComp( + _user, + poolToken, + morpho.borrowBalanceInOf(poolToken, _user).onPool + ); + + unchecked { + ++i; + } + } + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp(address _supplier, address _poolToken) + external + view + returns (uint256) + { + return + getAccruedSupplierComp( + _supplier, + _poolToken, + morpho.supplyBalanceInOf(_poolToken, _supplier).onPool + ); + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp(address _borrower, address _poolToken) + external + view + returns (uint256) + { + return + getAccruedBorrowerComp( + _borrower, + _poolToken, + morpho.borrowBalanceInOf(_poolToken, _borrower).onPool + ); + } + + /// PUBLIC /// + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp( + address _supplier, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 supplierIndex = rewardsManager.compSupplierIndex(_poolToken, _supplier); + + if (supplierIndex == 0) return 0; + return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp( + address _borrower, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 borrowerIndex = rewardsManager.compBorrowerIndex(_poolToken, _borrower); + + if (borrowerIndex == 0) return 0; + return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; + } + + /// @notice Returns the updated COMP supply index. + /// @param _poolToken The cToken address. + /// @return The updated COMP supply index. + function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localSupplyState = rewardsManager + .getLocalCompSupplyState(_poolToken); + + if (localSupplyState.block == block.number) return localSupplyState.index; + else { + IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( + _poolToken + ); + + uint256 deltaBlocks = block.number - supplyState.block; + uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); + + if (deltaBlocks > 0 && supplySpeed > 0) { + uint256 supplyTokens = ICToken(_poolToken).totalSupply(); + uint256 ratio = supplyTokens > 0 + ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens + : 0; + + return supplyState.index + ratio; + } + + return supplyState.index; + } + } + + /// @notice Returns the updated COMP borrow index. + /// @param _poolToken The cToken address. + /// @return The updated COMP borrow index. + function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localBorrowState = rewardsManager + .getLocalCompBorrowState(_poolToken); + + if (localBorrowState.block == block.number) return localBorrowState.index; + else { + IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( + _poolToken + ); + uint256 deltaBlocks = block.number - borrowState.block; + uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); + + if (deltaBlocks > 0 && borrowSpeed > 0) { + uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( + ICToken(_poolToken).borrowIndex() + ); + uint256 ratio = borrowAmount > 0 + ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount + : 0; + + return borrowState.index + ratio; + } + + return borrowState.index; + } + } +} diff --git a/src/compound/lens/UsersLens.sol b/src/compound/lens/UsersLens.sol index c19738376..716891c76 100644 --- a/src/compound/lens/UsersLens.sol +++ b/src/compound/lens/UsersLens.sol @@ -388,7 +388,7 @@ abstract contract UsersLens is IndexesLens { ICompoundOracle _oracle, uint256 _withdrawnAmount, uint256 _borrowedAmount - ) public view returns (Types.AssetLiquidityData memory assetData) { + ) internal view returns (Types.AssetLiquidityData memory assetData) { assetData.underlyingPrice = _oracle.getUnderlyingPrice(_poolToken); if (assetData.underlyingPrice == 0) revert CompoundOracleFailed(); diff --git a/src/compound/lens/interfaces/ILens.sol b/src/compound/lens/interfaces/ILens.sol index e6ad1d3f6..37ee021dc 100644 --- a/src/compound/lens/interfaces/ILens.sol +++ b/src/compound/lens/interfaces/ILens.sol @@ -40,7 +40,6 @@ interface ILens { /// MARKETS /// - /// @dev Deprecated. function isMarketCreated(address _poolToken) external view returns (bool); /// @dev Deprecated. diff --git a/test/compound/TestUpgradeable.t.sol b/test/compound/TestUpgradeable.t.sol index eadee9c2a..3dee8fc70 100644 --- a/test/compound/TestUpgradeable.t.sol +++ b/test/compound/TestUpgradeable.t.sol @@ -65,15 +65,18 @@ contract TestUpgradeable is TestSetup { /// Lens /// function testUpgradeLens() public { - _testUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); + _testUpgradeProxy(lensProxy, address(new Lens(address(rewardsLensLogic)))); } function testOnlyProxyOwnerCanUpgradeLens() public { - _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); + _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(rewardsLensLogic)))); } function testOnlyProxyOwnerCanUpgradeAndCallLens() public { - _testOnlyProxyOwnerCanUpgradeAndCallProxy(lensProxy, address(new Lens(address(morpho)))); + _testOnlyProxyOwnerCanUpgradeAndCallProxy( + lensProxy, + address(new Lens(address(rewardsLensLogic))) + ); } /// INTERNAL /// diff --git a/test/compound/setup/TestSetup.sol b/test/compound/setup/TestSetup.sol index a20089997..cf6ba6c41 100644 --- a/test/compound/setup/TestSetup.sol +++ b/test/compound/setup/TestSetup.sol @@ -107,7 +107,8 @@ contract TestSetup is Config, Utils { morpho.setRewardsManager(rewardsManager); - lensImplV1 = new Lens(address(morpho)); + rewardsLensLogic = new RewardsLensLogic(address(morpho)); + lensImplV1 = new Lens(address(rewardsLensLogic)); lensProxy = new TransparentUpgradeableProxy(address(lensImplV1), address(proxyAdmin), ""); lens = Lens(address(lensProxy)); } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index 7041cb4d7..2fa70b064 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -63,6 +63,7 @@ contract TestSetup is Config, Test { rewardsManager = RewardsManager(address(morpho.rewardsManager())); positionsManager = morpho.positionsManager(); interestRatesManager = morpho.interestRatesManager(); + rewardsLensLogic = RewardsLensLogic(address(lens.rewardsLensLogic())); rewardsManagerProxy = TransparentUpgradeableProxy(payable(address(rewardsManager))); } From 4421cfc1e6e8ec998655bea383e280ce9425b6c9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 15:24:04 +0100 Subject: [PATCH 073/105] Merge RewardsLensLogic into RewardsManager --- config/eth-mainnet/compound/Config.sol | 2 - src/compound/RewardsManager.sol | 183 ++++++++++++++++-- src/compound/interfaces/IRewardsManager.sol | 31 +++ src/compound/lens/Lens.sol | 4 +- src/compound/lens/LensStorage.sol | 9 +- src/compound/lens/RewardsLens.sol | 14 +- src/compound/lens/RewardsLensLogic.sol | 199 -------------------- src/compound/lens/interfaces/ILens.sol | 10 + test/compound/TestUpgradeable.t.sol | 9 +- test/compound/setup/TestSetup.sol | 3 +- test/prod/compound/setup/TestSetup.sol | 1 - 11 files changed, 224 insertions(+), 241 deletions(-) delete mode 100644 src/compound/lens/RewardsLensLogic.sol diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index ad49f806b..070a51e94 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -9,7 +9,6 @@ import {IMorpho} from "src/compound/interfaces/IMorpho.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import {RewardsLensLogic} from "src/compound/lens/RewardsLensLogic.sol"; import {RewardsManager} from "src/compound/RewardsManager.sol"; import {Lens} from "src/compound/lens/Lens.sol"; import {Morpho} from "src/compound/Morpho.sol"; @@ -55,5 +54,4 @@ contract Config is BaseConfig { RewardsManager public rewardsManager; IPositionsManager public positionsManager; IInterestRatesManager public interestRatesManager; - RewardsLensLogic public rewardsLensLogic; } diff --git a/src/compound/RewardsManager.sol b/src/compound/RewardsManager.sol index 4e573a6b7..82cbae7d5 100644 --- a/src/compound/RewardsManager.sol +++ b/src/compound/RewardsManager.sol @@ -61,26 +61,30 @@ contract RewardsManager is IRewardsManager, Initializable { /// EXTERNAL /// - /// @notice Returns the local COMP supply state. - /// @param _poolToken The cToken address. - /// @return The local COMP supply state. - function getLocalCompSupplyState(address _poolToken) + /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. + /// @param _poolTokens The cToken addresses for which to compute the rewards. + /// @param _user The address of the user. + function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) external view - returns (IComptroller.CompMarketState memory) + returns (uint256 unclaimedRewards) { - return localCompSupplyState[_poolToken]; - } + unclaimedRewards = userUnclaimedCompRewards[_user]; - /// @notice Returns the local COMP borrow state. - /// @param _poolToken The cToken address. - /// @return The local COMP borrow state. - function getLocalCompBorrowState(address _poolToken) - external - view - returns (IComptroller.CompMarketState memory) - { - return localCompBorrowState[_poolToken]; + for (uint256 i; i < _poolTokens.length; ) { + address poolToken = _poolTokens[i]; + + (bool isListed, , ) = comptroller.markets(poolToken); + if (!isListed) revert InvalidCToken(); + + unclaimedRewards += + getAccruedSupplierComp(_user, poolToken) + + getAccruedBorrowerComp(_user, poolToken); + + unchecked { + ++i; + } + } } /// @notice Accrues unclaimed COMP rewards for the given cToken addresses and returns the total COMP unclaimed rewards. @@ -123,6 +127,153 @@ contract RewardsManager is IRewardsManager, Initializable { userUnclaimedCompRewards[_user] += _accrueBorrowerComp(_user, _poolToken, _userBalance); } + /// PUBLIC /// + + /// @notice Returns the local COMP supply state. + /// @param _poolToken The cToken address. + /// @return The local COMP supply state. + function getLocalCompSupplyState(address _poolToken) + public + view + returns (IComptroller.CompMarketState memory) + { + return localCompSupplyState[_poolToken]; + } + + /// @notice Returns the local COMP borrow state. + /// @param _poolToken The cToken address. + /// @return The local COMP borrow state. + function getLocalCompBorrowState(address _poolToken) + public + view + returns (IComptroller.CompMarketState memory) + { + return localCompBorrowState[_poolToken]; + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp(address _supplier, address _poolToken) + public + view + returns (uint256) + { + return + getAccruedSupplierComp( + _supplier, + _poolToken, + morpho.supplyBalanceInOf(_poolToken, _supplier).onPool + ); + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp(address _borrower, address _poolToken) + public + view + returns (uint256) + { + return + getAccruedBorrowerComp( + _borrower, + _poolToken, + morpho.borrowBalanceInOf(_poolToken, _borrower).onPool + ); + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp( + address _supplier, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 supplierIndex = compSupplierIndex[_poolToken][_supplier]; + + if (supplierIndex == 0) return 0; + return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp( + address _borrower, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 borrowerIndex = compBorrowerIndex[_poolToken][_borrower]; + + if (borrowerIndex == 0) return 0; + return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; + } + + /// @notice Returns the updated COMP supply index. + /// @param _poolToken The cToken address. + /// @return The updated COMP supply index. + function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localSupplyState = localCompSupplyState[_poolToken]; + + if (localSupplyState.block == block.number) return localSupplyState.index; + else { + IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( + _poolToken + ); + + uint256 deltaBlocks = block.number - supplyState.block; + uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); + + if (deltaBlocks > 0 && supplySpeed > 0) { + uint256 supplyTokens = ICToken(_poolToken).totalSupply(); + uint256 ratio = supplyTokens > 0 + ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens + : 0; + + return supplyState.index + ratio; + } + + return supplyState.index; + } + } + + /// @notice Returns the updated COMP borrow index. + /// @param _poolToken The cToken address. + /// @return The updated COMP borrow index. + function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localBorrowState = localCompBorrowState[_poolToken]; + + if (localBorrowState.block == block.number) return localBorrowState.index; + else { + IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( + _poolToken + ); + uint256 deltaBlocks = block.number - borrowState.block; + uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); + + if (deltaBlocks > 0 && borrowSpeed > 0) { + uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( + ICToken(_poolToken).borrowIndex() + ); + uint256 ratio = borrowAmount > 0 + ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount + : 0; + + return borrowState.index + ratio; + } + + return borrowState.index; + } + } + /// INTERNAL /// /// @notice Accrues unclaimed COMP rewards for the cToken addresses and returns the total unclaimed COMP rewards. diff --git a/src/compound/interfaces/IRewardsManager.sol b/src/compound/interfaces/IRewardsManager.sol index 685982457..c891d1af8 100644 --- a/src/compound/interfaces/IRewardsManager.sol +++ b/src/compound/interfaces/IRewardsManager.sol @@ -35,4 +35,35 @@ interface IRewardsManager { address, uint256 ) external; + + function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) + external + view + returns (uint256 unclaimedRewards); + + function getAccruedSupplierComp( + address _supplier, + address _poolToken, + uint256 _balance + ) external view returns (uint256); + + function getAccruedBorrowerComp( + address _borrower, + address _poolToken, + uint256 _balance + ) external view returns (uint256); + + function getAccruedSupplierComp(address _supplier, address _poolToken) + external + view + returns (uint256); + + function getAccruedBorrowerComp(address _borrower, address _poolToken) + external + view + returns (uint256); + + function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256); + + function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256); } diff --git a/src/compound/lens/Lens.sol b/src/compound/lens/Lens.sol index 20de83b91..712f51f39 100644 --- a/src/compound/lens/Lens.sol +++ b/src/compound/lens/Lens.sol @@ -13,8 +13,8 @@ contract Lens is RewardsLens { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _rewardsLensLogic The address of the rewards lens logic. - constructor(address _rewardsLensLogic) LensStorage(_rewardsLensLogic) {} + /// @param _morpho The address of the main Morpho contract. + constructor(address _morpho) LensStorage(_morpho) {} /// EXTERNAL /// diff --git a/src/compound/lens/LensStorage.sol b/src/compound/lens/LensStorage.sol index 4240b1a19..eda6428cd 100644 --- a/src/compound/lens/LensStorage.sol +++ b/src/compound/lens/LensStorage.sol @@ -12,7 +12,6 @@ import "@morpho-dao/morpho-utils/math/PercentageMath.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {ERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol"; -import {RewardsLensLogic} from "./RewardsLensLogic.sol"; /// @title LensStorage. /// @author Morpho Labs. @@ -29,7 +28,6 @@ abstract contract LensStorage is ILens, Initializable { IMorpho public immutable morpho; IComptroller public immutable comptroller; IRewardsManager public immutable rewardsManager; - RewardsLensLogic public immutable rewardsLensLogic; /// STORAGE /// @@ -40,10 +38,9 @@ abstract contract LensStorage is ILens, Initializable { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _rewardsLensLogic The address of the rewards lens logic. - constructor(address _rewardsLensLogic) { - rewardsLensLogic = RewardsLensLogic(_rewardsLensLogic); - morpho = IMorpho(rewardsLensLogic.morpho()); + /// @param _morpho The address of the main Morpho contract. + constructor(address _morpho) { + morpho = IMorpho(_morpho); comptroller = IComptroller(morpho.comptroller()); rewardsManager = IRewardsManager(morpho.rewardsManager()); } diff --git a/src/compound/lens/RewardsLens.sol b/src/compound/lens/RewardsLens.sol index 78412cc97..c0f2dd16e 100644 --- a/src/compound/lens/RewardsLens.sol +++ b/src/compound/lens/RewardsLens.sol @@ -18,7 +18,7 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsLensLogic.getUserUnclaimedRewards(_poolTokens, _user); + return rewardsManager.getUserUnclaimedRewards(_poolTokens, _user); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -31,7 +31,7 @@ abstract contract RewardsLens is MarketsLens { address _poolToken, uint256 _balance ) external view returns (uint256) { - return rewardsLensLogic.getAccruedSupplierComp(_supplier, _poolToken, _balance); + return rewardsManager.getAccruedSupplierComp(_supplier, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -43,7 +43,7 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsLensLogic.getAccruedSupplierComp(_supplier, _poolToken); + return rewardsManager.getAccruedSupplierComp(_supplier, _poolToken); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -56,7 +56,7 @@ abstract contract RewardsLens is MarketsLens { address _poolToken, uint256 _balance ) external view returns (uint256) { - return rewardsLensLogic.getAccruedBorrowerComp(_borrower, _poolToken, _balance); + return rewardsManager.getAccruedBorrowerComp(_borrower, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -68,20 +68,20 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsLensLogic.getAccruedBorrowerComp(_borrower, _poolToken); + return rewardsManager.getAccruedBorrowerComp(_borrower, _poolToken); } /// @notice Returns the updated COMP supply index. /// @param _poolToken The cToken address. /// @return The updated COMP supply index. function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256) { - return rewardsLensLogic.getCurrentCompSupplyIndex(_poolToken); + return rewardsManager.getCurrentCompSupplyIndex(_poolToken); } /// @notice Returns the updated COMP borrow index. /// @param _poolToken The cToken address. /// @return The updated COMP borrow index. function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256) { - return rewardsLensLogic.getCurrentCompBorrowIndex(_poolToken); + return rewardsManager.getCurrentCompBorrowIndex(_poolToken); } } diff --git a/src/compound/lens/RewardsLensLogic.sol b/src/compound/lens/RewardsLensLogic.sol deleted file mode 100644 index 1c577a398..000000000 --- a/src/compound/lens/RewardsLensLogic.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.13; - -import "../interfaces/compound/ICompound.sol"; -import "../interfaces/IMorpho.sol"; - -import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; - -/// @title RewardsLensLogic. -/// @author Morpho Labs. -/// @custom:contact security@morpho.xyz -/// @notice External logic layer called by the Lens to calculate rewards-related data. -contract RewardsLensLogic { - using CompoundMath for uint256; - - /// IMMUTABLES /// - - IMorpho public immutable morpho; - IComptroller internal immutable comptroller; - IRewardsManager internal immutable rewardsManager; - - /// ERRORS /// - - /// @notice Thrown when an invalid cToken address is passed to compute accrued rewards. - error InvalidPoolToken(); - - /// CONSTRUCTOR /// - - /// @notice Constructs the contract. - /// @param _morpho The address of the main Morpho contract. - constructor(address _morpho) { - morpho = IMorpho(_morpho); - comptroller = IComptroller(morpho.comptroller()); - rewardsManager = IRewardsManager(morpho.rewardsManager()); - } - - /// EXTERNAL /// - - /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. - /// @param _poolTokens The cToken addresses for which to compute the rewards. - /// @param _user The address of the user. - function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) - external - view - returns (uint256 unclaimedRewards) - { - unclaimedRewards = rewardsManager.userUnclaimedCompRewards(_user); - - for (uint256 i; i < _poolTokens.length; ) { - address poolToken = _poolTokens[i]; - - (bool isListed, , ) = comptroller.markets(poolToken); - if (!isListed) revert InvalidPoolToken(); - - unclaimedRewards += - getAccruedSupplierComp( - _user, - poolToken, - morpho.supplyBalanceInOf(poolToken, _user).onPool - ) + - getAccruedBorrowerComp( - _user, - poolToken, - morpho.borrowBalanceInOf(poolToken, _user).onPool - ); - - unchecked { - ++i; - } - } - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _supplier The address of the supplier. - /// @param _poolToken The cToken address. - /// @return The accrued COMP rewards. - function getAccruedSupplierComp(address _supplier, address _poolToken) - external - view - returns (uint256) - { - return - getAccruedSupplierComp( - _supplier, - _poolToken, - morpho.supplyBalanceInOf(_poolToken, _supplier).onPool - ); - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _borrower The address of the borrower. - /// @param _poolToken The cToken address. - /// @return The accrued COMP rewards. - function getAccruedBorrowerComp(address _borrower, address _poolToken) - external - view - returns (uint256) - { - return - getAccruedBorrowerComp( - _borrower, - _poolToken, - morpho.borrowBalanceInOf(_poolToken, _borrower).onPool - ); - } - - /// PUBLIC /// - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _supplier The address of the supplier. - /// @param _poolToken The cToken address. - /// @param _balance The user balance of tokens in the distribution. - /// @return The accrued COMP rewards. - function getAccruedSupplierComp( - address _supplier, - address _poolToken, - uint256 _balance - ) public view returns (uint256) { - uint256 supplierIndex = rewardsManager.compSupplierIndex(_poolToken, _supplier); - - if (supplierIndex == 0) return 0; - return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _borrower The address of the borrower. - /// @param _poolToken The cToken address. - /// @param _balance The user balance of tokens in the distribution. - /// @return The accrued COMP rewards. - function getAccruedBorrowerComp( - address _borrower, - address _poolToken, - uint256 _balance - ) public view returns (uint256) { - uint256 borrowerIndex = rewardsManager.compBorrowerIndex(_poolToken, _borrower); - - if (borrowerIndex == 0) return 0; - return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; - } - - /// @notice Returns the updated COMP supply index. - /// @param _poolToken The cToken address. - /// @return The updated COMP supply index. - function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localSupplyState = rewardsManager - .getLocalCompSupplyState(_poolToken); - - if (localSupplyState.block == block.number) return localSupplyState.index; - else { - IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( - _poolToken - ); - - uint256 deltaBlocks = block.number - supplyState.block; - uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); - - if (deltaBlocks > 0 && supplySpeed > 0) { - uint256 supplyTokens = ICToken(_poolToken).totalSupply(); - uint256 ratio = supplyTokens > 0 - ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens - : 0; - - return supplyState.index + ratio; - } - - return supplyState.index; - } - } - - /// @notice Returns the updated COMP borrow index. - /// @param _poolToken The cToken address. - /// @return The updated COMP borrow index. - function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localBorrowState = rewardsManager - .getLocalCompBorrowState(_poolToken); - - if (localBorrowState.block == block.number) return localBorrowState.index; - else { - IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( - _poolToken - ); - uint256 deltaBlocks = block.number - borrowState.block; - uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); - - if (deltaBlocks > 0 && borrowSpeed > 0) { - uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( - ICToken(_poolToken).borrowIndex() - ); - uint256 ratio = borrowAmount > 0 - ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount - : 0; - - return borrowState.index + ratio; - } - - return borrowState.index; - } - } -} diff --git a/src/compound/lens/interfaces/ILens.sol b/src/compound/lens/interfaces/ILens.sol index 37ee021dc..0dff67d6e 100644 --- a/src/compound/lens/interfaces/ILens.sol +++ b/src/compound/lens/interfaces/ILens.sol @@ -283,6 +283,16 @@ interface ILens { uint256 _balance ) external view returns (uint256); + function getAccruedSupplierComp(address _supplier, address _poolToken) + external + view + returns (uint256); + + function getAccruedBorrowerComp(address _borrower, address _poolToken) + external + view + returns (uint256); + function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256); function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256); diff --git a/test/compound/TestUpgradeable.t.sol b/test/compound/TestUpgradeable.t.sol index 3dee8fc70..eadee9c2a 100644 --- a/test/compound/TestUpgradeable.t.sol +++ b/test/compound/TestUpgradeable.t.sol @@ -65,18 +65,15 @@ contract TestUpgradeable is TestSetup { /// Lens /// function testUpgradeLens() public { - _testUpgradeProxy(lensProxy, address(new Lens(address(rewardsLensLogic)))); + _testUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); } function testOnlyProxyOwnerCanUpgradeLens() public { - _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(rewardsLensLogic)))); + _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); } function testOnlyProxyOwnerCanUpgradeAndCallLens() public { - _testOnlyProxyOwnerCanUpgradeAndCallProxy( - lensProxy, - address(new Lens(address(rewardsLensLogic))) - ); + _testOnlyProxyOwnerCanUpgradeAndCallProxy(lensProxy, address(new Lens(address(morpho)))); } /// INTERNAL /// diff --git a/test/compound/setup/TestSetup.sol b/test/compound/setup/TestSetup.sol index cf6ba6c41..a20089997 100644 --- a/test/compound/setup/TestSetup.sol +++ b/test/compound/setup/TestSetup.sol @@ -107,8 +107,7 @@ contract TestSetup is Config, Utils { morpho.setRewardsManager(rewardsManager); - rewardsLensLogic = new RewardsLensLogic(address(morpho)); - lensImplV1 = new Lens(address(rewardsLensLogic)); + lensImplV1 = new Lens(address(morpho)); lensProxy = new TransparentUpgradeableProxy(address(lensImplV1), address(proxyAdmin), ""); lens = Lens(address(lensProxy)); } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index 2fa70b064..7041cb4d7 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -63,7 +63,6 @@ contract TestSetup is Config, Test { rewardsManager = RewardsManager(address(morpho.rewardsManager())); positionsManager = morpho.positionsManager(); interestRatesManager = morpho.interestRatesManager(); - rewardsLensLogic = RewardsLensLogic(address(lens.rewardsLensLogic())); rewardsManagerProxy = TransparentUpgradeableProxy(payable(address(rewardsManager))); } From c88474f8564d563aa02d85255aef484cddc32213 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 15:37:15 +0100 Subject: [PATCH 074/105] Increase tolerance --- test/prod/aave-v2/TestDeltas.t.sol | 4 ++-- test/prod/aave-v2/TestUpgradeLens.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/prod/aave-v2/TestDeltas.t.sol b/test/prod/aave-v2/TestDeltas.t.sol index 237e79481..e27cdde60 100644 --- a/test/prod/aave-v2/TestDeltas.t.sol +++ b/test/prod/aave-v2/TestDeltas.t.sol @@ -108,14 +108,14 @@ contract TestDeltas is TestSetup { assertApproxEqAbs( p2pSupplyUnderlying - supplyDeltaUnderlyingBefore, IAToken(test.market.poolToken).balanceOf(address(morpho)) - test.morphoSupplyBefore, - 1, + 10, "morpho pool supply" ); assertApproxEqAbs( p2pBorrowUnderlying - borrowDeltaUnderlyingBefore, IVariableDebtToken(test.market.debtToken).balanceOf(address(morpho)) - test.morphoBorrowBefore, - 1, + 10, "morpho pool borrow" ); } diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index 03ae0c8bb..4fc4e5664 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -90,7 +90,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e22, + 1e23, string.concat(supplyMarket.symbol, " supply rate") ); @@ -107,7 +107,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e22, + 1e23, string.concat(borrowMarket.symbol, " borrow rate") ); } From ec038da8e14b2fd13a21f6bb51c3e09080802a98 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 15:44:51 +0100 Subject: [PATCH 075/105] Reverting IRM change for readability --- src/aave-v2/libraries/InterestRatesModel.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/aave-v2/libraries/InterestRatesModel.sol b/src/aave-v2/libraries/InterestRatesModel.sol index 10212a5f2..4ed5c333f 100644 --- a/src/aave-v2/libraries/InterestRatesModel.sol +++ b/src/aave-v2/libraries/InterestRatesModel.sol @@ -119,15 +119,15 @@ library InterestRatesModel { if (_params.poolSupplyRatePerYear > _params.poolBorrowRatePerYear) { p2pSupplyRate = _params.poolBorrowRatePerYear; // The p2pSupplyRate is set to the poolBorrowRatePerYear because there is no rate spread. } else { - p2pSupplyRate = PercentageMath.weightedAvg( + uint256 p2pRate = PercentageMath.weightedAvg( _params.poolSupplyRatePerYear, _params.poolBorrowRatePerYear, _params.p2pIndexCursor ); - p2pSupplyRate -= (p2pSupplyRate - _params.poolSupplyRatePerYear).percentMul( - _params.reserveFactor - ); + p2pSupplyRate = + p2pRate - + (p2pRate - _params.poolSupplyRatePerYear).percentMul(_params.reserveFactor); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { @@ -155,15 +155,15 @@ library InterestRatesModel { if (_params.poolSupplyRatePerYear > _params.poolBorrowRatePerYear) { p2pBorrowRate = _params.poolBorrowRatePerYear; // The p2pBorrowRate is set to the poolBorrowRatePerYear because there is no rate spread. } else { - p2pBorrowRate = PercentageMath.weightedAvg( + uint256 p2pRate = PercentageMath.weightedAvg( _params.poolSupplyRatePerYear, _params.poolBorrowRatePerYear, _params.p2pIndexCursor ); - p2pBorrowRate += (_params.poolBorrowRatePerYear - p2pBorrowRate).percentMul( - _params.reserveFactor - ); + p2pBorrowRate = + p2pRate + + (_params.poolBorrowRatePerYear - p2pRate).percentMul(_params.reserveFactor); } if (_params.p2pDelta > 0 && _params.p2pAmount > 0) { From 2a3bab9b681d264ccffd9fb24715a4743ac4bea3 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 16:10:09 +0100 Subject: [PATCH 076/105] Decrease upgrade test tolerance --- test/prod/compound/TestUpgradeLens.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/prod/compound/TestUpgradeLens.t.sol b/test/prod/compound/TestUpgradeLens.t.sol index f64909a10..509385f5a 100644 --- a/test/prod/compound/TestUpgradeLens.t.sol +++ b/test/prod/compound/TestUpgradeLens.t.sol @@ -123,7 +123,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserSupplyRatePerBlock(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e21, + 1, string.concat(supplyMarket.symbol, " supply rate") ); @@ -140,7 +140,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserBorrowRatePerBlock(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e21, + 1, string.concat(borrowMarket.symbol, " borrow rate") ); } From 255d98831de7740d3c828e0975c090360c822e32 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 11 Jan 2023 16:40:41 +0100 Subject: [PATCH 077/105] Increase tolerance to 10 bps --- src/aave-v2/lens/RatesLens.sol | 12 ++++++------ test/prod/aave-v2/TestUpgradeLens.t.sol | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index 7b607d0be..1aebc1363 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -371,8 +371,8 @@ abstract contract RatesLens is UsersLens { address _poolToken, uint256 _suppliedOnPool, uint256 _borrowedFromPool, - uint256 _repaidOnPool, - uint256 _withdrawnFromPool + uint256 _withdrawnFromPool, + uint256 _repaidOnPool ) internal view @@ -528,8 +528,8 @@ abstract contract RatesLens is UsersLens { _poolToken, _suppliedOnPool, 0, - _repaidToPool, - 0 + 0, + _repaidToPool ); return @@ -561,8 +561,8 @@ abstract contract RatesLens is UsersLens { _poolToken, 0, _borrowedFromPool, - 0, - _withdrawnFromPool + _withdrawnFromPool, + 0 ); return diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index 4fc4e5664..04994aff6 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -90,7 +90,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e23, + 1e24, string.concat(supplyMarket.symbol, " supply rate") ); @@ -107,7 +107,7 @@ contract TestUpgradeLens is TestSetup { assertApproxEqAbs( lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e23, + 1e24, string.concat(borrowMarket.symbol, " borrow rate") ); } From 2ece466cf5bba2f44f48b85545a79936b92b0ce4 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 12 Jan 2023 09:28:27 +0100 Subject: [PATCH 078/105] Improve tests --- test/prod/aave-v2/TestLifecycle.t.sol | 15 +++++++++------ test/prod/aave-v2/TestUpgradeLens.t.sol | 7 ++++--- test/prod/compound/TestLifecycle.t.sol | 15 +++++++++------ test/prod/compound/TestUpgradeLens.t.sol | 7 ++++--- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/test/prod/aave-v2/TestLifecycle.t.sol b/test/prod/aave-v2/TestLifecycle.t.sol index 67613bfff..906640115 100644 --- a/test/prod/aave-v2/TestLifecycle.t.sol +++ b/test/prod/aave-v2/TestLifecycle.t.sol @@ -310,6 +310,10 @@ contract TestLifecycle is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -317,11 +321,8 @@ contract TestLifecycle is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused) continue; - uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( @@ -360,6 +361,10 @@ contract TestLifecycle is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -367,11 +372,9 @@ contract TestLifecycle is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused || borrowMarket.status.isBorrowPaused) - continue; + if (borrowMarket.status.isBorrowPaused) continue; uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index 04994aff6..58371e58c 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -53,6 +53,10 @@ contract TestUpgradeLens is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -60,11 +64,8 @@ contract TestUpgradeLens is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused) continue; - uint256 borrowedPrice = oracle.getAssetPrice(borrowMarket.underlying); uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( diff --git a/test/prod/compound/TestLifecycle.t.sol b/test/prod/compound/TestLifecycle.t.sol index 76f778fca..669b4c116 100644 --- a/test/prod/compound/TestLifecycle.t.sol +++ b/test/prod/compound/TestLifecycle.t.sol @@ -381,6 +381,10 @@ contract TestLifecycle is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -388,11 +392,8 @@ contract TestLifecycle is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused) continue; - uint256 borrowedPrice = oracle.getUnderlyingPrice(borrowMarket.poolToken); uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( @@ -429,6 +430,10 @@ contract TestLifecycle is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -436,11 +441,9 @@ contract TestLifecycle is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused || borrowMarket.status.isBorrowPaused) - continue; + if (borrowMarket.status.isBorrowPaused) continue; uint256 borrowAmount = _boundBorrowAmount( borrowMarket, diff --git a/test/prod/compound/TestUpgradeLens.t.sol b/test/prod/compound/TestUpgradeLens.t.sol index 509385f5a..7bcbb5604 100644 --- a/test/prod/compound/TestUpgradeLens.t.sol +++ b/test/prod/compound/TestUpgradeLens.t.sol @@ -88,6 +88,10 @@ contract TestUpgradeLens is TestSetup { supplyMarketIndex < collateralMarkets.length; ++supplyMarketIndex ) { + TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; + + if (supplyMarket.status.isSupplyPaused) continue; + for ( uint256 borrowMarketIndex; borrowMarketIndex < borrowableMarkets.length; @@ -95,11 +99,8 @@ contract TestUpgradeLens is TestSetup { ) { _revert(); - TestMarket memory supplyMarket = collateralMarkets[supplyMarketIndex]; TestMarket memory borrowMarket = borrowableMarkets[borrowMarketIndex]; - if (supplyMarket.status.isSupplyPaused) continue; - uint256 borrowedPrice = oracle.getUnderlyingPrice(borrowMarket.poolToken); uint256 borrowAmount = _boundBorrowAmount(borrowMarket, _amount, borrowedPrice); uint256 supplyAmount = _getMinimumCollateralAmount( From 02bbe2b070402d56677bd3f01d129e42f7c2f562 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 12 Jan 2023 13:25:40 +0100 Subject: [PATCH 079/105] Externalize rewards lens to lens extension --- config/eth-mainnet/compound/Config.sol | 3 + src/compound/RewardsManager.sol | 183 ++--------------- src/compound/interfaces/IRewardsManager.sol | 31 --- src/compound/lens/Lens.sol | 4 +- src/compound/lens/LensExtension.sol | 192 ++++++++++++++++++ src/compound/lens/LensStorage.sol | 9 +- src/compound/lens/RewardsLens.sol | 14 +- .../lens/interfaces/ILensExtension.sol | 41 ++++ test/compound/TestUpgradeable.t.sol | 9 +- test/compound/setup/TestSetup.sol | 3 +- test/prod/compound/setup/TestSetup.sol | 4 +- 11 files changed, 278 insertions(+), 215 deletions(-) create mode 100644 src/compound/lens/LensExtension.sol create mode 100644 src/compound/lens/interfaces/ILensExtension.sol diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index 070a51e94..b602fb65e 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -10,6 +10,7 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transpa import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {RewardsManager} from "src/compound/RewardsManager.sol"; +import {LensExtension} from "src/compound/lens/LensExtension.sol"; import {Lens} from "src/compound/lens/Lens.sol"; import {Morpho} from "src/compound/Morpho.sol"; import {BaseConfig} from "../BaseConfig.sol"; @@ -50,6 +51,8 @@ contract Config is BaseConfig { RewardsManager public rewardsManagerImplV1; Lens public lens; + LensExtension public lensExtension; + Morpho public morpho; RewardsManager public rewardsManager; IPositionsManager public positionsManager; diff --git a/src/compound/RewardsManager.sol b/src/compound/RewardsManager.sol index 82cbae7d5..4e573a6b7 100644 --- a/src/compound/RewardsManager.sol +++ b/src/compound/RewardsManager.sol @@ -61,30 +61,26 @@ contract RewardsManager is IRewardsManager, Initializable { /// EXTERNAL /// - /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. - /// @param _poolTokens The cToken addresses for which to compute the rewards. - /// @param _user The address of the user. - function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) + /// @notice Returns the local COMP supply state. + /// @param _poolToken The cToken address. + /// @return The local COMP supply state. + function getLocalCompSupplyState(address _poolToken) external view - returns (uint256 unclaimedRewards) + returns (IComptroller.CompMarketState memory) { - unclaimedRewards = userUnclaimedCompRewards[_user]; - - for (uint256 i; i < _poolTokens.length; ) { - address poolToken = _poolTokens[i]; - - (bool isListed, , ) = comptroller.markets(poolToken); - if (!isListed) revert InvalidCToken(); - - unclaimedRewards += - getAccruedSupplierComp(_user, poolToken) + - getAccruedBorrowerComp(_user, poolToken); + return localCompSupplyState[_poolToken]; + } - unchecked { - ++i; - } - } + /// @notice Returns the local COMP borrow state. + /// @param _poolToken The cToken address. + /// @return The local COMP borrow state. + function getLocalCompBorrowState(address _poolToken) + external + view + returns (IComptroller.CompMarketState memory) + { + return localCompBorrowState[_poolToken]; } /// @notice Accrues unclaimed COMP rewards for the given cToken addresses and returns the total COMP unclaimed rewards. @@ -127,153 +123,6 @@ contract RewardsManager is IRewardsManager, Initializable { userUnclaimedCompRewards[_user] += _accrueBorrowerComp(_user, _poolToken, _userBalance); } - /// PUBLIC /// - - /// @notice Returns the local COMP supply state. - /// @param _poolToken The cToken address. - /// @return The local COMP supply state. - function getLocalCompSupplyState(address _poolToken) - public - view - returns (IComptroller.CompMarketState memory) - { - return localCompSupplyState[_poolToken]; - } - - /// @notice Returns the local COMP borrow state. - /// @param _poolToken The cToken address. - /// @return The local COMP borrow state. - function getLocalCompBorrowState(address _poolToken) - public - view - returns (IComptroller.CompMarketState memory) - { - return localCompBorrowState[_poolToken]; - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _supplier The address of the supplier. - /// @param _poolToken The cToken address. - /// @return The accrued COMP rewards. - function getAccruedSupplierComp(address _supplier, address _poolToken) - public - view - returns (uint256) - { - return - getAccruedSupplierComp( - _supplier, - _poolToken, - morpho.supplyBalanceInOf(_poolToken, _supplier).onPool - ); - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _borrower The address of the borrower. - /// @param _poolToken The cToken address. - /// @return The accrued COMP rewards. - function getAccruedBorrowerComp(address _borrower, address _poolToken) - public - view - returns (uint256) - { - return - getAccruedBorrowerComp( - _borrower, - _poolToken, - morpho.borrowBalanceInOf(_poolToken, _borrower).onPool - ); - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _supplier The address of the supplier. - /// @param _poolToken The cToken address. - /// @param _balance The user balance of tokens in the distribution. - /// @return The accrued COMP rewards. - function getAccruedSupplierComp( - address _supplier, - address _poolToken, - uint256 _balance - ) public view returns (uint256) { - uint256 supplierIndex = compSupplierIndex[_poolToken][_supplier]; - - if (supplierIndex == 0) return 0; - return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; - } - - /// @notice Returns the accrued COMP rewards of a user since the last update. - /// @param _borrower The address of the borrower. - /// @param _poolToken The cToken address. - /// @param _balance The user balance of tokens in the distribution. - /// @return The accrued COMP rewards. - function getAccruedBorrowerComp( - address _borrower, - address _poolToken, - uint256 _balance - ) public view returns (uint256) { - uint256 borrowerIndex = compBorrowerIndex[_poolToken][_borrower]; - - if (borrowerIndex == 0) return 0; - return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; - } - - /// @notice Returns the updated COMP supply index. - /// @param _poolToken The cToken address. - /// @return The updated COMP supply index. - function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localSupplyState = localCompSupplyState[_poolToken]; - - if (localSupplyState.block == block.number) return localSupplyState.index; - else { - IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( - _poolToken - ); - - uint256 deltaBlocks = block.number - supplyState.block; - uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); - - if (deltaBlocks > 0 && supplySpeed > 0) { - uint256 supplyTokens = ICToken(_poolToken).totalSupply(); - uint256 ratio = supplyTokens > 0 - ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens - : 0; - - return supplyState.index + ratio; - } - - return supplyState.index; - } - } - - /// @notice Returns the updated COMP borrow index. - /// @param _poolToken The cToken address. - /// @return The updated COMP borrow index. - function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { - IComptroller.CompMarketState memory localBorrowState = localCompBorrowState[_poolToken]; - - if (localBorrowState.block == block.number) return localBorrowState.index; - else { - IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( - _poolToken - ); - uint256 deltaBlocks = block.number - borrowState.block; - uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); - - if (deltaBlocks > 0 && borrowSpeed > 0) { - uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( - ICToken(_poolToken).borrowIndex() - ); - uint256 ratio = borrowAmount > 0 - ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount - : 0; - - return borrowState.index + ratio; - } - - return borrowState.index; - } - } - /// INTERNAL /// /// @notice Accrues unclaimed COMP rewards for the cToken addresses and returns the total unclaimed COMP rewards. diff --git a/src/compound/interfaces/IRewardsManager.sol b/src/compound/interfaces/IRewardsManager.sol index c891d1af8..685982457 100644 --- a/src/compound/interfaces/IRewardsManager.sol +++ b/src/compound/interfaces/IRewardsManager.sol @@ -35,35 +35,4 @@ interface IRewardsManager { address, uint256 ) external; - - function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) - external - view - returns (uint256 unclaimedRewards); - - function getAccruedSupplierComp( - address _supplier, - address _poolToken, - uint256 _balance - ) external view returns (uint256); - - function getAccruedBorrowerComp( - address _borrower, - address _poolToken, - uint256 _balance - ) external view returns (uint256); - - function getAccruedSupplierComp(address _supplier, address _poolToken) - external - view - returns (uint256); - - function getAccruedBorrowerComp(address _borrower, address _poolToken) - external - view - returns (uint256); - - function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256); - - function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256); } diff --git a/src/compound/lens/Lens.sol b/src/compound/lens/Lens.sol index 712f51f39..9820204a3 100644 --- a/src/compound/lens/Lens.sol +++ b/src/compound/lens/Lens.sol @@ -13,8 +13,8 @@ contract Lens is RewardsLens { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _morpho The address of the main Morpho contract. - constructor(address _morpho) LensStorage(_morpho) {} + /// @param _lensExtension The address of the Lens extension. + constructor(address _lensExtension) LensStorage(_lensExtension) {} /// EXTERNAL /// diff --git a/src/compound/lens/LensExtension.sol b/src/compound/lens/LensExtension.sol new file mode 100644 index 000000000..e36c8b03d --- /dev/null +++ b/src/compound/lens/LensExtension.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.13; + +import "../interfaces/IRewardsManager.sol"; +import "../interfaces/IMorpho.sol"; +import "./interfaces/ILensExtension.sol"; + +import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; + +/// @title LensExtension. +/// @author Morpho Labs. +/// @custom:contact security@morpho.xyz +/// @notice This contract is an extension of the Lens. +contract LensExtension is ILensExtension { + using CompoundMath for uint256; + + /// STORAGE /// + + IMorpho public immutable morpho; + IComptroller internal immutable comptroller; + IRewardsManager internal immutable rewardsManager; + + /// ERRORS /// + + /// @notice Thrown when an invalid cToken address is passed to claim rewards. + error InvalidCToken(); + + /// CONSTRUCTOR /// + + /// @notice Constructs the contract. + /// @param _morpho The address of the main Morpho contract. + constructor(address _morpho) { + morpho = IMorpho(_morpho); + comptroller = IComptroller(morpho.comptroller()); + rewardsManager = IRewardsManager(morpho.rewardsManager()); + } + + /// EXTERNAL /// + + /// @notice Returns the unclaimed COMP rewards for the given cToken addresses. + /// @param _poolTokens The cToken addresses for which to compute the rewards. + /// @param _user The address of the user. + function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) + external + view + returns (uint256 unclaimedRewards) + { + unclaimedRewards = rewardsManager.userUnclaimedCompRewards(_user); + + for (uint256 i; i < _poolTokens.length; ) { + address poolToken = _poolTokens[i]; + + (bool isListed, , ) = comptroller.markets(poolToken); + if (!isListed) revert InvalidCToken(); + + unclaimedRewards += + getAccruedSupplierComp(_user, poolToken) + + getAccruedBorrowerComp(_user, poolToken); + + unchecked { + ++i; + } + } + } + + /// PUBLIC /// + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp(address _supplier, address _poolToken) + public + view + returns (uint256) + { + return + getAccruedSupplierComp( + _supplier, + _poolToken, + morpho.supplyBalanceInOf(_poolToken, _supplier).onPool + ); + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp(address _borrower, address _poolToken) + public + view + returns (uint256) + { + return + getAccruedBorrowerComp( + _borrower, + _poolToken, + morpho.borrowBalanceInOf(_poolToken, _borrower).onPool + ); + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _supplier The address of the supplier. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedSupplierComp( + address _supplier, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 supplierIndex = rewardsManager.compSupplierIndex(_poolToken, _supplier); + + if (supplierIndex == 0) return 0; + return (_balance * (getCurrentCompSupplyIndex(_poolToken) - supplierIndex)) / 1e36; + } + + /// @notice Returns the accrued COMP rewards of a user since the last update. + /// @param _borrower The address of the borrower. + /// @param _poolToken The cToken address. + /// @param _balance The user balance of tokens in the distribution. + /// @return The accrued COMP rewards. + function getAccruedBorrowerComp( + address _borrower, + address _poolToken, + uint256 _balance + ) public view returns (uint256) { + uint256 borrowerIndex = rewardsManager.compBorrowerIndex(_poolToken, _borrower); + + if (borrowerIndex == 0) return 0; + return (_balance * (getCurrentCompBorrowIndex(_poolToken) - borrowerIndex)) / 1e36; + } + + /// @notice Returns the updated COMP supply index. + /// @param _poolToken The cToken address. + /// @return The updated COMP supply index. + function getCurrentCompSupplyIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localSupplyState = rewardsManager + .getLocalCompSupplyState(_poolToken); + + if (localSupplyState.block == block.number) return localSupplyState.index; + else { + IComptroller.CompMarketState memory supplyState = comptroller.compSupplyState( + _poolToken + ); + + uint256 deltaBlocks = block.number - supplyState.block; + uint256 supplySpeed = comptroller.compSupplySpeeds(_poolToken); + + if (deltaBlocks > 0 && supplySpeed > 0) { + uint256 supplyTokens = ICToken(_poolToken).totalSupply(); + uint256 ratio = supplyTokens > 0 + ? (deltaBlocks * supplySpeed * 1e36) / supplyTokens + : 0; + + return supplyState.index + ratio; + } + + return supplyState.index; + } + } + + /// @notice Returns the updated COMP borrow index. + /// @param _poolToken The cToken address. + /// @return The updated COMP borrow index. + function getCurrentCompBorrowIndex(address _poolToken) public view returns (uint256) { + IComptroller.CompMarketState memory localBorrowState = rewardsManager + .getLocalCompBorrowState(_poolToken); + + if (localBorrowState.block == block.number) return localBorrowState.index; + else { + IComptroller.CompMarketState memory borrowState = comptroller.compBorrowState( + _poolToken + ); + uint256 deltaBlocks = block.number - borrowState.block; + uint256 borrowSpeed = comptroller.compBorrowSpeeds(_poolToken); + + if (deltaBlocks > 0 && borrowSpeed > 0) { + uint256 borrowAmount = ICToken(_poolToken).totalBorrows().div( + ICToken(_poolToken).borrowIndex() + ); + uint256 ratio = borrowAmount > 0 + ? (deltaBlocks * borrowSpeed * 1e36) / borrowAmount + : 0; + + return borrowState.index + ratio; + } + + return borrowState.index; + } + } +} diff --git a/src/compound/lens/LensStorage.sol b/src/compound/lens/LensStorage.sol index eda6428cd..4e2344535 100644 --- a/src/compound/lens/LensStorage.sol +++ b/src/compound/lens/LensStorage.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.13; import "../interfaces/compound/ICompound.sol"; +import "./interfaces/ILensExtension.sol"; import "../interfaces/IMorpho.sol"; import "./interfaces/ILens.sol"; @@ -28,6 +29,7 @@ abstract contract LensStorage is ILens, Initializable { IMorpho public immutable morpho; IComptroller public immutable comptroller; IRewardsManager public immutable rewardsManager; + ILensExtension internal immutable lensExtension; /// STORAGE /// @@ -38,9 +40,10 @@ abstract contract LensStorage is ILens, Initializable { /// CONSTRUCTOR /// /// @notice Constructs the contract. - /// @param _morpho The address of the main Morpho contract. - constructor(address _morpho) { - morpho = IMorpho(_morpho); + /// @param _lensExtension The address of the Lens extension. + constructor(address _lensExtension) { + lensExtension = ILensExtension(_lensExtension); + morpho = IMorpho(lensExtension.morpho()); comptroller = IComptroller(morpho.comptroller()); rewardsManager = IRewardsManager(morpho.rewardsManager()); } diff --git a/src/compound/lens/RewardsLens.sol b/src/compound/lens/RewardsLens.sol index c0f2dd16e..d65d73d8d 100644 --- a/src/compound/lens/RewardsLens.sol +++ b/src/compound/lens/RewardsLens.sol @@ -18,7 +18,7 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsManager.getUserUnclaimedRewards(_poolTokens, _user); + return lensExtension.getUserUnclaimedRewards(_poolTokens, _user); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -31,7 +31,7 @@ abstract contract RewardsLens is MarketsLens { address _poolToken, uint256 _balance ) external view returns (uint256) { - return rewardsManager.getAccruedSupplierComp(_supplier, _poolToken, _balance); + return lensExtension.getAccruedSupplierComp(_supplier, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -43,7 +43,7 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsManager.getAccruedSupplierComp(_supplier, _poolToken); + return lensExtension.getAccruedSupplierComp(_supplier, _poolToken); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -56,7 +56,7 @@ abstract contract RewardsLens is MarketsLens { address _poolToken, uint256 _balance ) external view returns (uint256) { - return rewardsManager.getAccruedBorrowerComp(_borrower, _poolToken, _balance); + return lensExtension.getAccruedBorrowerComp(_borrower, _poolToken, _balance); } /// @notice Returns the accrued COMP rewards of a user since the last update. @@ -68,20 +68,20 @@ abstract contract RewardsLens is MarketsLens { view returns (uint256) { - return rewardsManager.getAccruedBorrowerComp(_borrower, _poolToken); + return lensExtension.getAccruedBorrowerComp(_borrower, _poolToken); } /// @notice Returns the updated COMP supply index. /// @param _poolToken The cToken address. /// @return The updated COMP supply index. function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256) { - return rewardsManager.getCurrentCompSupplyIndex(_poolToken); + return lensExtension.getCurrentCompSupplyIndex(_poolToken); } /// @notice Returns the updated COMP borrow index. /// @param _poolToken The cToken address. /// @return The updated COMP borrow index. function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256) { - return rewardsManager.getCurrentCompBorrowIndex(_poolToken); + return lensExtension.getCurrentCompBorrowIndex(_poolToken); } } diff --git a/src/compound/lens/interfaces/ILensExtension.sol b/src/compound/lens/interfaces/ILensExtension.sol new file mode 100644 index 000000000..29ef57aa3 --- /dev/null +++ b/src/compound/lens/interfaces/ILensExtension.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.5.0; + +import "../../interfaces/compound/ICompound.sol"; +import "../../interfaces/IRewardsManager.sol"; +import "../../interfaces/IMorpho.sol"; + +interface ILensExtension { + function morpho() external view returns (IMorpho); + + function getUserUnclaimedRewards(address[] calldata _poolTokens, address _user) + external + view + returns (uint256 unclaimedRewards); + + function getAccruedSupplierComp( + address _supplier, + address _poolToken, + uint256 _balance + ) external view returns (uint256); + + function getAccruedBorrowerComp( + address _borrower, + address _poolToken, + uint256 _balance + ) external view returns (uint256); + + function getAccruedSupplierComp(address _supplier, address _poolToken) + external + view + returns (uint256); + + function getAccruedBorrowerComp(address _borrower, address _poolToken) + external + view + returns (uint256); + + function getCurrentCompSupplyIndex(address _poolToken) external view returns (uint256); + + function getCurrentCompBorrowIndex(address _poolToken) external view returns (uint256); +} diff --git a/test/compound/TestUpgradeable.t.sol b/test/compound/TestUpgradeable.t.sol index eadee9c2a..b60bc17b5 100644 --- a/test/compound/TestUpgradeable.t.sol +++ b/test/compound/TestUpgradeable.t.sol @@ -65,15 +65,18 @@ contract TestUpgradeable is TestSetup { /// Lens /// function testUpgradeLens() public { - _testUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); + _testUpgradeProxy(lensProxy, address(new Lens(address(lensExtension)))); } function testOnlyProxyOwnerCanUpgradeLens() public { - _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(morpho)))); + _testOnlyProxyOwnerCanUpgradeProxy(lensProxy, address(new Lens(address(lensExtension)))); } function testOnlyProxyOwnerCanUpgradeAndCallLens() public { - _testOnlyProxyOwnerCanUpgradeAndCallProxy(lensProxy, address(new Lens(address(morpho)))); + _testOnlyProxyOwnerCanUpgradeAndCallProxy( + lensProxy, + address(new Lens(address(lensExtension))) + ); } /// INTERNAL /// diff --git a/test/compound/setup/TestSetup.sol b/test/compound/setup/TestSetup.sol index a20089997..a1c65d4a5 100644 --- a/test/compound/setup/TestSetup.sol +++ b/test/compound/setup/TestSetup.sol @@ -107,7 +107,8 @@ contract TestSetup is Config, Utils { morpho.setRewardsManager(rewardsManager); - lensImplV1 = new Lens(address(morpho)); + lensExtension = new LensExtension(address(morpho)); + lensImplV1 = new Lens(address(lensExtension)); lensProxy = new TransparentUpgradeableProxy(address(lensImplV1), address(proxyAdmin), ""); lens = Lens(address(lensProxy)); } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index 7041cb4d7..37a2c9a64 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -261,7 +261,9 @@ contract TestSetup is Config, Test { proxyAdmin.upgrade(morphoProxy, morphoImplV2); vm.label(morphoImplV2, "MorphoImplV2"); - address lensImplV2 = address(new Lens(address(morpho))); + lensExtension = new LensExtension(address(morpho)); + + address lensImplV2 = address(new Lens(address(lensExtension))); proxyAdmin.upgrade(lensProxy, lensImplV2); vm.label(lensImplV2, "LensImplV2"); From ca95f52694a4fc9407963cb6e43aa241939bb0a3 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 12 Jan 2023 13:31:54 +0100 Subject: [PATCH 080/105] Increase tolerance for production test --- test/prod/compound/TestDeltas.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/prod/compound/TestDeltas.t.sol b/test/prod/compound/TestDeltas.t.sol index ef70bdce8..ec8f01116 100644 --- a/test/prod/compound/TestDeltas.t.sol +++ b/test/prod/compound/TestDeltas.t.sol @@ -98,13 +98,13 @@ contract TestDeltas is TestSetup { assertApproxEqAbs( test.avgSupplyRatePerBlock, ICToken(test.market.poolToken).supplyRatePerBlock(), - 1, + 10, "avg supply rate per year" ); assertApproxEqAbs( test.avgBorrowRatePerBlock, ICToken(test.market.poolToken).borrowRatePerBlock(), - 2, + 10, "avg borrow rate per year" ); From 6d769b891fbaa671c03fdbb58658da1dc762cd6c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 12 Jan 2023 14:47:09 +0100 Subject: [PATCH 081/105] Improve rates prediction --- config/eth-mainnet/aave-v2/Config.sol | 7 - .../interfaces/aave/IProtocolDataProvider.sol | 73 --------- .../interfaces/aave/IStableDebtToken.sol | 150 ++++++++++++++++++ src/aave-v2/lens/LensStorage.sol | 5 - src/aave-v2/lens/RatesLens.sol | 39 ++--- test/prod/aave-v2/TestUpgradeLens.t.sol | 8 +- test/prod/compound/TestUpgradeLens.t.sol | 4 +- 7 files changed, 167 insertions(+), 119 deletions(-) delete mode 100644 src/aave-v2/interfaces/aave/IProtocolDataProvider.sol create mode 100644 src/aave-v2/interfaces/aave/IStableDebtToken.sol diff --git a/config/eth-mainnet/aave-v2/Config.sol b/config/eth-mainnet/aave-v2/Config.sol index 54c87baf5..080dd90c5 100644 --- a/config/eth-mainnet/aave-v2/Config.sol +++ b/config/eth-mainnet/aave-v2/Config.sol @@ -3,7 +3,6 @@ pragma solidity >=0.8.0; import {ILendingPool} from "src/aave-v2/interfaces/aave/ILendingPool.sol"; import {IPriceOracleGetter} from "src/aave-v2/interfaces/aave/IPriceOracleGetter.sol"; -import {IProtocolDataProvider} from "src/aave-v2/interfaces/aave/IProtocolDataProvider.sol"; import {ILendingPoolAddressesProvider} from "src/aave-v2/interfaces/aave/ILendingPoolAddressesProvider.sol"; import {IEntryPositionsManager} from "src/aave-v2/interfaces/IEntryPositionsManager.sol"; import {IExitPositionsManager} from "src/aave-v2/interfaces/IExitPositionsManager.sol"; @@ -43,12 +42,6 @@ contract Config is BaseConfig { ILendingPoolAddressesProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); ILendingPoolConfigurator public lendingPoolConfigurator = ILendingPoolConfigurator(0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756); - IProtocolDataProvider public dataProvider = - IProtocolDataProvider( - poolAddressesProvider.getAddress( - 0x0100000000000000000000000000000000000000000000000000000000000000 - ) - ); IPriceOracleGetter public oracle = IPriceOracleGetter(poolAddressesProvider.getPriceOracle()); ILendingPool public pool = ILendingPool(poolAddressesProvider.getLendingPool()); diff --git a/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol b/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol deleted file mode 100644 index 69a0a1c83..000000000 --- a/src/aave-v2/interfaces/aave/IProtocolDataProvider.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.5.0; - -import {ILendingPoolAddressesProvider} from "./ILendingPoolAddressesProvider.sol"; - -interface IProtocolDataProvider { - struct TokenData { - string symbol; - address tokenAddress; - } - - function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); - - function getAllReservesTokens() external view returns (TokenData[] memory); - - function getAllATokens() external view returns (TokenData[] memory); - - function getReserveConfigurationData(address asset) - external - view - returns ( - uint256 decimals, - uint256 ltv, - uint256 liquidationThreshold, - uint256 liquidationBonus, - uint256 reserveFactor, - bool usageAsCollateralEnabled, - bool borrowingEnabled, - bool stableBorrowRateEnabled, - bool isActive, - bool isFrozen - ); - - function getReserveData(address asset) - external - view - returns ( - uint256 availableLiquidity, - uint256 totalStableDebt, - uint256 totalVariableDebt, - uint256 liquidityRate, - uint256 variableBorrowRate, - uint256 stableBorrowRate, - uint256 averageStableBorrowRate, - uint256 liquidityIndex, - uint256 variableBorrowIndex, - uint40 lastUpdateTimestamp - ); - - function getUserReserveData(address asset, address user) - external - view - returns ( - uint256 currentATokenBalance, - uint256 currentStableDebt, - uint256 currentVariableDebt, - uint256 principalStableDebt, - uint256 scaledVariableDebt, - uint256 stableBorrowRate, - uint256 liquidityRate, - uint40 stableRateLastUpdated, - bool usageAsCollateralEnabled - ); - - function getReserveTokensAddresses(address asset) - external - view - returns ( - address aTokenAddress, - address stableDebtTokenAddress, - address variableDebtTokenAddress - ); -} diff --git a/src/aave-v2/interfaces/aave/IStableDebtToken.sol b/src/aave-v2/interfaces/aave/IStableDebtToken.sol new file mode 100644 index 000000000..462c30902 --- /dev/null +++ b/src/aave-v2/interfaces/aave/IStableDebtToken.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.5.0; + +/** + * @title IStableDebtToken + * @author Aave + * @notice Defines the interface for the stable debt token + **/ +interface IStableDebtToken { + /** + * @dev Emitted when new stable debt is minted + * @param user The address of the user who triggered the minting + * @param onBehalfOf The recipient of stable debt tokens + * @param amount The amount minted (user entered amount + balance increase from interest) + * @param currentBalance The current balance of the user + * @param balanceIncrease The increase in balance since the last action of the user + * @param newRate The rate of the debt after the minting + * @param avgStableRate The next average stable rate after the minting + * @param newTotalSupply The next total supply of the stable debt token after the action + **/ + event Mint( + address indexed user, + address indexed onBehalfOf, + uint256 amount, + uint256 currentBalance, + uint256 balanceIncrease, + uint256 newRate, + uint256 avgStableRate, + uint256 newTotalSupply + ); + + /** + * @dev Emitted when new stable debt is burned + * @param from The address from which the debt will be burned + * @param amount The amount being burned (user entered amount - balance increase from interest) + * @param currentBalance The current balance of the user + * @param balanceIncrease The the increase in balance since the last action of the user + * @param avgStableRate The next average stable rate after the burning + * @param newTotalSupply The next total supply of the stable debt token after the action + **/ + event Burn( + address indexed from, + uint256 amount, + uint256 currentBalance, + uint256 balanceIncrease, + uint256 avgStableRate, + uint256 newTotalSupply + ); + + /** + * @notice Mints debt token to the `onBehalfOf` address. + * @dev The resulting rate is the weighted average between the rate of the new debt + * and the rate of the previous debt + * @param user The address receiving the borrowed underlying, being the delegatee in case + * of credit delegate, or same as `onBehalfOf` otherwise + * @param onBehalfOf The address receiving the debt tokens + * @param amount The amount of debt tokens to mint + * @param rate The rate of the debt being minted + * @return True if it is the first borrow, false otherwise + * @return The total stable debt + * @return The average stable borrow rate + **/ + function mint( + address user, + address onBehalfOf, + uint256 amount, + uint256 rate + ) + external + returns ( + bool, + uint256, + uint256 + ); + + /** + * @notice Burns debt of `user` + * @dev The resulting rate is the weighted average between the rate of the new debt + * and the rate of the previous debt + * @dev In some instances, a burn transaction will emit a mint event + * if the amount to burn is less than the interest the user earned + * @param from The address from which the debt will be burned + * @param amount The amount of debt tokens getting burned + * @return The total stable debt + * @return The average stable borrow rate + **/ + function burn(address from, uint256 amount) external returns (uint256, uint256); + + /** + * @notice Returns the average rate of all the stable rate loans. + * @return The average stable rate + **/ + function getAverageStableRate() external view returns (uint256); + + /** + * @notice Returns the stable rate of the user debt + * @param user The address of the user + * @return The stable rate of the user + **/ + function getUserStableRate(address user) external view returns (uint256); + + /** + * @notice Returns the timestamp of the last update of the user + * @param user The address of the user + * @return The timestamp + **/ + function getUserLastUpdated(address user) external view returns (uint40); + + /** + * @notice Returns the principal, the total supply, the average stable rate and the timestamp for the last update + * @return The principal + * @return The total supply + * @return The average stable rate + * @return The timestamp of the last update + **/ + function getSupplyData() + external + view + returns ( + uint256, + uint256, + uint256, + uint40 + ); + + /** + * @notice Returns the timestamp of the last update of the total supply + * @return The timestamp + **/ + function getTotalSupplyLastUpdated() external view returns (uint40); + + /** + * @notice Returns the total supply and the average stable rate + * @return The total supply + * @return The average rate + **/ + function getTotalSupplyAndAvgRate() external view returns (uint256, uint256); + + /** + * @notice Returns the principal debt balance of the user + * @return The debt balance of the user since the last burn/mint action + **/ + function principalBalanceOf(address user) external view returns (uint256); + + /** + * @notice Returns the address of the underlying asset of this stableDebtToken (E.g. WETH for stableDebtWETH) + * @return The address of the underlying asset + **/ + function UNDERLYING_ASSET_ADDRESS() external view returns (address); +} diff --git a/src/aave-v2/lens/LensStorage.sol b/src/aave-v2/lens/LensStorage.sol index f74145bd2..a548d3417 100644 --- a/src/aave-v2/lens/LensStorage.sol +++ b/src/aave-v2/lens/LensStorage.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.13; -import "../interfaces/aave/IProtocolDataProvider.sol"; import "../interfaces/aave/IPriceOracleGetter.sol"; import "../interfaces/aave/ILendingPool.sol"; import "../interfaces/aave/IAToken.sol"; @@ -22,8 +21,6 @@ import "@morpho-dao/morpho-utils/math/Math.sol"; abstract contract LensStorage is ILens { /// CONSTANTS /// - bytes32 internal constant DATA_PROVIDER_ID = - 0x0100000000000000000000000000000000000000000000000000000000000000; uint16 public constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 50_00; // 50% in basis points. uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; // Health factor below which the positions can be liquidated. @@ -37,7 +34,6 @@ abstract contract LensStorage is ILens { IMorpho public immutable morpho; ILendingPoolAddressesProvider public immutable addressesProvider; - IProtocolDataProvider internal immutable dataProvider; ILendingPool public immutable pool; /// CONSTRUCTOR /// @@ -48,7 +44,6 @@ abstract contract LensStorage is ILens { morpho = IMorpho(_morpho); pool = ILendingPool(morpho.pool()); addressesProvider = ILendingPoolAddressesProvider(morpho.addressesProvider()); - dataProvider = IProtocolDataProvider(addressesProvider.getAddress(DATA_PROVIDER_ID)); ST_ETH_BASE_REBASE_INDEX = morpho.ST_ETH_BASE_REBASE_INDEX(); } } diff --git a/src/aave-v2/lens/RatesLens.sol b/src/aave-v2/lens/RatesLens.sol index 1aebc1363..c35b52698 100644 --- a/src/aave-v2/lens/RatesLens.sol +++ b/src/aave-v2/lens/RatesLens.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.13; +import "../interfaces/aave/IStableDebtToken.sol"; import "../interfaces/aave/IVariableDebtToken.sol"; import "../interfaces/aave/IReserveInterestRateStrategy.sol"; @@ -349,14 +350,6 @@ abstract contract RatesLens is UsersLens { /// INTERNAL /// - struct PoolRatesVars { - uint256 availableLiquidity; - uint256 totalStableDebt; - uint256 totalVariableDebt; - uint256 avgStableRate; - uint256 reserveFactor; - } - /// @dev Computes and returns peer-to-peer and pool rates for a specific market. /// @param _poolToken The market address. /// @param _suppliedOnPool The amount hypothetically supplied to the underlying's pool (in underlying). @@ -440,31 +433,21 @@ abstract contract RatesLens is UsersLens { uint256 _repaid ) internal view returns (uint256 poolSupplyRate, uint256 poolBorrowRate) { DataTypes.ReserveData memory reserve = pool.getReserveData(_underlying); - - PoolRatesVars memory vars; - ( - vars.availableLiquidity, - vars.totalStableDebt, - vars.totalVariableDebt, - , - , - , - vars.avgStableRate, - , - , - - ) = dataProvider.getReserveData(_underlying); - (, , , , vars.reserveFactor) = reserve.configuration.getParamsMemory(); + (, , , , uint256 reserveFactor) = reserve.configuration.getParamsMemory(); (poolSupplyRate, , poolBorrowRate) = IReserveInterestRateStrategy( reserve.interestRateStrategyAddress ).calculateInterestRates( _underlying, - vars.availableLiquidity + _supplied + _repaid - _borrowed - _withdrawn, - vars.totalStableDebt, - vars.totalVariableDebt + _borrowed - _repaid, - vars.avgStableRate, - vars.reserveFactor + ERC20(_underlying).balanceOf(reserve.aTokenAddress) + + _supplied + + _repaid - + _borrowed - + _withdrawn, + ERC20(reserve.stableDebtTokenAddress).totalSupply(), + ERC20(reserve.variableDebtTokenAddress).totalSupply() + _borrowed - _repaid, + IStableDebtToken(reserve.stableDebtTokenAddress).getAverageStableRate(), + reserveFactor ); } diff --git a/test/prod/aave-v2/TestUpgradeLens.t.sol b/test/prod/aave-v2/TestUpgradeLens.t.sol index 58371e58c..1822c6c6f 100644 --- a/test/prod/aave-v2/TestUpgradeLens.t.sol +++ b/test/prod/aave-v2/TestUpgradeLens.t.sol @@ -86,12 +86,12 @@ contract TestUpgradeLens is TestSetup { _tip(supplyMarket.underlying, address(user), supplyAmount); user.approve(supplyMarket.underlying, supplyAmount); - user.supply(supplyMarket.poolToken, address(user), supplyAmount); + user.supply(supplyMarket.poolToken, address(user), supplyAmount, 1_000); // Only perform 1 matching loop, as simulated in getNextUserSupplyRatePerYear. assertApproxEqAbs( lens.getCurrentUserSupplyRatePerYear(supplyMarket.poolToken, address(user)), expectedSupplyRate, - 1e24, + 1e15, string.concat(supplyMarket.symbol, " supply rate") ); @@ -103,12 +103,12 @@ contract TestUpgradeLens is TestSetup { borrowAmount ); - user.borrow(borrowMarket.poolToken, borrowAmount); + user.borrow(borrowMarket.poolToken, borrowAmount, 1_000); // Only perform 1 matching loop, as simulated in getNextUserBorrowRatePerYear. assertApproxEqAbs( lens.getCurrentUserBorrowRatePerYear(borrowMarket.poolToken, address(user)), expectedBorrowRate, - 1e24, + 1e15, string.concat(borrowMarket.symbol, " borrow rate") ); } diff --git a/test/prod/compound/TestUpgradeLens.t.sol b/test/prod/compound/TestUpgradeLens.t.sol index 7bcbb5604..563a35a7f 100644 --- a/test/prod/compound/TestUpgradeLens.t.sol +++ b/test/prod/compound/TestUpgradeLens.t.sol @@ -119,7 +119,7 @@ contract TestUpgradeLens is TestSetup { _tip(supplyMarket.underlying, address(user), supplyAmount); user.approve(supplyMarket.underlying, supplyAmount); - user.supply(supplyMarket.poolToken, address(user), supplyAmount); + user.supply(supplyMarket.poolToken, address(user), supplyAmount, 1_000); // Only perform 1 matching loop, as simulated in getNextUserSupplyRatePerYear. assertApproxEqAbs( lens.getCurrentUserSupplyRatePerBlock(supplyMarket.poolToken, address(user)), @@ -136,7 +136,7 @@ contract TestUpgradeLens is TestSetup { borrowAmount ); - user.borrow(borrowMarket.poolToken, borrowAmount); + user.borrow(borrowMarket.poolToken, borrowAmount, 1_000); // Only perform 1 matching loop, as simulated in getNextUserBorrowRatePerBlock. assertApproxEqAbs( lens.getCurrentUserBorrowRatePerBlock(borrowMarket.poolToken, address(user)), From 1566ea6a0271aed399833df3bcef71f06a8424cf Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 17 Jan 2023 10:54:40 +0100 Subject: [PATCH 082/105] Applied suggestions --- src/compound/lens/IndexesLens.sol | 42 ++++++++++++++++------------- src/compound/lens/LensExtension.sol | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/compound/lens/IndexesLens.sol b/src/compound/lens/IndexesLens.sol index bf366470f..e4b5c915b 100644 --- a/src/compound/lens/IndexesLens.sol +++ b/src/compound/lens/IndexesLens.sol @@ -131,22 +131,24 @@ abstract contract IndexesLens is LensStorage { uint256 accrualBlockNumberPrior = _poolToken.accrualBlockNumber(); if (block.number == accrualBlockNumberPrior) { poolSupplyIndex = _poolToken.exchangeRateStored(); - } else { - uint256 borrowRateMantissa = _poolToken.borrowRatePerBlock(); - require(borrowRateMantissa <= 0.0005e16, "borrow rate is absurdly high"); - uint256 simpleInterestFactor = borrowRateMantissa * - (block.number - accrualBlockNumberPrior); - uint256 interestAccumulated = simpleInterestFactor.mul(vars.totalBorrows); + return (poolSupplyIndex, poolBorrowIndex, vars); + } - vars.totalBorrows += interestAccumulated; - vars.totalReserves += vars.reserveFactorMantissa.mul(interestAccumulated); + uint256 borrowRateMantissa = _poolToken.borrowRatePerBlock(); + require(borrowRateMantissa <= 0.0005e16, "borrow rate is absurdly high"); - poolSupplyIndex = (vars.cash + vars.totalBorrows - vars.totalReserves).div( - _poolToken.totalSupply() - ); - poolBorrowIndex += simpleInterestFactor.mul(poolBorrowIndex); - } + uint256 simpleInterestFactor = borrowRateMantissa * + (block.number - accrualBlockNumberPrior); + uint256 interestAccumulated = simpleInterestFactor.mul(vars.totalBorrows); + + vars.totalBorrows += interestAccumulated; + vars.totalReserves += vars.reserveFactorMantissa.mul(interestAccumulated); + + poolSupplyIndex = (vars.cash + vars.totalBorrows - vars.totalReserves).div( + _poolToken.totalSupply() + ); + poolBorrowIndex += simpleInterestFactor.mul(poolBorrowIndex); } /// @notice Returns the most up-to-date or virtually updated peer-to-peer indexes. @@ -172,13 +174,17 @@ abstract contract IndexesLens is LensStorage { returns ( uint256 _p2pSupplyIndex, uint256 _p2pBorrowIndex, - Types.MarketParameters memory params + Types.MarketParameters memory marketParameters ) { - params = morpho.marketParameters(_poolToken); + marketParameters = morpho.marketParameters(_poolToken); if (!_updated || block.number == _lastPoolIndexes.lastUpdateBlockNumber) { - return (morpho.p2pSupplyIndex(_poolToken), morpho.p2pBorrowIndex(_poolToken), params); + return ( + morpho.p2pSupplyIndex(_poolToken), + morpho.p2pBorrowIndex(_poolToken), + marketParameters + ); } InterestRatesModel.GrowthFactors memory growthFactors = InterestRatesModel @@ -186,8 +192,8 @@ abstract contract IndexesLens is LensStorage { _poolSupplyIndex, _poolBorrowIndex, _lastPoolIndexes, - params.p2pIndexCursor, - params.reserveFactor + marketParameters.p2pIndexCursor, + marketParameters.reserveFactor ); _p2pSupplyIndex = InterestRatesModel.computeP2PIndex( diff --git a/src/compound/lens/LensExtension.sol b/src/compound/lens/LensExtension.sol index e36c8b03d..7050c15b5 100644 --- a/src/compound/lens/LensExtension.sol +++ b/src/compound/lens/LensExtension.sol @@ -10,7 +10,7 @@ import "@morpho-dao/morpho-utils/math/CompoundMath.sol"; /// @title LensExtension. /// @author Morpho Labs. /// @custom:contact security@morpho.xyz -/// @notice This contract is an extension of the Lens. +/// @notice This contract is an extension of the Lens. It should be deployed before the Lens, as the Lens depends on its address to extends its functionalities. contract LensExtension is ILensExtension { using CompoundMath for uint256; From b3f1256a6e65b7ed980a0c121ca7fed0743f78b6 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 16 Feb 2023 15:00:22 +0100 Subject: [PATCH 083/105] fix: skip borrow pause when deprecated --- src/aave-v2/MorphoGovernance.sol | 2 +- src/compound/MorphoGovernance.sol | 2 +- test/aave-v2/TestPausableMarket.t.sol | 24 ++++++++++++++++++++++++ test/compound/TestPausableMarket.t.sol | 24 ++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 3aa3057d3..4af309d7d 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -467,7 +467,7 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; - pause.isBorrowPaused = _isPaused; + if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; pause.isWithdrawPaused = _isPaused; pause.isRepayPaused = _isPaused; pause.isLiquidateCollateralPaused = _isPaused; diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 0f639e4fd..dad0b7cd7 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -467,7 +467,7 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; - pause.isBorrowPaused = _isPaused; + if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; pause.isWithdrawPaused = _isPaused; pause.isRepayPaused = _isPaused; pause.isLiquidateCollateralPaused = _isPaused; diff --git a/test/aave-v2/TestPausableMarket.t.sol b/test/aave-v2/TestPausableMarket.t.sol index 49f78acd4..b168e4078 100644 --- a/test/aave-v2/TestPausableMarket.t.sol +++ b/test/aave-v2/TestPausableMarket.t.sol @@ -102,6 +102,19 @@ contract TestPausableMarket is TestSetup { } } + function testBorrowPauseCheckSkipped() public { + // Deprecate a market. + morpho.setIsBorrowPaused(aDai, true); + morpho.setIsDeprecated(aDai, true); + _checkPauseEquality(aDai, true, true); + + morpho.setIsPausedForAllMarkets(false); + _checkPauseEquality(aDai, true, true); + + morpho.setIsPausedForAllMarkets(true); + _checkPauseEquality(aDai, true, true); + } + function testPauseSupply() public { uint256 amount = 10_000 ether; morpho.setIsSupplyPaused(aDai, true); @@ -184,4 +197,15 @@ contract TestPausableMarket is TestSetup { vm.expectRevert(abi.encodeWithSignature("MarketNotCreated()")); morpho.setIsDeprecated(address(1), true); } + + function _checkPauseEquality( + address poolToken, + bool shouldBePaused, + bool shouldBeDeprecated + ) public { + (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(poolToken); + + assertEq(isBorrowPaused, shouldBePaused); + assertEq(isDeprecated, shouldBeDeprecated); + } } diff --git a/test/compound/TestPausableMarket.t.sol b/test/compound/TestPausableMarket.t.sol index ab325267f..5eb4e4841 100644 --- a/test/compound/TestPausableMarket.t.sol +++ b/test/compound/TestPausableMarket.t.sol @@ -74,6 +74,19 @@ contract TestPausableMarket is TestSetup { } } + function testBorrowPauseCheckSkipped() public { + // Deprecate a market. + morpho.setIsBorrowPaused(cDai, true); + morpho.setIsDeprecated(cDai, true); + _checkPauseEquality(cDai, true, true); + + morpho.setIsPausedForAllMarkets(false); + _checkPauseEquality(cDai, true, true); + + morpho.setIsPausedForAllMarkets(true); + _checkPauseEquality(cDai, true, true); + } + function testOnlyOwnerShouldDisableSupply() public { uint256 amount = 10_000 ether; @@ -145,4 +158,15 @@ contract TestPausableMarket is TestSetup { vm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); supplier1.liquidate(cDai, cUsdc, address(supplier2), amount); } + + function _checkPauseEquality( + address poolToken, + bool shouldBePaused, + bool shouldBeDeprecated + ) public { + (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(poolToken); + + assertEq(isBorrowPaused, shouldBePaused); + assertEq(isDeprecated, shouldBeDeprecated); + } } From b8150fd3b79e5823a573552157cfea9622306a37 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 16 Feb 2023 15:34:07 +0100 Subject: [PATCH 084/105] feat: do not deprecate market if IsLiquidateBorrowPaused --- src/aave-v2/MorphoGovernance.sol | 9 +++++++-- src/compound/MorphoGovernance.sol | 9 +++++++-- test/aave-v2/TestGovernance.t.sol | 6 ++++++ test/compound/TestGovernance.t.sol | 6 ++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 4af309d7d..f9a5366d6 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -126,6 +126,9 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when market borrow is not paused. error BorrowNotPaused(); + /// @notice Thrown when deprecating a market while liquidating the borrow is paused. + error LiquidateBorrowIsPaused(); + /// @notice Thrown when market is deprecated. error MarketIsDeprecated(); @@ -360,8 +363,10 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); - marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; + Types.MarketPauseStatus storage status = marketPauseStatus[_poolToken]; + if (!status.isBorrowPaused) revert BorrowNotPaused(); + if (_isDeprecated && status.isLiquidateBorrowPaused) revert LiquidateBorrowIsPaused(); + status.isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index dad0b7cd7..5111d405e 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -126,6 +126,9 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when market borrow is not paused. error BorrowNotPaused(); + /// @notice Thrown when deprecating a market while liquidating the borrow is paused. + error LiquidateBorrowIsPaused(); + /// @notice Thrown when market is deprecated. error MarketIsDeprecated(); @@ -370,8 +373,10 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); - marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; + Types.MarketPauseStatus storage status = marketPauseStatus[_poolToken]; + if (!status.isBorrowPaused) revert BorrowNotPaused(); + if (_isDeprecated && status.isLiquidateBorrowPaused) revert LiquidateBorrowIsPaused(); + status.isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/test/aave-v2/TestGovernance.t.sol b/test/aave-v2/TestGovernance.t.sol index cc30e2f71..a8a35a990 100644 --- a/test/aave-v2/TestGovernance.t.sol +++ b/test/aave-v2/TestGovernance.t.sol @@ -413,6 +413,12 @@ contract TestGovernance is TestSetup { morpho.setIsDeprecated(aDai, true); morpho.setIsBorrowPaused(aDai, true); + + morpho.setIsLiquidateBorrowPaused(aDai, true); + hevm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); + morpho.setIsDeprecated(aDai, true); + + morpho.setIsLiquidateBorrowPaused(aDai, false); morpho.setIsDeprecated(aDai, true); hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index f37bf2d1e..6f4490803 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -438,6 +438,12 @@ contract TestGovernance is TestSetup { morpho.setIsDeprecated(cDai, true); morpho.setIsBorrowPaused(cDai, true); + + morpho.setIsLiquidateBorrowPaused(cDai, true); + hevm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); + morpho.setIsDeprecated(cDai, true); + + morpho.setIsLiquidateBorrowPaused(cDai, false); morpho.setIsDeprecated(cDai, true); hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); From fe2ed9815a066b0268e7406a3201a6b228357edc Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Thu, 16 Feb 2023 18:06:23 +0100 Subject: [PATCH 085/105] ci: fix ci upgrade --- config/eth-mainnet/aave-v2/Config.sol | 1 - config/eth-mainnet/compound/Config.sol | 1 - test/prod/aave-v2/TestDeltas.t.sol | 4 ++-- test/prod/aave-v2/setup/TestSetup.sol | 2 +- test/prod/compound/TestDeltas.t.sol | 4 ++-- test/prod/compound/setup/TestSetup.sol | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/config/eth-mainnet/aave-v2/Config.sol b/config/eth-mainnet/aave-v2/Config.sol index 080dd90c5..6a8e1b785 100644 --- a/config/eth-mainnet/aave-v2/Config.sol +++ b/config/eth-mainnet/aave-v2/Config.sol @@ -37,7 +37,6 @@ contract Config is BaseConfig { address constant swapRouterAddress = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - address public morphoDao = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; ILendingPoolAddressesProvider public poolAddressesProvider = ILendingPoolAddressesProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); ILendingPoolConfigurator public lendingPoolConfigurator = diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index b602fb65e..2cdbf65d4 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -34,7 +34,6 @@ contract Config is BaseConfig { address constant cUsdp = 0x041171993284df560249B57358F931D9eB7b925D; address constant cSushi = 0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7; - address public morphoDao = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; IComptroller public comptroller = IComptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); ICompoundOracle public oracle = ICompoundOracle(comptroller.oracle()); diff --git a/test/prod/aave-v2/TestDeltas.t.sol b/test/prod/aave-v2/TestDeltas.t.sol index e27cdde60..a39911a13 100644 --- a/test/prod/aave-v2/TestDeltas.t.sol +++ b/test/prod/aave-v2/TestDeltas.t.sol @@ -59,7 +59,7 @@ contract TestDeltas is TestSetup { address(morpho) ); - vm.prank(morphoDao); + vm.prank(morpho.owner()); morpho.increaseP2PDeltas(test.market.poolToken, type(uint256).max); ( @@ -150,7 +150,7 @@ contract TestDeltas is TestSetup { p2pBorrowUnderlying > borrowDeltaUnderlyingBefore ) continue; - vm.prank(morphoDao); + vm.prank(morpho.owner()); vm.expectRevert(PositionsManagerUtils.AmountIsZero.selector); morpho.increaseP2PDeltas(test.market.poolToken, type(uint256).max); } diff --git a/test/prod/aave-v2/setup/TestSetup.sol b/test/prod/aave-v2/setup/TestSetup.sol index 50195d22f..724c43221 100644 --- a/test/prod/aave-v2/setup/TestSetup.sol +++ b/test/prod/aave-v2/setup/TestSetup.sol @@ -286,7 +286,7 @@ contract TestSetup is Config, Test { /// @dev Upgrades all the protocol contracts. function _upgrade() internal { - vm.startPrank(morphoDao); + vm.startPrank(proxyAdmin.owner()); address morphoImplV2 = address(new Morpho()); proxyAdmin.upgrade(morphoProxy, morphoImplV2); vm.label(morphoImplV2, "MorphoImplV2"); diff --git a/test/prod/compound/TestDeltas.t.sol b/test/prod/compound/TestDeltas.t.sol index ec8f01116..0eaf438fc 100644 --- a/test/prod/compound/TestDeltas.t.sol +++ b/test/prod/compound/TestDeltas.t.sol @@ -63,7 +63,7 @@ contract TestDeltas is TestSetup { address(morpho) ); - vm.prank(morphoDao); + vm.prank(morpho.owner()); morpho.increaseP2PDeltas(test.market.poolToken, type(uint256).max); ( @@ -158,7 +158,7 @@ contract TestDeltas is TestSetup { continue; } - vm.prank(morphoDao); + vm.prank(morpho.owner()); vm.expectRevert(PositionsManager.AmountIsZero.selector); morpho.increaseP2PDeltas(test.market.poolToken, type(uint256).max); } diff --git a/test/prod/compound/setup/TestSetup.sol b/test/prod/compound/setup/TestSetup.sol index 37a2c9a64..740102a95 100644 --- a/test/prod/compound/setup/TestSetup.sol +++ b/test/prod/compound/setup/TestSetup.sol @@ -252,7 +252,7 @@ contract TestSetup is Config, Test { /// @dev Upgrades all the protocol contracts. function _upgrade() internal { - vm.startPrank(morphoDao); + vm.startPrank(proxyAdmin.owner()); address rewardsManagerImplV2 = address(new RewardsManager()); proxyAdmin.upgrade(rewardsManagerProxy, rewardsManagerImplV2); vm.label(rewardsManagerImplV2, "RewardsManagerImplV2"); From 566397346bdd60ac7b157c892627aa9f8272e001 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 17 Feb 2023 16:23:01 +0100 Subject: [PATCH 086/105] Revert "feat: do not deprecate market if IsLiquidateBorrowPaused" This reverts commit b8150fd3b79e5823a573552157cfea9622306a37. --- src/aave-v2/MorphoGovernance.sol | 9 ++------- src/compound/MorphoGovernance.sol | 9 ++------- test/aave-v2/TestGovernance.t.sol | 6 ------ test/compound/TestGovernance.t.sol | 6 ------ 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index f9a5366d6..4af309d7d 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -126,9 +126,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when market borrow is not paused. error BorrowNotPaused(); - /// @notice Thrown when deprecating a market while liquidating the borrow is paused. - error LiquidateBorrowIsPaused(); - /// @notice Thrown when market is deprecated. error MarketIsDeprecated(); @@ -363,10 +360,8 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - Types.MarketPauseStatus storage status = marketPauseStatus[_poolToken]; - if (!status.isBorrowPaused) revert BorrowNotPaused(); - if (_isDeprecated && status.isLiquidateBorrowPaused) revert LiquidateBorrowIsPaused(); - status.isDeprecated = _isDeprecated; + if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); + marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index 5111d405e..dad0b7cd7 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -126,9 +126,6 @@ abstract contract MorphoGovernance is MorphoUtils { /// @notice Thrown when market borrow is not paused. error BorrowNotPaused(); - /// @notice Thrown when deprecating a market while liquidating the borrow is paused. - error LiquidateBorrowIsPaused(); - /// @notice Thrown when market is deprecated. error MarketIsDeprecated(); @@ -373,10 +370,8 @@ abstract contract MorphoGovernance is MorphoUtils { onlyOwner isMarketCreated(_poolToken) { - Types.MarketPauseStatus storage status = marketPauseStatus[_poolToken]; - if (!status.isBorrowPaused) revert BorrowNotPaused(); - if (_isDeprecated && status.isLiquidateBorrowPaused) revert LiquidateBorrowIsPaused(); - status.isDeprecated = _isDeprecated; + if (!marketPauseStatus[_poolToken].isBorrowPaused) revert BorrowNotPaused(); + marketPauseStatus[_poolToken].isDeprecated = _isDeprecated; emit IsDeprecatedSet(_poolToken, _isDeprecated); } diff --git a/test/aave-v2/TestGovernance.t.sol b/test/aave-v2/TestGovernance.t.sol index a8a35a990..cc30e2f71 100644 --- a/test/aave-v2/TestGovernance.t.sol +++ b/test/aave-v2/TestGovernance.t.sol @@ -413,12 +413,6 @@ contract TestGovernance is TestSetup { morpho.setIsDeprecated(aDai, true); morpho.setIsBorrowPaused(aDai, true); - - morpho.setIsLiquidateBorrowPaused(aDai, true); - hevm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); - morpho.setIsDeprecated(aDai, true); - - morpho.setIsLiquidateBorrowPaused(aDai, false); morpho.setIsDeprecated(aDai, true); hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); diff --git a/test/compound/TestGovernance.t.sol b/test/compound/TestGovernance.t.sol index 6f4490803..f37bf2d1e 100644 --- a/test/compound/TestGovernance.t.sol +++ b/test/compound/TestGovernance.t.sol @@ -438,12 +438,6 @@ contract TestGovernance is TestSetup { morpho.setIsDeprecated(cDai, true); morpho.setIsBorrowPaused(cDai, true); - - morpho.setIsLiquidateBorrowPaused(cDai, true); - hevm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); - morpho.setIsDeprecated(cDai, true); - - morpho.setIsLiquidateBorrowPaused(cDai, false); morpho.setIsDeprecated(cDai, true); hevm.expectRevert(abi.encodeWithSignature("MarketIsDeprecated()")); From 2b089824cc88f0b47ef1747456cdf6596ff3678a Mon Sep 17 00:00:00 2001 From: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> Date: Sun, 19 Feb 2023 09:22:28 +0100 Subject: [PATCH 087/105] 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/compound/MorphoGovernance.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index dad0b7cd7..b0540db86 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -467,6 +467,7 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; + // Note that pause.isDeprecated implies pause.isBorrowPaused. if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; pause.isWithdrawPaused = _isPaused; pause.isRepayPaused = _isPaused; From 78be1107e8d13195e7ad575f8267e190915bf4e8 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sun, 19 Feb 2023 10:41:32 +0100 Subject: [PATCH 088/105] docs: add missing comment --- src/aave-v2/MorphoGovernance.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index 4af309d7d..a36088e60 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -467,6 +467,7 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; + // Note that pause.isDeprecated implies pause.isBorrowPaused. if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; pause.isWithdrawPaused = _isPaused; pause.isRepayPaused = _isPaused; From 5c4d4c2e960c59681b6c40c352c2a2c9364ae6b6 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sun, 19 Feb 2023 14:53:48 +0100 Subject: [PATCH 089/105] test: refactor test --- test/aave-v2/TestPausableMarket.t.sol | 26 ++++++++++++-------------- test/compound/TestPausableMarket.t.sol | 26 ++++++++++++-------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/test/aave-v2/TestPausableMarket.t.sol b/test/aave-v2/TestPausableMarket.t.sol index b168e4078..151fa2641 100644 --- a/test/aave-v2/TestPausableMarket.t.sol +++ b/test/aave-v2/TestPausableMarket.t.sol @@ -106,13 +106,22 @@ contract TestPausableMarket is TestSetup { // Deprecate a market. morpho.setIsBorrowPaused(aDai, true); morpho.setIsDeprecated(aDai, true); - _checkPauseEquality(aDai, true, true); + (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(aDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); morpho.setIsPausedForAllMarkets(false); - _checkPauseEquality(aDai, true, true); + (, isBorrowPaused, , , , , isDeprecated) = morpho.marketPauseStatus(aDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); morpho.setIsPausedForAllMarkets(true); - _checkPauseEquality(aDai, true, true); + (, isBorrowPaused, , , , , isDeprecated) = morpho.marketPauseStatus(aDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); } function testPauseSupply() public { @@ -197,15 +206,4 @@ contract TestPausableMarket is TestSetup { vm.expectRevert(abi.encodeWithSignature("MarketNotCreated()")); morpho.setIsDeprecated(address(1), true); } - - function _checkPauseEquality( - address poolToken, - bool shouldBePaused, - bool shouldBeDeprecated - ) public { - (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(poolToken); - - assertEq(isBorrowPaused, shouldBePaused); - assertEq(isDeprecated, shouldBeDeprecated); - } } diff --git a/test/compound/TestPausableMarket.t.sol b/test/compound/TestPausableMarket.t.sol index 5eb4e4841..a39c65896 100644 --- a/test/compound/TestPausableMarket.t.sol +++ b/test/compound/TestPausableMarket.t.sol @@ -78,13 +78,22 @@ contract TestPausableMarket is TestSetup { // Deprecate a market. morpho.setIsBorrowPaused(cDai, true); morpho.setIsDeprecated(cDai, true); - _checkPauseEquality(cDai, true, true); + (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(cDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); morpho.setIsPausedForAllMarkets(false); - _checkPauseEquality(cDai, true, true); + (, isBorrowPaused, , , , , isDeprecated) = morpho.marketPauseStatus(cDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); morpho.setIsPausedForAllMarkets(true); - _checkPauseEquality(cDai, true, true); + (, isBorrowPaused, , , , , isDeprecated) = morpho.marketPauseStatus(cDai); + + assertTrue(isBorrowPaused); + assertTrue(isDeprecated); } function testOnlyOwnerShouldDisableSupply() public { @@ -158,15 +167,4 @@ contract TestPausableMarket is TestSetup { vm.expectRevert(abi.encodeWithSignature("LiquidateBorrowIsPaused()")); supplier1.liquidate(cDai, cUsdc, address(supplier2), amount); } - - function _checkPauseEquality( - address poolToken, - bool shouldBePaused, - bool shouldBeDeprecated - ) public { - (, bool isBorrowPaused, , , , , bool isDeprecated) = morpho.marketPauseStatus(poolToken); - - assertEq(isBorrowPaused, shouldBePaused); - assertEq(isDeprecated, shouldBeDeprecated); - } } From 3d6e6b6e8dc5dccf38c85faed4fc8b0d4a083734 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 20 Feb 2023 11:10:14 +0100 Subject: [PATCH 090/105] refactor: log event only if borrow pause updated --- src/aave-v2/MorphoGovernance.sol | 20 +++++++++++++------- src/compound/MorphoGovernance.sol | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/aave-v2/MorphoGovernance.sol b/src/aave-v2/MorphoGovernance.sol index a36088e60..55f41ff25 100644 --- a/src/aave-v2/MorphoGovernance.sol +++ b/src/aave-v2/MorphoGovernance.sol @@ -467,18 +467,24 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; + emit IsSupplyPausedSet(_poolToken, _isPaused); + // Note that pause.isDeprecated implies pause.isBorrowPaused. - if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; - pause.isWithdrawPaused = _isPaused; - pause.isRepayPaused = _isPaused; - pause.isLiquidateCollateralPaused = _isPaused; - pause.isLiquidateBorrowPaused = _isPaused; + if (!pause.isDeprecated) { + pause.isBorrowPaused = _isPaused; + emit IsBorrowPausedSet(_poolToken, _isPaused); + } - emit IsSupplyPausedSet(_poolToken, _isPaused); - emit IsBorrowPausedSet(_poolToken, _isPaused); + pause.isWithdrawPaused = _isPaused; emit IsWithdrawPausedSet(_poolToken, _isPaused); + + pause.isRepayPaused = _isPaused; emit IsRepayPausedSet(_poolToken, _isPaused); + + pause.isLiquidateCollateralPaused = _isPaused; emit IsLiquidateCollateralPausedSet(_poolToken, _isPaused); + + pause.isLiquidateBorrowPaused = _isPaused; emit IsLiquidateBorrowPausedSet(_poolToken, _isPaused); } } diff --git a/src/compound/MorphoGovernance.sol b/src/compound/MorphoGovernance.sol index b0540db86..59be26d22 100644 --- a/src/compound/MorphoGovernance.sol +++ b/src/compound/MorphoGovernance.sol @@ -467,18 +467,24 @@ abstract contract MorphoGovernance is MorphoUtils { Types.MarketPauseStatus storage pause = marketPauseStatus[_poolToken]; pause.isSupplyPaused = _isPaused; + emit IsSupplyPausedSet(_poolToken, _isPaused); + // Note that pause.isDeprecated implies pause.isBorrowPaused. - if (!pause.isDeprecated) pause.isBorrowPaused = _isPaused; - pause.isWithdrawPaused = _isPaused; - pause.isRepayPaused = _isPaused; - pause.isLiquidateCollateralPaused = _isPaused; - pause.isLiquidateBorrowPaused = _isPaused; + if (!pause.isDeprecated) { + pause.isBorrowPaused = _isPaused; + emit IsBorrowPausedSet(_poolToken, _isPaused); + } - emit IsSupplyPausedSet(_poolToken, _isPaused); - emit IsBorrowPausedSet(_poolToken, _isPaused); + pause.isWithdrawPaused = _isPaused; emit IsWithdrawPausedSet(_poolToken, _isPaused); + + pause.isRepayPaused = _isPaused; emit IsRepayPausedSet(_poolToken, _isPaused); + + pause.isLiquidateCollateralPaused = _isPaused; emit IsLiquidateCollateralPausedSet(_poolToken, _isPaused); + + pause.isLiquidateBorrowPaused = _isPaused; emit IsLiquidateBorrowPausedSet(_poolToken, _isPaused); } } From 7ebc3ebda8498ff451d2f7d7cdb5ef243342d24c Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 15:18:23 +0100 Subject: [PATCH 091/105] feat: add CI for autogenerated doc with foundry --- .github/workflows/ci-docs-autogen.yml | 77 +++++++++++++++++++++++++++ .gitignore | 3 ++ Makefile | 3 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-docs-autogen.yml diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml new file mode 100644 index 000000000..7869e0146 --- /dev/null +++ b/.github/workflows/ci-docs-autogen.yml @@ -0,0 +1,77 @@ + +on: + push: + - main + +jobs: + autogen-docs: + steps: + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: yarn + + - name: Install Foundry + uses: onbjerg/foundry-toolchain@v1 + with: + version: nightly + + - name: Install all dependencies + run: make install + shell: bash + + - name: Generate docs + run: make docs + shell: bash + + - name: Upload docs + uses: actions/upload-artifact@v2 + with: + name: docs-foundry + path: docs + + upload-to-ipfs: + needs: autogen-docs + steps: + - name: Download docs + uses: actions/download-artifact@v2 + with: + name: docs-foundry + path: docs + + - name: Upload docs to IPFS + uses: aquiladev/ipfs-action@12cc5d253735dc2894fe19828bd042c8532acc5d # v0.3.1 + with: + path: ./docs + service: pinata + pinataKey: ${{ secrets.PINATA_KEY }} + pinataSecret: ${{ secrets.PINATA_SECRET }} + + upload-to-s3: + needs: autogen-docs + environment: + name: docs + url: https://developers.morpho.xyz + steps: + - name: Download docs + uses: actions/download-artifact@v2 + with: + name: docs-foundry + path: docs + + - name: Setup AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Upload docs to S3 + run: aws s3 sync ./docs s3://$BUCKET --delete + env: + BUCKET: ${{ secrets.AWS_S3_BUCKET }} + + - name: Invalidate CloudFront cache + run: aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths "/*" + env: + DISTRIBUTION: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 32e91d606..b1410ce61 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ yarn-error.log* *.ansi *.html lcov.* + +# docs +/docs \ No newline at end of file diff --git a/Makefile b/Makefile index 9c07ecade..3d4309b75 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,7 @@ storage-layout-check-no-rewards: config: @forge config - +docs: + @forge doc --build .PHONY: test config test-common foundry coverage contracts From eea830c5e466c7da1e043e9701c8efaa57ce47ba Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 15:35:26 +0100 Subject: [PATCH 092/105] fix(ci): change the path to upload --- .github/workflows/ci-docs-autogen.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 7869e0146..482787ba9 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -1,7 +1,9 @@ +name: Autogenerated documentation on: push: - - main + branches: + - main jobs: autogen-docs: @@ -28,7 +30,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: docs-foundry - path: docs + path: docs/book upload-to-ipfs: needs: autogen-docs From c2974d89e0e1f44437c3d5adfab39ec399a564f1 Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 15:51:03 +0100 Subject: [PATCH 093/105] fix(ci): add `runs-on` property & missing setup step --- .github/workflows/ci-docs-autogen.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 482787ba9..58baad67b 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -7,7 +7,12 @@ on: jobs: autogen-docs: + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-node@v3 with: node-version: 16 @@ -18,10 +23,6 @@ jobs: with: version: nightly - - name: Install all dependencies - run: make install - shell: bash - - name: Generate docs run: make docs shell: bash @@ -33,6 +34,7 @@ jobs: path: docs/book upload-to-ipfs: + runs-on: ubuntu-latest needs: autogen-docs steps: - name: Download docs @@ -50,6 +52,7 @@ jobs: pinataSecret: ${{ secrets.PINATA_SECRET }} upload-to-s3: + runs-on: ubuntu-latest needs: autogen-docs environment: name: docs From ea2bdf1f7f09b11b37a5c05bcf4bc8d851a3c073 Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 15:57:43 +0100 Subject: [PATCH 094/105] fix(ci): fix Node cache and installation --- .github/workflows/ci-docs-autogen.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 58baad67b..6bf3a3f92 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -23,6 +23,16 @@ jobs: with: version: nightly + - name: Node dependencies cache + uses: actions/cache@v3 + with: + path: "node_modules" + key: yarn-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + run: yarn install --frozen-lockfile + shell: bash + - name: Generate docs run: make docs shell: bash From 9dec5b2fea4e139822e9697addef93b537f85367 Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 16:03:12 +0100 Subject: [PATCH 095/105] fix(ci): remove upload to ipfs --- .github/workflows/ci-docs-autogen.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 6bf3a3f92..ad2f842c2 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -43,24 +43,6 @@ jobs: name: docs-foundry path: docs/book - upload-to-ipfs: - runs-on: ubuntu-latest - needs: autogen-docs - steps: - - name: Download docs - uses: actions/download-artifact@v2 - with: - name: docs-foundry - path: docs - - - name: Upload docs to IPFS - uses: aquiladev/ipfs-action@12cc5d253735dc2894fe19828bd042c8532acc5d # v0.3.1 - with: - path: ./docs - service: pinata - pinataKey: ${{ secrets.PINATA_KEY }} - pinataSecret: ${{ secrets.PINATA_SECRET }} - upload-to-s3: runs-on: ubuntu-latest needs: autogen-docs From 5f20fc7b3813e2ebee086fbdf938c5544fb12601 Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 16:04:07 +0100 Subject: [PATCH 096/105] fix(ci): add public acl to added files --- .github/workflows/ci-docs-autogen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index ad2f842c2..5c2f9e8ec 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -64,7 +64,7 @@ jobs: aws-region: ${{ secrets.AWS_REGION }} - name: Upload docs to S3 - run: aws s3 sync ./docs s3://$BUCKET --delete + run: aws s3 sync ./docs s3://$BUCKET --delete --acl public-read env: BUCKET: ${{ secrets.AWS_S3_BUCKET }} From 3b29f86b075ee3348a0ae0027733fb093286e9ff Mon Sep 17 00:00:00 2001 From: julien Date: Sat, 4 Mar 2023 16:28:46 +0100 Subject: [PATCH 097/105] fix(ci): remove necessary make script --- .github/workflows/ci-docs-autogen.yml | 2 +- Makefile | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 5c2f9e8ec..1f506bdc3 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -34,7 +34,7 @@ jobs: shell: bash - name: Generate docs - run: make docs + run: forge doc --build shell: bash - name: Upload docs diff --git a/Makefile b/Makefile index 3d4309b75..9c07ecade 100644 --- a/Makefile +++ b/Makefile @@ -112,7 +112,6 @@ storage-layout-check-no-rewards: config: @forge config -docs: - @forge doc --build + .PHONY: test config test-common foundry coverage contracts From 5f9bcbc78e28e3f2498f0d3e42fe25938d53dae6 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 4 Mar 2023 18:22:33 +0100 Subject: [PATCH 098/105] docs: add yellow --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac0601b3c..dab0b186b 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ It also interacts with `RewardsManager`, which manages the underlying pool's rew - [White Paper](https://whitepaper.morpho.xyz) - [Morpho Documentation](https://docs.morpho.xyz) -- Yellow Paper (coming soon) +- [Yellow Paper](https://yellowpaper.morpho.xyz/) --- From cece24b5bd44b0bc7f03d271820823a60aae67d8 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 4 Mar 2023 18:22:53 +0100 Subject: [PATCH 099/105] chore: add audit --- audits/Spearbit_MorphoV1.pdf | Bin 0 -> 263745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audits/Spearbit_MorphoV1.pdf diff --git a/audits/Spearbit_MorphoV1.pdf b/audits/Spearbit_MorphoV1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..294e3809eda4691cf57790262d38359d28d49916 GIT binary patch literal 263745 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 hO&ZTmOEGSdF_riKAoED8x; z+Vx9!I87SiBW#o#8X?}#F+UGGD$qqlYWnftd>z{7>jI8GW#{?LJ0{@xI%xaW(Ay*J z>Zw~?!CKpX%2gRW@zE(0%ZwM0RAfgVx;2`vv!Uk9s{MRSe6LQwG3*KO&bHxAf*)`` z3P1gRnC49YS49L>nMI|mq1O?lXl0i+AD9zgq3?=iZJi=UI{r@d6r1nhS{yczWH@6Z z_rEh}v?bD)VuK8RzF210kNrN{Lp?xY`HE%B(-i~Z$D96d?k+FZ)y2xJEB7U-zx`~e zwb&HizJOv}DMTkJifF5sFJBcNGR0FT4|jU+VquUlZT{)%ulkmJeqxB* zSArn2w;Z|>H}W5@IL|7m|B9ubpU$iBSaQ#If*-M;NX~BM~-gyt*QfL9soUTnNGW zzxJR{sbia(R`X*C&-0bjvaPr_0Vgq+sLp*xARig#A6k6%kRXS z_4oN<9VXH2ID#xU{h~Z*Sx_zk-_?5B_fv_a{z4eh5A`W3^BbW#p!UDI#vf7VUs8*rJ(%b=e0B(+l_T1*>5-2deplD9@ge`g5 zNZ%a_5K1Y=$b742Dd<;?b&``jc?>9}CQ#6^#ce+^#)0{h1c=8$v>yORxgJECehVpk zIXdHup$avn^AL%d2kpc`xM9OdvrsNmlL~i^Ae$J#CGUgtaS5E~ERR`HSJOc7y=Trp zdB52Mca$OTu-yz`v}*=$jXfLXTN*o>5sD2COga=0shH$VTEyg`=C}@sT;zm0+~K=8 zgEK)>4g-cmVSToO+}X05X^*LY`GXr6aw?zKTU)jB(-i$^>hfzwgH)al-lrlq$%Oco z9j)xyg`)%bqh)TzHe2^7@M1DXqM{1>0;J@Cx2t0Bc<(VvQ4))mQg8Ft)QUpl)T`M_ z9yCg3;F#KypjwOu?W>;kbzyltjB21B|1Ulf!R|gSanZOyACAYPzcUOrf4U)y5WS4y zxQO1el@T_|AJn$6bErcbC?359%=C+Cehi#WU2r~Uph^PSibY6pGUp{>Qt7qC0_-zy zETCgjReBY!=P-SJR!0;;IOiaPW8|ztL>D@`-mAp3V5>vqHQhFN zxUZXTYcc8ocWqLj5K&#bPMwTb&T-+VF^t*r7FfbJ8tP;2iP`+q&&>!4s`6X%tTa^9 zbDMV6n8dP=1OTrcyMwdkuc?|4^N>LXaHW-gkT3-mER!^+Pm8n zw{RYa!Fw5^hjL1$OssbfUqD*>5)fCatGLmR&ASNMLJmW=C9~#u%t}caSR$KfR?V=% zO!9C({BY%@a-4B_2_(h#b`lV#K<&Ns-EuH#9{MyWHS)X)`a;JgiUu>r9bMY|yL7#)OiteT@g|OSU}p|76MWY#9$v4o zfO3Erh0;-B9Yu<#PXmb8yWUoJ<@MvYk*q3%a;_7mWsVKsslR&HnjSJ)`0FhqyLZB| zrk|@@FblS^)K?=Hj}4eORAA>u@Gpqy;nOO7b;lpd2fCg}T|EusLQ?sXOfT3J z*e0vuO6Y>49(w(G!>RwzdaBHQtVU4!jeSROyfv=SJiF80yoLHMS$Bb8Oi16?gI>wj z6;x3NN4tHfy0nw29l&#x&_P}M&ntYSJL#AXzyzj^@p0o;39pIB{5AU|(#D}sR%@sKnU{WP%x3Zb3}CQ|k!Q}i{w{SX8N&pB z#2n)_FQ`B{8Zi?6^``s}I`Wtv3fr68>#G1q5y-uUd=g)%SNls?isloRDD5KpQxaqF z1L(I1007dKX&m_Usrg(hzM2e+Jej1=haHZJv^n^%#e*_8yl~=xg9&q}GDSHd z*e2MS_;(l(%3}v?J?*`47#8RtW4-G~rKbAZdm#DvE5qOm<2Ih2E2w24k-bE)1XtzR zO|>Ovv{AYN*ZAxRO{Ld8tH^xGrIJ@7I2M37L5Xn*mYqo6tpGkD-v_MAnlJ#Ij|ImG z3;$~TbL~|bb_vl{9jN>Abt=v6oo3OIH;65lYTL=5z0@)%GQ*`bj+T7340nEAeAm~! zXbM@H38_jxUd1XNcP8EF1TI$l|EO=F{uqd`Sq0-lK=W$IW_o8bTc(_0N$qj(RSpA7 z97(9ICyg1ptwZ|PUgG4QJrx~?ks!TIv=87Jll6BgfY+%O&CoLxgL%^8O7OD{V`w;4 zL!5vQnZ7tiQF&>JE!3ytVJr?`v35oh9`V*C0=i)txw3iMS0+?1*%})UG|^k6sN&%4 zZrlW9t1`frpcaSg(*TkhrwA)Qe5$@a{h!qFhe)7rZov;&N|U79H3mDX-s;qwG1H?d z+g8Ff$1e&pRQgP~)Of01*ol4lL2O7?^BL!2bE7AeQ2w|8TQEya72ng8pJdhb=DA^c zkq|Ib@S$^5q5gY;=%QNcsKfiPhIj#oI)XHlXDFYP@dTZ?KgfPep{-nl{Zw0hy^|=W zs)}hG{7qS=XD3E+hgvMkQ5yiFOyLs37HZFIRW0+GiOm<-9M%plYGaR`zdz|Ss7Rm( z#H!%(m=Mc#ktVAz}V!4 z)~0$+mvfn_X;dl%1XFPAAUeGSMaq6Z`fs^P$rOVaNH6Dps%ZER>5%ZYxeU*Dh%YF6mz#Oa23H-%lW$$3E$u3Y^Gp56S92)z(c320w6Q#*xa)^{0A2$J)QU?GTT+gA)p~=KE9ToMvq(<@qDmZo z2>_+&r^|YDqw^d`8o1eC!cWmh3Rt&$?eBMTJ=Vi7M9HZ#&gis1O)~T~?Mq6sKv~3{ zrWShD-=qlNSTJ3LQy0rTm-qnZ7=z-WEo7({On5f0AfS#hH?5L!Ho{wOVQI1!@ZV=K zOr0OIA;w}g5$2cC>7q!Mri1a;wze6o;d-d3<&93}$q%Gw`u9?Zzu@|5>y;@e8 z)l~9aT#_Kr18#Z;!rwl^r+m#0GGGlK%N~ky2AdDB(vel0d(j=Nf$^ihfv(rHR?Gni zCGvPJp?h6pF(f29B&*GrRC^pJHC%n|6PUc)7wov1SD|ueyLvLnN}f(C!=haMiP^gl zQ>)}$uInTP1I>haE(ItsrQyVczWQ2GsVPH0+NLqk&LR0$z|^trJ$$vj-8tzYq;RuE zI5KF5=Jr6iY?8cdH3u{9pGu$eu@=_YpGHlp{}4K>wf4yb^o3A{*?3h(Yp#j)YpV&6Bmu?{u1>mAIehb(qKZiRb_ zXRMW7Q%PAaa;4V?$R8P~)EWP=Y6`jv1STiTAh5QLk#SK8^Cfxa{OFTH8cJTSzkYth z53>4nc3?zZnT{lFBQ&AKOu}R&{pBp|?ZC2U*uJ8|(AX9oerMxO=INp`Qa16g?vwW5?+me`yFzacg0t{n{MB+_uF8xB4}7k~MLDEmBvv4u37w=K z4<;ShODkhJVb<(-W(S5HE&gy2UIRsLPe3G{9!b{=Na)e8Uj;(y)jKfJTIF-}`Cye%X3IF7p6v6c>B5yMPHs#;W;VpgS3`S(| z<`MtJQ@@+svf_vrUh{|V;|Ed;%rV7~aT2qVJQmpx>V)Z9xu=hr+tyk36=4>`HYe)E zHP)ns^dc_lA|OZ#=XbWL2kkqqR=B1U?zRe zln%^2kj7shs0a$+SCDe;qC=_%7n2!@o#Eb?{iY}arZ?VZXy>w1WI8CKPn}0h^dEE} zQA*%P1CJ|K=!42^DZB;>w}oDb+CK!r$&$Wf=?%S@#S20h{Cn?7n1@f3#=XtgxNVH9qH$=T{?Tv~;Mt9x? zYF6+?(vu)xUz{Se+^S~Vc{S*p4-x;LAWW8sw8XJaBRMPqp*)keWc*S0OB$#TCT~-n zv6_QnU|OED5!%s)GDQcslQ~5Kl01Q}4k<^>)a!Y4ElMZP^q|H=amz1>WOdcHPf9T~ z9c_67HD+Z_*i4HHNjF+h9H8_^xrEDV-J%+!+G=4Ed$A#BJmiGO95&1X^COWku;)E> z%)SRy=a?X09R)tXM>=o?F6}y14&^^l^T(f?p0)$6T6O>juiY*%kc`!C&MnCU9l1T2 zemZt=8yKsl>m3tOn?hYMFfCrD^jqYYFV7)dptI2U_lix5 zzMuPTXP|`>RstBG#WcKeLpqjhr*6yDI0gF%xCOZq=+=7PnI4lGZtHyWnJ zt4m7GG3igb{~tWl!1fv)384pos_9DG2sKhCvNsFaGH`w!LxB1(Z3W0dNd-ijZU2_u zpdb6BL8Zra8-9Z73pB8*rH%=Hs5q8=Xjrv z$IVA$I@OuWC-0mC=8ba?*QRHbzPVqUg|0SxZ~d&PmF#s4s&8*L6s$%u_%o-5S8@%X z5c&8f$3Q5;UF18GcsMEoMM*3|V#uAWn##IRgY@Spt3NS58}%4V+-R@^ig zgVv@C8CXcw@CsC|wQcN~c?e*w)QmZmN4Y!Hpq&c^`$A>^(=|4H&+9btG?%-g!H(R! zVDxPp)?`UeMYvM<^rfXrNWp0hJWL!g$MgRIx7CD+lsl<$FTpDSX1T`A`|?qoq-RqI zqJ{GbA=!r2ai#zVO?%oRTFnim5WYBR&L@swOU?(dP1naE+-=pcA{p`GSAzxu8UASd z<5E^|Y3b7=IzavBN{D+=2s7N%Q2WI_;S@ttWnBHJf51Y?TdL~q5Er0t$ z@v2v@Y`bNkDxd^lIUN(te9b;D_>O4zH&`5zH(>g!w@0t>3_U*f$Se&03nT$gr};2a$G9~KRZIw95D3T19&b98cLVQmU!Ze(v_Y6>?rFd#4> zZ(?c+JUj|7Ol59obZ9XkH!?K}FHB`_XLM*XATu{JFf$4-Ol59obZ9dmFbXeBWo~D5 zXdp8;I506FARr(h3NJ=!Y;HuJ50x&T! zFv5@mgzX(Xoh;2QTmTftl>b%&)NBk*ENv~F0BZI&_HLHO762YMH#Y${XIDBWS3bIb zf|N~70WKD%05eM)Q-H9%f~KULIDkT2P8A?-YG>+XXai7iHL|fZ2FO?%o7y>>QUc8E zod7ofIRK39?My8Hg~^%j-vI2~Or2c*0cPf8ZwrtWRS^`Ems0_V3e&3y0}Sm<05TH) zgxk3|bN%COYV6|juj*(3F8`Hm4F4+`{a5<0)yd;O5*-X9Bf!Mc*acu@YHn!>qDe8yh)8TT=jqu)VE=tBa`aocS9b#4F7um zN7DU&UP40l9^SM}3>*MjW;O-@BQrY-fPHpO7Kj(iBlc}kP zsWHsjvb`~PkX33+NV%VA;cNvs_3X5PD)llW@XA!rjW)97poNM4;T8713U;159Nvt- z5%2Ehj^uqakzuSU-0{R@6k(X|GWKgRafP{G&DOhO{#$*R-ZhvSmaVZI*iHDIOZxg+ zfCAMdOJ>e_jg(_4FS0meltEcXKRD8M-0H%koj9c~IZB4sH=OA{bv4qgMR38j5&!Gn z8c1kf;4a(^DVDxPLAk%fVW?Jq{gsulwx+@yW0wR*uJ!>ImdI8J*|uB7SIppMfZ+WV z^BzRnrn}(sq0Unt{qQ_QL;?5>kNoELAAVUp`CBf?ti@01N*>jS*sUjCmWoc_@Wb61 zw->)@b#)zb$A~{J)8x9_QuCrjFac}=u!r6wwtHMT>(WauGL9Pp-s7AbW2@>YRCwi< z$FuSJo9IN9k!=x)^Vd#i1@F#Syf!MOx&C%1a5BQDA<(0z zZEaEsaOKSJK#v6Ue@D#&^cZ06VlDXN5f*Mtr``9X@Hso1omuyj$;C&7EtxZ#V*Yvy zvhoWiuCVQ&tC%%_kH@Z8eXQa6e7;?v{o*Lwu+!=FQBvKF+N&(HeUGFlQF_0SUnH=QB$dWlKjj!O2N8XgJ4Jq zTzL)`Q&kMe)CFO$1bd?l|3Yb6a_Viq;vT!NC{z)cC#B72X3M)#Wuo32c*-jVB_mTm z;~wCl+{B@??^OO4+}Vfy>E4SK@VF8l8A2|^_-HJbMnP-yz z(64tLl#jLSC6jLtSJ)}lawI$ae(f-9CZ%w&s^j??r}JlOh=BeSX~;GLFD6rS z8H;IpqN1UBl*Fw1=~Owc9y{T}>~HItjtnX$oKG5YQu7xF&OQg!W@j#=z+o3)8HM%% zK5x;%ZfxVAVo_H2qkWIn_xyexDXUF$&61wul;Fs(aI2_svJN%tL?d9Oq$u#(*ejS_ z(;Vqye5xA3eDUJrD$CWC)zxThJHH5y&FPBOuD6CY+LfyYQJr#J^P~lF|?Zu z{(e|tQMU0WNDlqpJXbx5TmDB-rUEJC(H6ExJBD={ro>74CIu`He>LAY&AJsbs1A*% z2tcWzy-|=T8-l#97LzX28gbD|GriK?w`dpe^rJF9n>gKZpvXELAl3`nYi_zQaJREN z0rFSH*Vd>8tt8=MIID7M6#+egwh=bXUYm>!P%@82o;97MHWd_Zq+B?ZdDzY`Y9!5Y zsD^{Ia}Sdr3QbZ_c8ZHOsP7oN6Xho5C7fcnKz&rjbJKD5_P{!^XX)G};)_{3cA9(1fFRiL!gD)`a_35!1=E|U5Re#%(bP?spN*e4` z=dEX3Omw|VB9@Ne)6*-G?~^yM`sfhMOn=3AlUWFYwJsF}W7Us4y(u|GQ7LiH_{ZM7}mWC0{P4ZM0a zwc7fD*6}aiT%}ptXgT2Z5?IBx3MjP*)h$*g^zUQ^tE9hY^Koa$PF1}{y7{F}T{fd8 zH(P{?8A2p>C%>F=L$_YU!9k$Pp12=#UTC#xsAIW;*%9lBOb=Ax&Ij;j!NLqC(ns|{qW$npNOiM`rR?Qb%?WQNhp3;>l zpnR!qH6xu;&PiHvY;*#pf?=Zcg!N!7xF&>UK`)F+aX|PKGhz*p-7KEPB>Bk~%tMcK z0n)yY$G0c{JA-Fus7R-v$CEr+9<+7TW;kkJLR^LS-*Zk!Xw0Z&-5^g2LjO)oF#b$-!7LA(oflcWUIOG6yG;IL(2N#rSb_BAR#SNa)X zQ&DcLh5vel^0tMKID_9_1#$vBF68;XS#A{T2(W|A%RQJPiNEgmGs`&^GRA%?5QSxsLj1UwvKpK zl>FhBQr?f_Hs+{NItHV@uLj#cgz@F$&pWxaDrGjT90W`y#9TZvKTt;HeOb~9{pA_! z;&V8!%5OhLn~!FgjKq&B;e8;N^Mv)mp=y;$O}g`?Hg+(}Z{Bco%<3;~Jg!)M5m&Bz zH1@zEWX()Xz0gt4S6E-=6ze$d3t?NUFvJ`t(@2tdIX@5a=%H$PU3I2Jl+SSHXDbkj z(njFn3u&-bVpgJ)w|tGm-1+{=$16md2=Zage*sd(bu?(Cs_2F}8!>nDMzs)FeRDkU z^}7?UU@Fv?F__cNN0*;-Nj7r$5tMA?(PJx*twX6}dZHYxQ<@R?0^%h06GP|Y;%42v zaeEbnAaYw-A8GrEOELIwNrO(eGUUB$8#-eL zKBx{YrB=lZ>^m8Oigo#5&&k=JhCwFb#{o(0A{dGlSEUQ!l1C@QsV`5(OeM_#r`-f7 zLh~_Nj(W?t^c*hkA&QX+ve^&#Y|Ub0u}Y`;&TtVibZewe$LSKulANz<+W`snGq>@H)6gGY2rdC+a2+FeG))q-Ep!51I0 zR3H6I`S8H$6!x~O!4oHy6mhk4S`7vf+BSaRv`Sq@K$s8NKHjX5nDDJCn?dRAyn4kl zEwb}#G-2IYZkS>#)RyzNaSZ?Nli#=KV^>`h6xt@&7)s$3YGEiJT#z@%4YFY z0qq71?2QdkCgNwT&dJ-feObn0F?&+Nj=!FJL@MqAGz(A}WmA7`!eV&|B_i>(>x
NtZFZ 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⁢fmU3s<|u8c(vHR9T{$!hdS_XeKgojR-I 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 Date: Sat, 4 Mar 2023 21:51:18 +0100 Subject: [PATCH 100/105] fix: add new lines at EOF --- .github/workflows/ci-docs-autogen.yml | 2 +- .gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-docs-autogen.yml b/.github/workflows/ci-docs-autogen.yml index 1f506bdc3..8eea02ba5 100644 --- a/.github/workflows/ci-docs-autogen.yml +++ b/.github/workflows/ci-docs-autogen.yml @@ -71,4 +71,4 @@ jobs: - name: Invalidate CloudFront cache run: aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths "/*" env: - DISTRIBUTION: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} \ No newline at end of file + DISTRIBUTION: ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} diff --git a/.gitignore b/.gitignore index b1410ce61..87882328d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ yarn-error.log* lcov.* # docs -/docs \ No newline at end of file +/docs From 3362abc2af5e7f7d628592a32c0a8fdbd6e495a2 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sun, 5 Mar 2023 21:58:02 +0100 Subject: [PATCH 101/105] docs: add developer doc too --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dab0b186b..ce905625e 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ It also interacts with `RewardsManager`, which manages the underlying pool's rew ## Documentation - [White Paper](https://whitepaper.morpho.xyz) -- [Morpho Documentation](https://docs.morpho.xyz) - [Yellow Paper](https://yellowpaper.morpho.xyz/) +- [Morpho Documentation](https://docs.morpho.xyz) +- [Morpho Developers Documentation](https://developers.morpho.xyz) --- From 458bcc6d7e439dfc4b5b947bfbcac7f699b2f214 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 9 Mar 2023 15:07:56 +0100 Subject: [PATCH 102/105] fix: config Aave-v2 warning remapping --- config/aave-v2/Config.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/aave-v2/Config.sol b/config/aave-v2/Config.sol index 6a8e1b785..705a5e85a 100644 --- a/config/aave-v2/Config.sol +++ b/config/aave-v2/Config.sol @@ -7,7 +7,7 @@ import {ILendingPoolAddressesProvider} from "src/aave-v2/interfaces/aave/ILendin import {IEntryPositionsManager} from "src/aave-v2/interfaces/IEntryPositionsManager.sol"; import {IExitPositionsManager} from "src/aave-v2/interfaces/IExitPositionsManager.sol"; import {IInterestRatesManager} from "src/aave-v2/interfaces/IInterestRatesManager.sol"; -import {ILendingPoolConfigurator} from "../../../test/aave-v2/helpers/ILendingPoolConfigurator.sol"; +import {ILendingPoolConfigurator} from "../../test/aave-v2/helpers/ILendingPoolConfigurator.sol"; import {IMorpho} from "src/aave-v2/interfaces/IMorpho.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; From b37db74a073fb244d533dbd2cad4182100f85f35 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 9 Mar 2023 17:10:30 +0100 Subject: [PATCH 103/105] refactor: use absolute path --- config/aave-v2/Config.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/aave-v2/Config.sol b/config/aave-v2/Config.sol index 705a5e85a..b8d39c9ed 100644 --- a/config/aave-v2/Config.sol +++ b/config/aave-v2/Config.sol @@ -7,7 +7,7 @@ import {ILendingPoolAddressesProvider} from "src/aave-v2/interfaces/aave/ILendin import {IEntryPositionsManager} from "src/aave-v2/interfaces/IEntryPositionsManager.sol"; import {IExitPositionsManager} from "src/aave-v2/interfaces/IExitPositionsManager.sol"; import {IInterestRatesManager} from "src/aave-v2/interfaces/IInterestRatesManager.sol"; -import {ILendingPoolConfigurator} from "../../test/aave-v2/helpers/ILendingPoolConfigurator.sol"; +import {ILendingPoolConfigurator} from "test/aave-v2/helpers/ILendingPoolConfigurator.sol"; import {IMorpho} from "src/aave-v2/interfaces/IMorpho.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; From d7a1a308b9a9ad69cff6ff0cfc47721f9a4f5035 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sun, 19 Mar 2023 17:12:37 +0100 Subject: [PATCH 104/105] chore: update morpho-data-structures version --- .gitmodules | 2 +- lib/morpho-data-structures | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index a31b3d9b0..7347a404d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,7 @@ [submodule "lib/morpho-data-structures"] path = lib/morpho-data-structures url = https://github.com/morpho-dao/morpho-data-structures - branch = upgrade-morpho-1 + branch = morpho-v1 [submodule "lib/morpho-utils"] path = lib/morpho-utils url = https://github.com/morpho-dao/morpho-utils diff --git a/lib/morpho-data-structures b/lib/morpho-data-structures index 7e43ca750..9f0ec723e 160000 --- a/lib/morpho-data-structures +++ b/lib/morpho-data-structures @@ -1 +1 @@ -Subproject commit 7e43ca750054d89f4ce365ca07e2a4ace1340ad1 +Subproject commit 9f0ec723e197c9a917fef0e8b6a5b793119dfc91 From 9ff39c1de8a07dee7b2633ce7f877b429a0352e0 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 20 Mar 2023 09:32:55 +0100 Subject: [PATCH 105/105] docs: update deployment addresses in README --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ce905625e..e2d59b981 100644 --- a/README.md +++ b/README.md @@ -65,24 +65,25 @@ You can also send an email to [security@morpho.xyz](mailto:security@morpho.xyz) ### Morpho-Compound Ethereum - Morpho Proxy: [0x8888882f8f843896699869179fb6e4f7e3b58888](https://etherscan.io/address/0x8888882f8f843896699869179fb6e4f7e3b58888) -- Morpho Implementation: [0xbbb011b923f382543a94e67e1d0c88d9763356e5](https://etherscan.io/address/0xbbb011b923f382543a94e67e1d0c88d9763356e5) -- PositionsManager: [0x309a4505d79fcc59affaba205fdcb880d400ef39](https://etherscan.io/address/0x309a4505d79fcc59affaba205fdcb880d400ef39) -- InterestRatesManager: [0x3e483225666871d192b686c42e6834e217a9871c](https://etherscan.io/address/0x3e483225666871d192b686c42e6834e217a9871c) +- Morpho Implementation: [0xe3d7a242614174ccf9f96bd479c42795d666fc81](https://etherscan.io/address/0xe3d7a242614174ccf9f96bd479c42795d666fc81) +- PositionsManager: [0x79a1b5888009bB4887E00EA27CF52551aAf2A004](https://etherscan.io/address/0x79a1b5888009bB4887E00EA27CF52551aAf2A004) +- InterestRatesManager: [0xD9B7209eD2936b5c06990A8356D155c3665d43Ab](https://etherscan.io/address/0xD9B7209eD2936b5c06990A8356D155c3665d43Ab) - RewardsManager Proxy: [0x78681e63b6f3ad81ecd64aecc404d765b529c80d](https://etherscan.io/address/0x78681e63b6f3ad81ecd64aecc404d765b529c80d) -- RewardsManager Implementation: [0xf47963cc317ebe4b8ebcf30f6e144b7e7e5571b7](https://etherscan.io/address/0xf47963cc317ebe4b8ebcf30f6e144b7e7e5571b7) +- RewardsManager Implementation: [0x581c3816589ad0de7f9c76bc242c97fe96c9f100](https://etherscan.io/address/0x581c3816589ad0de7f9c76bc242c97fe96c9f100) - Lens Proxy: [0x930f1b46e1d081ec1524efd95752be3ece51ef67](https://etherscan.io/address/0x930f1b46e1d081ec1524efd95752be3ece51ef67) -- Lens Implementation: [0xe54dde06d245fadcba50dd786f717d44c341f81b](https://etherscan.io/address/0xe54dde06d245fadcba50dd786f717d44c341f81b) +- Lens Implementation: [0x834632a7c70ddd7badd3d21ba9d885a9da66b0de](https://etherscan.io/address/0x834632a7c70ddd7badd3d21ba9d885a9da66b0de) +- Lens Extension: [0xc5c3bB32c70d1d547023346BD1E32a6c5BC7FD1e](https://etherscan.io/address/0xc5c3bB32c70d1d547023346BD1E32a6c5BC7FD1e) - CompRewardsLens: [0x9e977f745d5ae26c6d47ac5417ee112312873ba7](https://etherscan.io/address/0x9e977f745d5ae26c6d47ac5417ee112312873ba7) ### Morpho-Aave-V2 Ethereum - Morpho Proxy: [0x777777c9898d384f785ee44acfe945efdff5f3e0](https://etherscan.io/address/0x777777c9898d384f785ee44acfe945efdff5f3e0) -- Morpho Implementation: [0x206a1609a484db5129ca118f138e5a8abb9c61e0](https://etherscan.io/address/0x206a1609a484db5129ca118f138e5a8abb9c61e0) -- EntryPositionsManager: [0x2a46cad23484c15f60663ece368395b3a249632a](https://etherscan.io/address/0x2a46cad23484c15f60663ece368395b3a249632a) -- ExitPositionsManager: [0xfa652aa169c23277a941cf2d23d2d707fda60ed9](https://etherscan.io/address/0xfa652aa169c23277a941cf2d23d2d707fda60ed9) -- InterestRatesManager: [0x4f54235e17eb8dcdfc941a77e7734a537f7bed86](https://etherscan.io/address/0x4f54235e17eb8dcdfc941a77e7734a537f7bed86) +- Morpho Implementation: [0xFBc7693f114273739C74a3FF028C13769C49F2d0](https://etherscan.io/address/0xFBc7693f114273739C74a3FF028C13769C49F2d0) +- EntryPositionsManager: [0x029Ee1AF5BafC481f9E8FBeD5164253f1266B968](https://etherscan.io/address/0x029Ee1AF5BafC481f9E8FBeD5164253f1266B968) +- ExitPositionsManager: [0xfd9b1Ad429667D27cE666EA800f828B931A974D2](https://etherscan.io/address/0xfd9b1Ad429667D27cE666EA800f828B931A974D2) +- InterestRatesManager: [0x22a4ecf5195c87605ae6bad413ae79d5c4170ff1](https://etherscan.io/address/0x22a4ecf5195c87605ae6bad413ae79d5c4170ff1) - Lens Proxy: [0x507fa343d0a90786d86c7cd885f5c49263a91ff4](https://etherscan.io/address/0x507fa343d0a90786d86c7cd885f5c49263a91ff4) -- Lens Implementation: [0xce23e457fb01454b8c59e31f4f72e4bd3d29b5eb](https://etherscan.io/address/0xce23e457fb01454b8c59e31f4f72e4bd3d29b5eb) +- Lens Implementation: [0x4bf26012b64312b462bf70f2e42d1be8881d0f84](https://etherscan.io/address/0x4bf26012b64312b462bf70f2e42d1be8881d0f84) ### Common Ethereum