Skip to content

Commit

Permalink
Feat/morpho is borrowing any (#123)
Browse files Browse the repository at this point in the history
* Add test demonstrating the 1 wei issue

* Add isBorrowingLogic to Morpho Aave V2 adaptor

* Add Health Factor checks to Morpho Aave V3 borrows and withdraws. Also add tests

* Add health factor checks to Aave V2 morpho withdraws and borrows. Also add tests

* Add missing modifier to new test
  • Loading branch information
crispymangoes committed Jun 16, 2023
1 parent 569d676 commit ac9547f
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 80 deletions.
6 changes: 6 additions & 0 deletions src/interfaces/external/Morpho/IMorphoLensV2.sol
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

interface IMorphoLensV2 {
function getUserHealthFactor(address user) external view returns (uint256);
}
66 changes: 49 additions & 17 deletions src/modules/adaptors/Morpho/MorphoAaveV2ATokenAdaptor.sol
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.16;

import { BaseAdaptor, ERC20, SafeTransferLib, Math } from "src/modules/adaptors/BaseAdaptor.sol";
import { IMorphoV2 } from "src/interfaces/external/Morpho/IMorphoV2.sol";
import { IMorphoLensV2 } from "src/interfaces/external/Morpho/IMorphoLensV2.sol";
import { MorphoRewardHandler } from "src/modules/adaptors/Morpho/MorphoRewardHandler.sol";
import { IAaveToken } from "src/interfaces/external/IAaveToken.sol";

Expand Down Expand Up @@ -30,6 +31,18 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
// negatively affect the cellars health factor.
//====================================================================

/**
* @notice Bit mask used to determine if a cellar has any open borrow positions
* by getting the cellar's userMarkets, and performing an AND operation
* with the borrow mask.
*/
bytes32 public constant BORROWING_MASK = 0x5555555555555555555555555555555555555555555555555555555555555555;

/**
@notice Attempted withdraw would lower Cellar health factor too low.
*/
error MorphoAaveV2ATokenAdaptor__HealthFactorTooLow();

//============================================ Global Functions ===========================================
/**
* @dev Identifier unique to this adaptor for a shared registry.
Expand All @@ -48,6 +61,20 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
return IMorphoV2(0x777777c9898D384F785Ee44Acfe945efDFf5f3E0);
}

/**
* @notice The Morpho Aave V2 Lens contract on Ethereum Mainnet.
*/
function morphoLens() internal pure returns (IMorphoLensV2) {
return IMorphoLensV2(0x507fA343d0A90786d86C7cd885f5C49263A91FF4);
}

/**
* @notice Minimum Health Factor enforced after every withdraw.
*/
function HFMIN() internal pure returns (uint256) {
return 1.05e18;
}

//============================================ Implement Base Functions ===========================================
/**
* @notice Cellar must approve Morpho to spend its assets, then call supply to lend its assets.
Expand All @@ -73,20 +100,13 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
* @param assets the amount of assets to withdraw from Morpho
* @param receiver the address to send withdrawn assets to
* @param adaptorData adaptor data containing the abi encoded aToken
* @param configData abi encoded bool indicating whether the position is liquid or not
*/
function withdraw(
uint256 assets,
address receiver,
bytes memory adaptorData,
bytes memory configData
) public override {
*/
function withdraw(uint256 assets, address receiver, bytes memory adaptorData, bytes memory) public override {
// Run external receiver check.
_externalReceiverCheck(receiver);

// Make sure position is setup to be liquid.
bool isLiquid = abi.decode(configData, (bool));
if (!isLiquid) revert BaseAdaptor__UserWithdrawsNotAllowed();
// Make sure position is not backing a borrow.
if (isBorrowingAny(address(this))) revert BaseAdaptor__UserWithdrawsNotAllowed();

IAaveToken aToken = abi.decode(adaptorData, (IAaveToken));

Expand All @@ -97,12 +117,10 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
/**
* @notice Uses configuration data to determine if the position is liquid or not.
*/
function withdrawableFrom(
bytes memory adaptorData,
bytes memory configData
) public view override returns (uint256) {
bool isLiquid = abi.decode(configData, (bool));
if (!isLiquid) return 0;
function withdrawableFrom(bytes memory adaptorData, bytes memory) public view override returns (uint256) {
// If position is backing a borrow, then return 0.
// else return the balance of in underlying.
if (isBorrowingAny(msg.sender)) return 0;
else {
address aToken = abi.decode(adaptorData, (address));
return _balanceOfInUnderlying(aToken, msg.sender);
Expand Down Expand Up @@ -156,6 +174,10 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
*/
function withdrawFromAaveV2Morpho(IAaveToken aToken, uint256 amountToWithdraw) public {
morpho().withdraw(address(aToken), amountToWithdraw, address(this));

// Check that health factor is above adaptor minimum.
uint256 healthFactor = morphoLens().getUserHealthFactor(address(this));
if (healthFactor < HFMIN()) revert MorphoAaveV2ATokenAdaptor__HealthFactorTooLow();
}

/**
Expand All @@ -172,4 +194,14 @@ contract MorphoAaveV2ATokenAdaptor is BaseAdaptor, MorphoRewardHandler {
if (onPool > 0) balanceInUnderlying += onPool.mulDivDown(morpho().poolIndexes(poolToken).poolSupplyIndex, 1e27);
return balanceInUnderlying;
}

/**
* @dev Returns if a user has been borrowing from any market.
* @param user The address to check if it is borrowing or not.
* @return True if the user has been borrowing on any market, false otherwise.
*/
function isBorrowingAny(address user) public view returns (bool) {
bytes32 userMarkets = morpho().userMarkets(user);
return userMarkets & BORROWING_MASK != 0;
}
}
29 changes: 24 additions & 5 deletions src/modules/adaptors/Morpho/MorphoAaveV2DebtTokenAdaptor.sol
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.16;

import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, Registry, Math } from "src/modules/adaptors/BaseAdaptor.sol";
import { IMorphoV2 } from "src/interfaces/external/Morpho/IMorphoV2.sol";
import { IMorphoLensV2 } from "src/interfaces/external/Morpho/IMorphoLensV2.sol";
import { IAaveToken } from "src/interfaces/external/IAaveToken.sol";

/**
Expand All @@ -28,6 +29,11 @@ contract MorphoAaveV2DebtTokenAdaptor is BaseAdaptor {
*/
error MorphoAaveV2DebtTokenAdaptor__DebtPositionsMustBeTracked(address untrackedDebtPosition);

/**
@notice Attempted borrow would lower Cellar health factor too low.
*/
error MorphoAaveV2DebtTokenAdaptor__HealthFactorTooLow();

//============================================ Global Functions ===========================================
/**
* @dev Identifier unique to this adaptor for a shared registry.
Expand All @@ -46,6 +52,20 @@ contract MorphoAaveV2DebtTokenAdaptor is BaseAdaptor {
return IMorphoV2(0x777777c9898D384F785Ee44Acfe945efDFf5f3E0);
}

/**
* @notice The Morpho Aave V2 Lens contract on Ethereum Mainnet.
*/
function morphoLens() internal pure returns (IMorphoLensV2) {
return IMorphoLensV2(0x507fA343d0A90786d86C7cd885f5C49263A91FF4);
}

/**
* @notice Minimum Health Factor enforced after every borrow.
*/
function HFMIN() internal pure returns (uint256) {
return 1.05e18;
}

//============================================ Implement Base Functions ===========================================

/**
Expand Down Expand Up @@ -110,6 +130,10 @@ contract MorphoAaveV2DebtTokenAdaptor is BaseAdaptor {

// Borrow from morpho.
morpho().borrow(aToken, amountToBorrow);

// Check that health factor is above adaptor minimum.
uint256 healthFactor = morphoLens().getUserHealthFactor(address(this));
if (healthFactor < HFMIN()) revert MorphoAaveV2DebtTokenAdaptor__HealthFactorTooLow();
}

/**
Expand All @@ -119,11 +143,6 @@ contract MorphoAaveV2DebtTokenAdaptor is BaseAdaptor {
*/
function repayAaveV2MorphoDebt(IAaveToken aToken, uint256 amountToRepay) public {
ERC20 underlying = ERC20(aToken.UNDERLYING_ASSET_ADDRESS());
if (amountToRepay == type(uint256).max) {
uint256 availableUnderlying = underlying.balanceOf(address(this));
uint256 debt = _balanceOfInUnderlying(address(aToken), address(this));
amountToRepay = availableUnderlying > debt ? debt : availableUnderlying;
}
underlying.safeApprove(address(morpho()), amountToRepay);
morpho().repay(address(aToken), amountToRepay);

Expand Down
Expand Up @@ -4,13 +4,14 @@ pragma solidity 0.8.16;
import { BaseAdaptor, ERC20, SafeTransferLib } from "src/modules/adaptors/BaseAdaptor.sol";
import { IMorphoV3 } from "src/interfaces/external/Morpho/IMorphoV3.sol";
import { MorphoRewardHandler } from "src/modules/adaptors/Morpho/MorphoRewardHandler.sol";
import { MorphoAaveV3HealthFactorLogic } from "src/modules/adaptors/Morpho/MorphoAaveV3HealthFactorLogic.sol";

/**
* @title Morpho Aave V3 aToken Adaptor
* @notice Allows Cellars to interact with Morpho Aave V3 positions.
* @author crispymangoes
*/
contract MorphoAaveV3ATokenCollateralAdaptor is BaseAdaptor, MorphoRewardHandler {
contract MorphoAaveV3ATokenCollateralAdaptor is BaseAdaptor, MorphoRewardHandler, MorphoAaveV3HealthFactorLogic {
using SafeTransferLib for ERC20;

//==================== Adaptor Data Specification ====================
Expand All @@ -21,6 +22,11 @@ contract MorphoAaveV3ATokenCollateralAdaptor is BaseAdaptor, MorphoRewardHandler
// NA
//====================================================================

/**
@notice Attempted withdraw would lower Cellar health factor too low.
*/
error MorphoAaveV3ATokenCollateralAdaptor__HealthFactorTooLow();

//============================================ Global Functions ===========================================
/**
* @dev Identifier unique to this adaptor for a shared registry.
Expand All @@ -39,6 +45,13 @@ contract MorphoAaveV3ATokenCollateralAdaptor is BaseAdaptor, MorphoRewardHandler
return IMorphoV3(0x33333aea097c193e66081E930c33020272b33333);
}

/**
* @notice Minimum Health Factor enforced after every withdraw.
*/
function HFMIN() internal pure returns (uint256) {
return 1.05e18;
}

//============================================ Implement Base Functions ===========================================
/**
* @notice Cellar must approve Morpho to spend its assets, then call supplyCollateral to lend its assets.
Expand Down Expand Up @@ -137,5 +150,9 @@ contract MorphoAaveV3ATokenCollateralAdaptor is BaseAdaptor, MorphoRewardHandler
*/
function withdrawFromAaveV3Morpho(ERC20 tokenToWithdraw, uint256 amountToWithdraw) public {
morpho().withdrawCollateral(address(tokenToWithdraw), amountToWithdraw, address(this), address(this));

// Check that health factor is above adaptor minimum.
uint256 healthFactor = _getUserHealthFactor(morpho(), address(this));
if (healthFactor < HFMIN()) revert MorphoAaveV3ATokenCollateralAdaptor__HealthFactorTooLow();
}
}
19 changes: 3 additions & 16 deletions src/modules/adaptors/Morpho/MorphoAaveV3DebtTokenAdaptor.sol
Expand Up @@ -3,13 +3,14 @@ pragma solidity 0.8.16;

import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, Registry, Math } from "src/modules/adaptors/BaseAdaptor.sol";
import { IMorphoV3 } from "src/interfaces/external/Morpho/IMorphoV3.sol";
import { MorphoAaveV3HealthFactorLogic } from "src/modules/adaptors/Morpho/MorphoAaveV3HealthFactorLogic.sol";

/**
* @title Morpho Aave V3 debtToken Adaptor
* @notice Allows Cellars to interact with Morpho Aave V3 debtToken positions.
* @author crispymangoes
*/
contract MorphoAaveV3DebtTokenAdaptor is BaseAdaptor {
contract MorphoAaveV3DebtTokenAdaptor is BaseAdaptor, MorphoAaveV3HealthFactorLogic {
using SafeTransferLib for ERC20;
using Math for uint256;

Expand Down Expand Up @@ -52,7 +53,6 @@ contract MorphoAaveV3DebtTokenAdaptor is BaseAdaptor {

/**
* @notice Minimum Health Factor enforced after every borrow.
* @notice Overwrites strategist set minimums if they are lower.
*/
function HFMIN() internal pure returns (uint256) {
return 1.05e18;
Expand Down Expand Up @@ -124,7 +124,7 @@ contract MorphoAaveV3DebtTokenAdaptor is BaseAdaptor {
morpho().borrow(underlying, amountToBorrow, address(this), address(this), maxIterations);

// Check that health factor is above adaptor minimum.
uint256 healthFactor = _getUserHealthFactor(address(this));
uint256 healthFactor = _getUserHealthFactor(morpho(), address(this));
if (healthFactor < HFMIN()) revert MorphoAaveV3DebtTokenAdaptor__HealthFactorTooLow();
}

Expand All @@ -140,17 +140,4 @@ contract MorphoAaveV3DebtTokenAdaptor is BaseAdaptor {
// Zero out approvals if necessary.
_revokeExternalApproval(tokenToRepay, address(morpho()));
}

/**
* @notice Code pulled directly from Morpho Position Manager.
* https://etherscan.io/address/0x4592e45e0c5DbEe94a135720cCfF2e4353dAc6De#code
*/
function _getUserHealthFactor(address user) internal view returns (uint256) {
IMorphoV3.LiquidityData memory liquidityData = morpho().liquidityData(user);

return
liquidityData.debt > 0
? uint256(1e18).mulDivDown(liquidityData.maxDebt, liquidityData.debt)
: type(uint256).max;
}
}
28 changes: 28 additions & 0 deletions src/modules/adaptors/Morpho/MorphoAaveV3HealthFactorLogic.sol
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import { Math } from "src/utils/Math.sol";
import { IMorphoV3 } from "src/interfaces/external/Morpho/IMorphoV3.sol";

/**
* @title Morpho Aave V3 Health Factor Logic contract.
* @notice Implements health factor logic used by both
* the Morpho Aave V3 A Token and debt Token adaptors.
* @author crispymangoes
*/
contract MorphoAaveV3HealthFactorLogic {
using Math for uint256;

/**
* @notice Code pulled directly from Morpho Position Maanager.
* https://etherscan.io/address/0x4592e45e0c5DbEe94a135720cCfF2e4353dAc6De#code
*/
function _getUserHealthFactor(IMorphoV3 morpho, address user) internal view returns (uint256) {
IMorphoV3.LiquidityData memory liquidityData = morpho.liquidityData(user);

return
liquidityData.debt > 0
? uint256(1e18).mulDivDown(liquidityData.maxDebt, liquidityData.debt)
: type(uint256).max;
}
}

0 comments on commit ac9547f

Please sign in to comment.