diff --git a/src/MorphoInternal.sol b/src/MorphoInternal.sol index b8349b76b..089f2c827 100644 --- a/src/MorphoInternal.sol +++ b/src/MorphoInternal.sol @@ -281,6 +281,8 @@ abstract contract MorphoInternal is MorphoStorage { view returns (uint256 borrowable, uint256 maxDebt) { + if (!_market[underlying].isCollateral) return (0, 0); + (uint256 underlyingPrice, uint256 ltv, uint256 liquidationThreshold, uint256 underlyingUnit) = _assetLiquidityData(underlying, vars); diff --git a/src/MorphoSetters.sol b/src/MorphoSetters.sol index a9de468c3..0a58e99a3 100644 --- a/src/MorphoSetters.sol +++ b/src/MorphoSetters.sol @@ -9,6 +9,9 @@ import {Events} from "./libraries/Events.sol"; import {Errors} from "./libraries/Errors.sol"; import {MarketLib} from "./libraries/MarketLib.sol"; +import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; +import {UserConfiguration} from "@aave-v3-core/protocol/libraries/configuration/UserConfiguration.sol"; + import {MorphoInternal} from "./MorphoInternal.sol"; /// @title MorphoSetters @@ -18,6 +21,8 @@ import {MorphoInternal} from "./MorphoInternal.sol"; abstract contract MorphoSetters is IMorphoSetters, MorphoInternal { using MarketLib for Types.Market; + using UserConfiguration for DataTypes.UserConfigurationMap; + /* MODIFIERS */ /// @notice Prevents to update a market not created yet. @@ -74,6 +79,32 @@ abstract contract MorphoSetters is IMorphoSetters, MorphoInternal { emit Events.TreasuryVaultSet(treasuryVault); } + /// @notice Sets the `underlying` asset as `isCollateral` on the pool. + /// @dev The following invariant must hold: is collateral on Morpho => is collateral on pool. + function setAssetIsCollateralOnPool(address underlying, bool isCollateral) + external + onlyOwner + isMarketCreated(underlying) + { + if (_market[underlying].isCollateral) revert Errors.AssetIsCollateralOnMorpho(); + + _pool.setUserUseReserveAsCollateral(underlying, isCollateral); + } + + /// @notice Sets the `underlying` asset as `isCollateral` on Morpho. + /// @dev The following invariant must hold: is collateral on Morpho => is collateral on pool. + function setAssetIsCollateral(address underlying, bool isCollateral) + external + onlyOwner + isMarketCreated(underlying) + { + if (!_pool.getUserConfiguration(address(this)).isUsingAsCollateral(_pool.getReserveData(underlying).id)) { + revert Errors.AssetNotCollateralOnPool(); + } + + _market[underlying].setAssetIsCollateral(isCollateral); + } + /// @notice Sets the `underlying`'s reserve factor to `newReserveFactor` (in bps). function setReserveFactor(address underlying, uint16 newReserveFactor) external diff --git a/src/PositionsManagerInternal.sol b/src/PositionsManagerInternal.sol index e070ba5ff..69f7a3a7a 100644 --- a/src/PositionsManagerInternal.sol +++ b/src/PositionsManagerInternal.sol @@ -21,6 +21,7 @@ import {LogarithmicBuckets} from "@morpho-data-structures/LogarithmicBuckets.sol import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {DataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol"; +import {UserConfiguration} from "@aave-v3-core/protocol/libraries/configuration/UserConfiguration.sol"; import {ReserveConfiguration} from "@aave-v3-core/protocol/libraries/configuration/ReserveConfiguration.sol"; import {ERC20} from "@solmate/tokens/ERC20.sol"; @@ -44,6 +45,7 @@ abstract contract PositionsManagerInternal is MatchingEngine { using EnumerableSet for EnumerableSet.AddressSet; using LogarithmicBuckets for LogarithmicBuckets.Buckets; + using UserConfiguration for DataTypes.UserConfigurationMap; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; /// @dev Validates the manager's permission. @@ -91,6 +93,7 @@ abstract contract PositionsManagerInternal is MatchingEngine { function _validateSupplyCollateral(address underlying, uint256 amount, address user) internal view { Types.Market storage market = _validateInput(underlying, amount, user); if (market.isSupplyCollateralPaused()) revert Errors.SupplyCollateralIsPaused(); + if (!market.isCollateral) revert Errors.AssetNotCollateralOnMorpho(); } /// @dev Validates a borrow action. diff --git a/src/interfaces/IMorpho.sol b/src/interfaces/IMorpho.sol index d36bf2529..014bcdf4b 100644 --- a/src/interfaces/IMorpho.sol +++ b/src/interfaces/IMorpho.sol @@ -53,6 +53,8 @@ interface IMorphoSetters { function setP2PIndexCursor(address underlying, uint16 p2pIndexCursor) external; function setReserveFactor(address underlying, uint16 newReserveFactor) external; + function setAssetIsCollateralOnPool(address underlying, bool isCollateral) external; + function setAssetIsCollateral(address underlying, bool isCollateral) external; function setIsClaimRewardsPaused(bool isPaused) external; function setIsPaused(address underlying, bool isPaused) external; function setIsPausedForAllMarkets(bool isPaused) external; diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 449382271..617b2196b 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -41,6 +41,10 @@ library Errors { error UnauthorizedLiquidate(); error SentinelLiquidateNotEnabled(); + error AssetNotCollateralOnPool(); + error AssetNotCollateralOnMorpho(); + error AssetIsCollateralOnMorpho(); + error ExceedsMaxBasisPoints(); error InvalidValueS(); diff --git a/src/libraries/Events.sol b/src/libraries/Events.sol index 553730c9e..8ab0d4bb2 100644 --- a/src/libraries/Events.sol +++ b/src/libraries/Events.sol @@ -90,6 +90,8 @@ library Events { address indexed claimer, address indexed onBehalf, address indexed rewardToken, uint256 amountClaimed ); + event IsCollateralSet(address indexed underlying, bool isCollateral); + event IsClaimRewardsPausedSet(bool isPaused); event IsSupplyPausedSet(address indexed underlying, bool isPaused); diff --git a/src/libraries/MarketLib.sol b/src/libraries/MarketLib.sol index acf61bd24..5a870c3d6 100644 --- a/src/libraries/MarketLib.sol +++ b/src/libraries/MarketLib.sol @@ -73,6 +73,12 @@ library MarketLib { return market.pauseStatuses.isP2PDisabled; } + function setAssetIsCollateral(Types.Market storage market, bool isCollateral) internal { + market.isCollateral = isCollateral; + + emit Events.IsCollateralSet(market.underlying, isCollateral); + } + function setIsSupplyPaused(Types.Market storage market, bool isPaused) internal { market.pauseStatuses.isSupplyPaused = isPaused; diff --git a/src/libraries/Types.sol b/src/libraries/Types.sol index 7a6fca176..f4ee7e056 100644 --- a/src/libraries/Types.sol +++ b/src/libraries/Types.sol @@ -67,6 +67,7 @@ library Types { // SLOT 6 address underlying; // 160 bits PauseStatuses pauseStatuses; // 80 bits + bool isCollateral; // 8 bits // SLOT 7 address variableDebtToken; // 160 bits uint32 lastUpdateTimestamp; // 32 bits diff --git a/test/helpers/IntegrationTest.sol b/test/helpers/IntegrationTest.sol index 17edd1ab4..a7b6cdaa6 100644 --- a/test/helpers/IntegrationTest.sol +++ b/test/helpers/IntegrationTest.sol @@ -59,9 +59,7 @@ contract IntegrationTest is ForkTest { _createTestMarket(allUnderlyings[i], 0, 33_33); } - // Supply dust to make UserConfigurationMap.isUsingAsCollateralOne() always return true. - _deposit(testMarkets[weth], 1e12, address(morpho)); - _deposit(testMarkets[dai], 1e12, address(morpho)); + _setAllAssetsAsCollateral(); _forward(1); // All markets are outdated in Morpho's storage. @@ -165,6 +163,18 @@ contract IntegrationTest is ForkTest { morpho.createMarket(market.underlying, market.reserveFactor, market.p2pIndexCursor); } + function _setAllAssetsAsCollateral() internal { + for (uint256 i; i < allUnderlyings.length; ++i) { + _setAssetAsCollateral(testMarkets[allUnderlyings[i]]); + } + } + + function _setAssetAsCollateral(TestMarket storage market) internal { + // Supply dust to make UserConfigurationMap.isUsingAsCollateralOne() return true. + _deposit(market, (10 ** market.decimals) / 1e6, address(morpho)); + morpho.setAssetIsCollateral(market.underlying, true); + } + function _randomCollateral(uint256 seed) internal view returns (address) { return collateralUnderlyings[seed % collateralUnderlyings.length]; } @@ -212,11 +222,18 @@ contract IntegrationTest is ForkTest { internal bypassSupplyCap(market, amount) { - _deal(market.underlying, address(this), amount); + deal(market.underlying, address(this), type(uint256).max); ERC20(market.underlying).approve(address(pool), amount); pool.deposit(market.underlying, amount, onBehalf, 0); } + /// @dev Deposits the given amount of tokens on behalf of the given address, on AaveV3. + function _depositSimple(address underlying, uint256 amount, address onBehalf) internal { + deal(underlying, address(this), amount); + ERC20(underlying).approve(address(pool), amount); + pool.deposit(underlying, amount, onBehalf, 0); + } + /// @dev Bounds the input supply cap of AaveV3 so that it is exceeded after having deposited a given amount function _boundSupplyCapExceeded(TestMarket storage market, uint256 amount, uint256 supplyCap) internal diff --git a/test/integration/TestIntegrationAssetAsCollateral.sol b/test/integration/TestIntegrationAssetAsCollateral.sol new file mode 100644 index 000000000..05d2f406e --- /dev/null +++ b/test/integration/TestIntegrationAssetAsCollateral.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {IMorpho} from "src/interfaces/IMorpho.sol"; + +import {Errors} from "src/libraries/Errors.sol"; +import {UserConfiguration} from "@aave-v3-core/protocol/libraries/configuration/UserConfiguration.sol"; + +import {Morpho} from "src/Morpho.sol"; + +import {InvariantTest as ForgeInvariantTest} from "@forge-std/InvariantTest.sol"; +import "test/helpers/IntegrationTest.sol"; + +contract TestIntegrationAssetAsCollateral is IntegrationTest, ForgeInvariantTest { + using UserConfiguration for DataTypes.UserConfigurationMap; + + function setUp() public override { + super.setUp(); + + // Deposit LINK dust so that setting LINK as collateral does not revert on the pool. + _depositSimple(link, 1e12, address(morpho)); + + morpho.setAssetIsCollateral(dai, false); + morpho.setAssetIsCollateral(usdc, false); + morpho.setAssetIsCollateral(aave, false); + morpho.setAssetIsCollateral(wbtc, false); + morpho.setAssetIsCollateral(weth, false); + + vm.startPrank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, false); + pool.setUserUseReserveAsCollateral(usdc, false); + pool.setUserUseReserveAsCollateral(aave, false); + pool.setUserUseReserveAsCollateral(wbtc, false); + pool.setUserUseReserveAsCollateral(weth, false); + pool.setUserUseReserveAsCollateral(link, false); + vm.stopPrank(); + + targetSender(address(this)); + targetContract(address(morpho)); + + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = this.setAssetIsCollateralOnPool.selector; + selectors[1] = this.setAssetIsCollateral.selector; + + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + } + + function setAssetIsCollateralOnPool(bool isCollateral) external { + morpho.setAssetIsCollateralOnPool(dai, isCollateral); + } + + function setAssetIsCollateral(bool isCollateral) external { + morpho.setAssetIsCollateral(dai, isCollateral); + } + + function invariantAssetAsCollateral() public { + if (morpho.market(dai).isCollateral) assertTrue(_isUsingAsCollateral(dai)); + if (!_isUsingAsCollateral(dai)) assertFalse(morpho.market(dai).isCollateral); + } + + function testSetAssetIsCollateralShouldRevertWhenMarketNotCreated(address underlying) public { + vm.expectRevert(Errors.MarketNotCreated.selector); + morpho.setAssetIsCollateral(underlying, true); + } + + function testSetAssetIsCollateralShouldRevertWhenMarketNotCollateralOnPool() public { + assertEq(_isUsingAsCollateral(dai), false); + + vm.expectRevert(Errors.AssetNotCollateralOnPool.selector); + morpho.setAssetIsCollateral(dai, true); + + vm.expectRevert(Errors.AssetNotCollateralOnPool.selector); + morpho.setAssetIsCollateral(dai, false); + } + + function testSetAssetIsCollateral() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + + morpho.setAssetIsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, true); + assertEq(_isUsingAsCollateral(dai), true); + } + + function testSetAssetIsNotCollateral() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + + morpho.setAssetIsCollateral(dai, false); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + } + + function testSetAssetIsCollateralOnPoolShouldRevertWhenMarketIsNotCreated() public { + assertEq(morpho.market(link).isCollateral, false); + assertEq(pool.getUserConfiguration(address(morpho)).isUsingAsCollateral(pool.getReserveData(link).id), false); + + vm.expectRevert(Errors.MarketNotCreated.selector); + morpho.setAssetIsCollateralOnPool(link, true); + + vm.expectRevert(Errors.MarketNotCreated.selector); + morpho.setAssetIsCollateralOnPool(link, false); + } + + function testSetAssetIsCollateralOnPoolShouldRevertWhenMarketIsCollateralOnMorpho() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + morpho.setAssetIsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, true); + assertEq(_isUsingAsCollateral(dai), true); + + vm.expectRevert(Errors.AssetIsCollateralOnMorpho.selector); + morpho.setAssetIsCollateralOnPool(dai, false); + } + + function testSetAssetIsCollateralOnPoolShouldRevertWhenMarketIsCreatedAndIsCollateralOnMorpho() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + morpho.setAssetIsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, true); + assertEq(_isUsingAsCollateral(dai), true); + + vm.expectRevert(Errors.AssetIsCollateralOnMorpho.selector); + morpho.setAssetIsCollateralOnPool(dai, true); + + vm.expectRevert(Errors.AssetIsCollateralOnMorpho.selector); + morpho.setAssetIsCollateralOnPool(dai, false); + } + + function testSetAssetIsCollateralOnPoolWhenMarketIsCreatedAndIsNotCollateralOnMorphoOnly() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + + morpho.setAssetIsCollateralOnPool(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + } + + function testSetAssetIsCollateralOnPoolWhenMarketIsCreatedAndIsNotCollateral() public { + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), false); + + morpho.setAssetIsCollateralOnPool(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + } + + function testSetAssetIsNotCollateralOnPoolWhenMarketIsCreatedAndIsNotCollateralOnMorphoOnly() public { + vm.prank(address(morpho)); + pool.setUserUseReserveAsCollateral(dai, true); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), true); + + morpho.setAssetIsCollateralOnPool(dai, false); + + assertEq(morpho.market(dai).isCollateral, false); + assertEq(_isUsingAsCollateral(dai), false); + } + + function testSetAssetIsCollateralLifecycle() public { + morpho.setAssetIsCollateralOnPool(dai, true); + morpho.setAssetIsCollateral(dai, true); + + morpho.setAssetIsCollateral(dai, false); + morpho.setAssetIsCollateralOnPool(dai, false); + } + + function _isUsingAsCollateral(address underlying) internal view returns (bool) { + return pool.getUserConfiguration(address(morpho)).isUsingAsCollateral(pool.getReserveData(underlying).id); + } +} diff --git a/test/internal/TestInternalMorphoInternal.sol b/test/internal/TestInternalMorphoInternal.sol index bdc2ee610..518e4ec95 100644 --- a/test/internal/TestInternalMorphoInternal.sol +++ b/test/internal/TestInternalMorphoInternal.sol @@ -393,9 +393,23 @@ contract TestInternalMorphoInternal is InternalTest { assertEq(units, 10 ** poolDecimals, "units not equal to pool decimals 2"); } + function testCollateralDataNoCollateral(uint256 amount) public { + amount = bound(amount, 0, 1_000_000 ether); + + _marketBalances[dai].collateral[address(1)] = amount.rayDivUp(_market[dai].indexes.supply.poolIndex); + + DataTypes.EModeCategory memory eModeCategory = _pool.getEModeCategoryData(0); + Types.LiquidityVars memory vars = Types.LiquidityVars(address(1), oracle, eModeCategory); + + (uint256 borrowable, uint256 maxDebt) = _collateralData(dai, vars); + assertEq(borrowable, 0, "borrowable != 0"); + assertEq(maxDebt, 0, "maxDebt != 0"); + } + function testLiquidityDataCollateral(uint256 amount) public { amount = bound(amount, 0, 1_000_000 ether); + _market[dai].isCollateral = true; _marketBalances[dai].collateral[address(1)] = amount.rayDivUp(_market[dai].indexes.supply.poolIndex); DataTypes.EModeCategory memory eModeCategory = _pool.getEModeCategoryData(0); diff --git a/test/internal/TestInternalPositionsManagerInternal.sol b/test/internal/TestInternalPositionsManagerInternal.sol index 0990d22dd..637104333 100644 --- a/test/internal/TestInternalPositionsManagerInternal.sol +++ b/test/internal/TestInternalPositionsManagerInternal.sol @@ -102,7 +102,13 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI this.validateSupplyCollateral(dai, 1, address(1)); } - function testValidateSupplyCollateral() public view { + function testValidateSupplyCollateralShouldRevertIfNotCollateral() public { + vm.expectRevert(abi.encodeWithSelector(Errors.AssetNotCollateralOnMorpho.selector)); + this.validateSupplyCollateral(dai, 1, address(1)); + } + + function testValidateSupplyCollateral() public { + _market[dai].isCollateral = true; this.validateSupplyCollateral(dai, 1, address(1)); } @@ -212,6 +218,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI } function testAuthorizeLiquidateShouldReturnMaxCloseFactorIfDeprecatedBorrow() public { + _market[dai].isCollateral = true; _userCollaterals[address(this)].add(dai); _userBorrows[address(this)].add(dai); _market[dai].pauseStatuses.isDeprecated = true; @@ -242,6 +249,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI } function testAuthorizeLiquidateShouldRevertIfSentinelDisallows(address borrower, uint256 healthFactor) public { + _market[dai].isCollateral = true; borrower = _boundAddressNotZero(borrower); healthFactor = bound( healthFactor, @@ -258,6 +266,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI } function testAuthorizeLiquidateShouldRevertIfBorrowerHealthy(address borrower, uint256 healthFactor) public { + _market[dai].isCollateral = true; borrower = _boundAddressNotZero(borrower); healthFactor = bound(healthFactor, Constants.DEFAULT_LIQUIDATION_MAX_HF.percentAdd(1), type(uint128).max); @@ -270,6 +279,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI function testAuthorizeLiquidateShouldReturnMaxCloseFactorIfBelowMinThreshold(address borrower, uint256 healthFactor) public { + _market[dai].isCollateral = true; borrower = _boundAddressNotZero(borrower); healthFactor = bound(healthFactor, 0, Constants.DEFAULT_LIQUIDATION_MIN_HF.percentSub(1)); @@ -283,6 +293,7 @@ contract TestInternalPositionsManagerInternal is InternalTest, PositionsManagerI address borrower, uint256 healthFactor ) public { + _market[dai].isCollateral = true; borrower = _boundAddressNotZero(borrower); healthFactor = bound( healthFactor, diff --git a/test/unit/TestUnitMarketLib.sol b/test/unit/TestUnitMarketLib.sol index 6c8eb6edb..2ebf70cd2 100644 --- a/test/unit/TestUnitMarketLib.sol +++ b/test/unit/TestUnitMarketLib.sol @@ -292,4 +292,13 @@ contract TestUnitMarketLib is BaseTest { ); assertLe(proportionIdle, WadRayMath.RAY); } + + function testSetAssetIsCollateral(bool isCollateral) public { + vm.expectEmit(true, true, true, true); + emit Events.IsCollateralSet(market.underlying, isCollateral); + + market.setAssetIsCollateral(isCollateral); + + assertEq(market.isCollateral, isCollateral); + } }