diff --git a/.gitignore b/.gitignore index fed44e5fd..5c315306a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ cache/ out/ broadcast/ +gnosisTxs/ # Environment variables! .env diff --git a/.gitmodules b/.gitmodules index 0b9e7cc0e..df3c6db09 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ [submodule "lib/pendle-core-v2-public"] path = lib/pendle-core-v2-public url = https://github.com/pendle-finance/pendle-core-v2-public +[submodule "lib/ccip"] + path = lib/ccip + url = https://github.com/smartcontractkit/ccip diff --git a/hfCalcMethods_Comparison_OptionA b/hfCalcMethods_Comparison_OptionA new file mode 100644 index 000000000..6ecd3e5ff --- /dev/null +++ b/hfCalcMethods_Comparison_OptionA @@ -0,0 +1,10 @@ +CompoundV2AdditionalTests:testAlreadyInMarket(uint256) (runs: 256, μ: 682581, ~: 682706) +CompoundV2AdditionalTests:testDefaultCheckInMarket(uint256) (runs: 256, μ: 411276, ~: 411360) +CompoundV2AdditionalTests:testEnterMarket(uint256) (runs: 256, μ: 604239, ~: 604364) +CompoundV2AdditionalTests:testGAS_Borrow(uint256) (runs: 256, μ: 1256230, ~: 1256363) +CompoundV2AdditionalTests:testGAS_HFRevert(uint256) (runs: 256, μ: 1151126, ~: 1151256) +CompoundV2AdditionalTests:testMultipleRepayments(uint256) (runs: 256, μ: 1703268, ~: 1703396) +CompoundV2AdditionalTests:testRepayingLoans(uint256) (runs: 256, μ: 1445732, ~: 1445870) +CompoundV2AdditionalTests:testTakingOutLoanInUntrackedPositionV2(uint256) (runs: 256, μ: 941820, ~: 941950) +CompoundV2AdditionalTests:testWithdrawEnteredMarketPosition(uint256) (runs: 256, μ: 889067, ~: 889155) +CompoundV2AdditionalTests:testWithdrawableFrom() (gas: 276) \ No newline at end of file diff --git a/lib/ccip b/lib/ccip new file mode 160000 index 000000000..c8eed8079 --- /dev/null +++ b/lib/ccip @@ -0,0 +1 @@ +Subproject commit c8eed8079feec16824e974c66819d3f857b3a49e diff --git a/remappings.txt b/remappings.txt index 2ba893b25..62618ecb2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,4 +8,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ @chainlink/=lib/chainlink/ @uniswapV3P=lib/v3-periphery/contracts/ @uniswapV3C=lib/v3-core/contracts/ -@balancer=lib/balancer-v2-monorepo/pkg \ No newline at end of file +@balancer=lib/balancer-v2-monorepo/pkg +@ccip=lib/ccip/ \ No newline at end of file diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index c0239bb72..ee4ddd811 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -83,6 +83,10 @@ contract Cellar is ERC4626, Owned, ERC721Holder { locked = false; } + // ========================================= _onlyOwner ======================================== + + function _onlyOwner() internal onlyOwner {} + // ========================================= PRICE ROUTER CACHE ========================================= /** @@ -104,11 +108,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * desired behavior. * @dev Callable by Sommelier Governance. */ - function cachePriceRouter( - bool checkTotalAssets, - uint16 allowableRange, - address expectedPriceRouter - ) external onlyOwner { + function cachePriceRouter(bool checkTotalAssets, uint16 allowableRange, address expectedPriceRouter) external { + _onlyOwner(); uint256 minAssets; uint256 maxAssets; @@ -228,12 +229,12 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Array of uint32s made up of cellars credit positions Ids. */ - uint32[] public creditPositions; + uint32[] internal creditPositions; /** * @notice Array of uint32s made up of cellars debt positions Ids. */ - uint32[] public debtPositions; + uint32[] internal debtPositions; /** * @notice Tell whether a position is currently used. @@ -243,7 +244,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Get position data given position id. */ - mapping(uint32 => Registry.PositionData) public getPositionData; + mapping(uint32 => Registry.PositionData) internal getPositionData; /** * @notice Get the ids of the credit positions currently used by the cellar. @@ -262,13 +263,14 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Maximum amount of positions a cellar can have in it's credit/debt arrays. */ - uint256 public constant MAX_POSITIONS = 32; + uint256 internal constant MAX_POSITIONS = 32; /** * @notice Allows owner to change the holding position. * @dev Callable by Sommelier Strategist. */ - function setHoldingPosition(uint32 positionId) public onlyOwner { + function setHoldingPosition(uint32 positionId) public { + _onlyOwner(); if (!isPositionUsed[positionId]) revert Cellar__PositionNotUsed(positionId); if (_assetOf(positionId) != asset) revert Cellar__AssetMismatch(address(asset), address(_assetOf(positionId))); if (getPositionData[positionId].isDebt) revert Cellar__InvalidHoldingPosition(positionId); @@ -278,18 +280,19 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Positions the strategist is approved to use without any governance intervention. */ - mapping(uint32 => bool) public positionCatalogue; + mapping(uint32 => bool) internal positionCatalogue; /** * @notice Adaptors the strategist is approved to use without any governance intervention. */ - mapping(address => bool) public adaptorCatalogue; + mapping(address => bool) internal adaptorCatalogue; /** * @notice Allows Governance to add positions to this cellar's catalogue. * @dev Callable by Sommelier Governance. */ - function addPositionToCatalogue(uint32 positionId) public onlyOwner { + function addPositionToCatalogue(uint32 positionId) public { + _onlyOwner(); // Make sure position is not paused and is trusted. registry.revertIfPositionIsNotTrusted(positionId); positionCatalogue[positionId] = true; @@ -300,7 +303,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Allows Governance to remove positions from this cellar's catalogue. * @dev Callable by Sommelier Strategist. */ - function removePositionFromCatalogue(uint32 positionId) external onlyOwner { + function removePositionFromCatalogue(uint32 positionId) external { + _onlyOwner(); positionCatalogue[positionId] = false; emit PositionCatalogueAltered(positionId, false); } @@ -309,7 +313,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Allows Governance to add adaptors to this cellar's catalogue. * @dev Callable by Sommelier Governance. */ - function addAdaptorToCatalogue(address adaptor) external onlyOwner { + function addAdaptorToCatalogue(address adaptor) external { + _onlyOwner(); // Make sure adaptor is not paused and is trusted. registry.revertIfAdaptorIsNotTrusted(adaptor); adaptorCatalogue[adaptor] = true; @@ -320,7 +325,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Allows Governance to remove adaptors from this cellar's catalogue. * @dev Callable by Sommelier Strategist. */ - function removeAdaptorFromCatalogue(address adaptor) external onlyOwner { + function removeAdaptorFromCatalogue(address adaptor) external { + _onlyOwner(); adaptorCatalogue[adaptor] = false; emit AdaptorCatalogueAltered(adaptor, false); } @@ -332,12 +338,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @param configurationData data used to configure how the position behaves * @dev Callable by Sommelier Strategist. */ - function addPosition( - uint32 index, - uint32 positionId, - bytes memory configurationData, - bool inDebtArray - ) public onlyOwner { + function addPosition(uint32 index, uint32 positionId, bytes memory configurationData, bool inDebtArray) public { + _onlyOwner(); _whenNotShutdown(); // Check if position is already being used. @@ -381,7 +383,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @param index index at which to remove the position * @dev Callable by Sommelier Strategist. */ - function removePosition(uint32 index, bool inDebtArray) external onlyOwner { + function removePosition(uint32 index, bool inDebtArray) external { + _onlyOwner(); // Get position being removed. uint32 positionId = inDebtArray ? debtPositions[index] : creditPositions[index]; @@ -396,7 +399,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Allows Sommelier Governance to forceably remove a position from the Cellar without checking its balance is zero. * @dev Callable by Sommelier Governance. */ - function forcePositionOut(uint32 index, uint32 positionId, bool inDebtArray) external onlyOwner { + function forcePositionOut(uint32 index, uint32 positionId, bool inDebtArray) external { + _onlyOwner(); // Get position being removed. uint32 _positionId = inDebtArray ? debtPositions[index] : creditPositions[index]; // Make sure position id right, and is distrusted. @@ -432,7 +436,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @param inDebtArray bool indicating to switch positions in the debt array, or the credit array. * @dev Callable by Sommelier Strategist. */ - function swapPositions(uint32 index1, uint32 index2, bool inDebtArray) external onlyOwner { + function swapPositions(uint32 index1, uint32 index2, bool inDebtArray) external { + _onlyOwner(); // Get the new positions that will be at each index. uint32 newPosition1; uint32 newPosition2; @@ -507,19 +512,20 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Sets the max possible performance fee for this cellar. */ - uint64 public constant MAX_PLATFORM_FEE = 0.2e18; + uint64 internal constant MAX_PLATFORM_FEE = 0.2e18; /** * @notice Sets the max possible fee cut for this cellar. */ - uint64 public constant MAX_FEE_CUT = 1e18; + uint64 internal constant MAX_FEE_CUT = 1e18; /** * @notice Sets the Strategists cut of platform fees * @param cut the platform cut for the strategist * @dev Callable by Sommelier Governance. */ - function setStrategistPlatformCut(uint64 cut) external onlyOwner { + function setStrategistPlatformCut(uint64 cut) external { + _onlyOwner(); if (cut > MAX_FEE_CUT) revert Cellar__InvalidFeeCut(); emit StrategistPlatformCutChanged(feeData.strategistPlatformCut, cut); @@ -531,7 +537,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @param payout the new strategist payout address * @dev Callable by Sommelier Strategist. */ - function setStrategistPayoutAddress(address payout) external onlyOwner { + function setStrategistPayoutAddress(address payout) external { + _onlyOwner(); emit StrategistPayoutAddressChanged(feeData.strategistPayoutAddress, payout); feeData.strategistPayoutAddress = payout; @@ -583,7 +590,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Allows governance to choose whether or not to respect a pause. * @dev Callable by Sommelier Governance. */ - function toggleIgnorePause() external onlyOwner { + function toggleIgnorePause() external { + _onlyOwner(); ignorePause = ignorePause ? false : true; } @@ -598,7 +606,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Shutdown the cellar. Used in an emergency or if the cellar has been deprecated. * @dev Callable by Sommelier Strategist. */ - function initiateShutdown() external onlyOwner { + function initiateShutdown() external { + _onlyOwner(); _whenNotShutdown(); isShutdown = true; @@ -609,7 +618,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Restart the cellar. * @dev Callable by Sommelier Strategist. */ - function liftShutdown() external onlyOwner { + function liftShutdown() external { + _onlyOwner(); if (!isShutdown) revert Cellar__ContractNotShutdown(); isShutdown = false; @@ -621,12 +631,12 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Id to get the gravity bridge from the registry. */ - uint256 public constant GRAVITY_BRIDGE_REGISTRY_SLOT = 0; + uint256 internal constant GRAVITY_BRIDGE_REGISTRY_SLOT = 0; /** * @notice Id to get the price router from the registry. */ - uint256 public constant PRICE_ROUTER_REGISTRY_SLOT = 2; + uint256 internal constant PRICE_ROUTER_REGISTRY_SLOT = 2; /** * @notice The minimum amount of shares to be minted in the contructor. @@ -722,17 +732,18 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice called at the beginning of deposit. */ - function beforeDeposit(uint256, uint256, address) internal view virtual { + function beforeDeposit(ERC20, uint256, uint256, address) internal view virtual { _whenNotShutdown(); _checkIfPaused(); } /** * @notice called at the end of deposit. + * @param position the position to deposit to. * @param assets amount of assets deposited by user. */ - function afterDeposit(uint256 assets, uint256, address) internal virtual { - _depositTo(holdingPosition, assets); + function afterDeposit(uint32 position, uint256 assets, uint256, address) internal virtual { + _depositTo(position, assets); } /** @@ -745,17 +756,23 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Called when users enter the cellar via deposit or mint. */ - function _enter(uint256 assets, uint256 shares, address receiver) internal { - beforeDeposit(assets, shares, receiver); + function _enter( + ERC20 depositAsset, + uint32 position, + uint256 assets, + uint256 shares, + address receiver + ) internal virtual { + beforeDeposit(asset, assets, shares, receiver); // Need to transfer before minting or ERC777s could reenter. - asset.safeTransferFrom(msg.sender, address(this), assets); + depositAsset.safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); - afterDeposit(assets, shares, receiver); + afterDeposit(position, assets, shares, receiver); } /** @@ -764,7 +781,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @param receiver address to receive the shares. * @return shares amount of shares given for deposit. */ - function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { + function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256 shares) { // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); @@ -773,7 +790,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); - _enter(assets, shares, receiver); + _enter(asset, holdingPosition, assets, shares, receiver); } /** @@ -790,7 +807,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); - _enter(assets, shares, receiver); + _enter(asset, holdingPosition, assets, shares, receiver); } /** @@ -1174,36 +1191,6 @@ contract Cellar is ERC4626, Owned, ERC721Holder { shares = assets.mulDivUp(_totalSupply, _totalAssets); } - // =========================================== AUTOMATION ACTIONS LOGIC =========================================== - - /** - * Emitted when sender is not approved to call `callOnAdaptor`. - */ - error Cellar__CallerNotApprovedToRebalance(); - - /** - * @notice Emitted when `setAutomationActions` is called. - */ - event Cellar__AutomationActionsUpdated(address newAutomationActions); - - /** - * @notice The Automation Actions contract that can rebalance this Cellar. - * @dev Set to zero address if not in use. - */ - address public automationActions; - - /** - * @notice Set the Automation Actions contract. - * @param _registryId Registry Id to get the automation action. - * @param _expectedAutomationActions The registry automation actions differed from the expected automation actions. - * @dev Callable by Sommelier Governance. - */ - function setAutomationActions(uint256 _registryId, address _expectedAutomationActions) external onlyOwner { - _checkRegistryAddressAgainstExpected(_registryId, _expectedAutomationActions); - automationActions = _expectedAutomationActions; - emit Cellar__AutomationActionsUpdated(_expectedAutomationActions); - } - // =========================================== ADAPTOR LOGIC =========================================== /** @@ -1244,19 +1231,20 @@ contract Cellar is ERC4626, Owned, ERC721Holder { /** * @notice Stores the max possible rebalance deviation for this cellar. */ - uint64 public constant MAX_REBALANCE_DEVIATION = 0.1e18; + uint64 internal constant MAX_REBALANCE_DEVIATION = 0.1e18; /** * @notice The percent the total assets of a cellar may deviate during a `callOnAdaptor`(rebalance) call. */ - uint256 public allowedRebalanceDeviation = 0.0003e18; + uint256 internal allowedRebalanceDeviation = 0.0003e18; /** * @notice Allows governance to change this cellars rebalance deviation. * @param newDeviation the new rebalance deviation value. * @dev Callable by Sommelier Governance. */ - function setRebalanceDeviation(uint256 newDeviation) external onlyOwner { + function setRebalanceDeviation(uint256 newDeviation) external { + _onlyOwner(); if (newDeviation > MAX_REBALANCE_DEVIATION) revert Cellar__InvalidRebalanceDeviation(newDeviation, MAX_REBALANCE_DEVIATION); @@ -1309,7 +1297,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @dev Callable by Sommelier Strategist, and Automation Actions contract. */ function callOnAdaptor(AdaptorCall[] calldata data) external virtual nonReentrant { - if (msg.sender != owner && msg.sender != automationActions) revert Cellar__CallerNotApprovedToRebalance(); + _onlyOwner(); _whenNotShutdown(); _checkIfPaused(); blockExternalReceiver = true; @@ -1354,7 +1342,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Increases the share supply cap. * @dev Callable by Sommelier Governance. */ - function increaseShareSupplyCap(uint192 _newShareSupplyCap) public onlyOwner { + function increaseShareSupplyCap(uint192 _newShareSupplyCap) public { + _onlyOwner(); if (_newShareSupplyCap < shareSupplyCap) revert Cellar__InvalidShareSupplyCap(); shareSupplyCap = _newShareSupplyCap; @@ -1364,7 +1353,8 @@ contract Cellar is ERC4626, Owned, ERC721Holder { * @notice Decreases the share supply cap. * @dev Callable by Sommelier Strategist. */ - function decreaseShareSupplyCap(uint192 _newShareSupplyCap) public onlyOwner { + function decreaseShareSupplyCap(uint192 _newShareSupplyCap) public { + _onlyOwner(); if (_newShareSupplyCap > shareSupplyCap) revert Cellar__InvalidShareSupplyCap(); shareSupplyCap = _newShareSupplyCap; @@ -1492,30 +1482,4 @@ contract Cellar is ERC4626, Owned, ERC721Holder { if (_registryId == 0) revert Cellar__SettingValueToRegistryIdZeroIsProhibited(); if (registry.getAddress(_registryId) != _expected) revert Cellar__ExpectedAddressDoesNotMatchActual(); } - - /** - * @notice View the amount of assets in each Cellar Position. - */ - function viewPositionBalances() - external - view - returns (ERC20[] memory assets, uint256[] memory balances, bool[] memory isDebt) - { - uint256 creditLen = creditPositions.length; - uint256 debtLen = debtPositions.length; - assets = new ERC20[](creditLen + debtLen); - balances = new uint256[](creditLen + debtLen); - isDebt = new bool[](creditLen + debtLen); - for (uint256 i = 0; i < creditLen; ++i) { - assets[i] = _assetOf(creditPositions[i]); - balances[i] = _balanceOf(creditPositions[i]); - isDebt[i] = false; - } - - for (uint256 i = 0; i < debtLen; ++i) { - assets[i + creditPositions.length] = _assetOf(debtPositions[i]); - balances[i + creditPositions.length] = _balanceOf(debtPositions[i]); - isDebt[i + creditPositions.length] = true; - } - } } diff --git a/src/base/permutations/CellarWithMultiAssetDeposit.sol b/src/base/permutations/CellarWithMultiAssetDeposit.sol new file mode 100644 index 000000000..e960ab1ea --- /dev/null +++ b/src/base/permutations/CellarWithMultiAssetDeposit.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Cellar, Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; + +contract CellarWithMultiAssetDeposit is Cellar { + using Math for uint256; + using SafeTransferLib for ERC20; + using Address for address; + + // ========================================= STRUCTS ========================================= + + /** + * @notice Stores data needed for multi-asset deposits into this cellar. + * @param isSupported bool indicating that mapped asset is supported + * @param holdingPosition the holding position to deposit alternative assets into + * @param depositFee fee taken for depositing this alternative asset + */ + struct AlternativeAssetData { + bool isSupported; + uint32 holdingPosition; + uint32 depositFee; + } + + // ========================================= CONSTANTS ========================================= + + /** + * @notice The max possible fee that can be charged for an alternative asset deposit. + */ + uint32 internal constant MAX_ALTERNATIVE_ASSET_FEE = 0.1e8; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice Maps alternative assets to alternative asset data. + */ + mapping(ERC20 => AlternativeAssetData) public alternativeAssetData; + + //============================== ERRORS =============================== + + error CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + error CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + error CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); + + //============================== EVENTS =============================== + + /** + * @notice Emitted when an alternative asset is added or updated. + */ + event AlternativeAssetUpdated(address asset, uint32 holdingPosition, uint32 depositFee); + + /** + * @notice Emitted when an alternative asser is removed. + */ + event AlternativeAssetDropped(address asset); + + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap + ) + Cellar( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap + ) + {} + + //============================== OWNER FUNCTIONS =============================== + + /** + * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the ERC20 alternative asset that can be deposited + * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to + * @param _alternativeAssetFee the fee to charge for depositing this alternative asset + */ + function setAlternativeAssetData( + ERC20 _alternativeAsset, + uint32 _alternativeHoldingPosition, + uint32 _alternativeAssetFee + ) external { + _onlyOwner(); + if (!isPositionUsed[_alternativeHoldingPosition]) revert Cellar__PositionNotUsed(_alternativeHoldingPosition); + if (_assetOf(_alternativeHoldingPosition) != _alternativeAsset) + revert Cellar__AssetMismatch(address(_alternativeAsset), address(_assetOf(_alternativeHoldingPosition))); + if (getPositionData[_alternativeHoldingPosition].isDebt) + revert Cellar__InvalidHoldingPosition(_alternativeHoldingPosition); + if (_alternativeAssetFee > MAX_ALTERNATIVE_ASSET_FEE) + revert CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + + alternativeAssetData[_alternativeAsset] = AlternativeAssetData( + true, + _alternativeHoldingPosition, + _alternativeAssetFee + ); + + emit AlternativeAssetUpdated(address(_alternativeAsset), _alternativeHoldingPosition, _alternativeAssetFee); + } + + /** + * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore + */ + function dropAlternativeAssetData(ERC20 _alternativeAsset) external { + _onlyOwner(); + delete alternativeAssetData[_alternativeAsset]; + + emit AlternativeAssetDropped(address(_alternativeAsset)); + } + + /** + * @notice Deposits assets into the cellar, and returns shares to receiver. + * @param assets amount of assets deposited by user. + * @param receiver address to receive the shares. + * @return shares amount of shares given for deposit. + */ + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. + ( + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + + // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. + // Check for rounding error since we round down in previewDeposit. + // NOTE for totalAssets, we add the delta between assetsConvertedToAsset, and assetsConvertedToAssetWithFeeRemoved, so that the fee the caller pays + // to join with the alternative asset is factored into share price calculation. + if ( + (shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + )) == 0 + ) revert Cellar__ZeroShares(); + + if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); + + // _enter into holding position but passing in actual assets. + _enter(depositAsset, position, assets, shares, receiver); + } + + /** + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. + */ + function _getMultiAssetDepositData( + ERC20 depositAsset, + uint256 assets + ) + internal + view + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) + { + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; + } +} diff --git a/src/base/permutations/CellarWithNativeSupport.sol b/src/base/permutations/CellarWithNativeSupport.sol new file mode 100644 index 000000000..c4311f545 --- /dev/null +++ b/src/base/permutations/CellarWithNativeSupport.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Cellar, Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; + +contract CellarWithNativeSuppport is Cellar { + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap + ) + Cellar( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap + ) + {} + + /** + * @notice Implement receive so Cellar can accept native transfers. + */ + receive() external payable {} +} diff --git a/src/base/permutations/CellarWithOracle.sol b/src/base/permutations/CellarWithOracle.sol index 37a06d4a6..75150f4ec 100644 --- a/src/base/permutations/CellarWithOracle.sol +++ b/src/base/permutations/CellarWithOracle.sol @@ -47,7 +47,7 @@ contract CellarWithOracle is Cellar { /** * @notice The decimals the Cellar is expecting the oracle to have. */ - uint8 public constant ORACLE_DECIMALS = 18; + uint8 internal constant ORACLE_DECIMALS = 18; /** * @notice Some failure occurred while trying to setup/use the oracle. diff --git a/src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol b/src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol index ff54cda3a..067be416e 100644 --- a/src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol +++ b/src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol @@ -15,7 +15,7 @@ contract CellarWithOracleWithBalancerFlashLoans is CellarWithOracle, IFlashLoanR * @notice The Balancer Vault contract on current network. * @dev For mainnet use 0xBA12222222228d8Ba445958a75a0704d566BF2C8. */ - address public immutable balancerVault; + address internal immutable balancerVault; constructor( address _owner, diff --git a/src/base/permutations/CellarWithShareLockPeriod.sol b/src/base/permutations/CellarWithShareLockPeriod.sol index b7e27c266..bce4d65fe 100644 --- a/src/base/permutations/CellarWithShareLockPeriod.sol +++ b/src/base/permutations/CellarWithShareLockPeriod.sol @@ -123,8 +123,13 @@ contract CellarWithShareLockPeriod is Cellar { * @param assets amount of assets deposited by user. * @param receiver address receiving the shares. */ - function beforeDeposit(uint256 assets, uint256 shares, address receiver) internal view override { - super.beforeDeposit(assets, shares, receiver); + function beforeDeposit( + ERC20 depositAsset, + uint256 assets, + uint256 shares, + address receiver + ) internal view override { + super.beforeDeposit(depositAsset, assets, shares, receiver); if (msg.sender != receiver) { if (!registry.approvedForDepositOnBehalf(msg.sender)) revert Cellar__NotApprovedToDepositOnBehalf(msg.sender); @@ -133,11 +138,12 @@ contract CellarWithShareLockPeriod is Cellar { /** * @notice called at the end of deposit. + * @param position the position to deposit to. * @param assets amount of assets deposited by user. */ - function afterDeposit(uint256 assets, uint256 shares, address receiver) internal override { + function afterDeposit(uint32 position, uint256 assets, uint256 shares, address receiver) internal override { userShareLockStartTime[receiver] = block.timestamp; - super.afterDeposit(assets, shares, receiver); + super.afterDeposit(position, assets, shares, receiver); } /** diff --git a/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol new file mode 100644 index 000000000..bbad27239 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithAaveFlashLoans } from "src/base/permutations/CellarWithOracleWithAaveFlashLoans.sol"; + +contract CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit is CellarWithOracleWithAaveFlashLoans { + using Math for uint256; + using SafeTransferLib for ERC20; + using Address for address; + + // ========================================= STRUCTS ========================================= + + /** + * @notice Stores data needed for multi-asset deposits into this cellar. + * @param isSupported bool indicating that mapped asset is supported + * @param holdingPosition the holding position to deposit alternative assets into + * @param depositFee fee taken for depositing this alternative asset + */ + struct AlternativeAssetData { + bool isSupported; + uint32 holdingPosition; + uint32 depositFee; + } + + // ========================================= CONSTANTS ========================================= + + /** + * @notice The max possible fee that can be charged for an alternative asset deposit. + */ + uint32 internal constant MAX_ALTERNATIVE_ASSET_FEE = 0.1e8; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice Maps alternative assets to alternative asset data. + */ + mapping(ERC20 => AlternativeAssetData) public alternativeAssetData; + + //============================== ERRORS =============================== + + error CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + error CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + error CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); + + //============================== EVENTS =============================== + + /** + * @notice Emitted when an alternative asset is added or updated. + */ + event AlternativeAssetUpdated(address asset, uint32 holdingPosition, uint32 depositFee); + + /** + * @notice Emitted when an alternative asser is removed. + */ + event AlternativeAssetDropped(address asset); + + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _aavePool + ) + CellarWithOracleWithAaveFlashLoans( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _aavePool + ) + {} + + //============================== OWNER FUNCTIONS =============================== + + /** + * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the ERC20 alternative asset that can be deposited + * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to + * @param _alternativeAssetFee the fee to charge for depositing this alternative asset + */ + function setAlternativeAssetData( + ERC20 _alternativeAsset, + uint32 _alternativeHoldingPosition, + uint32 _alternativeAssetFee + ) external { + _onlyOwner(); + if (!isPositionUsed[_alternativeHoldingPosition]) revert Cellar__PositionNotUsed(_alternativeHoldingPosition); + if (_assetOf(_alternativeHoldingPosition) != _alternativeAsset) + revert Cellar__AssetMismatch(address(_alternativeAsset), address(_assetOf(_alternativeHoldingPosition))); + if (getPositionData[_alternativeHoldingPosition].isDebt) + revert Cellar__InvalidHoldingPosition(_alternativeHoldingPosition); + if (_alternativeAssetFee > MAX_ALTERNATIVE_ASSET_FEE) + revert CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + + alternativeAssetData[_alternativeAsset] = AlternativeAssetData( + true, + _alternativeHoldingPosition, + _alternativeAssetFee + ); + + emit AlternativeAssetUpdated(address(_alternativeAsset), _alternativeHoldingPosition, _alternativeAssetFee); + } + + /** + * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore + */ + function dropAlternativeAssetData(ERC20 _alternativeAsset) external { + _onlyOwner(); + delete alternativeAssetData[_alternativeAsset]; + + emit AlternativeAssetDropped(address(_alternativeAsset)); + } + + /** + * @notice Deposits assets into the cellar, and returns shares to receiver. + * @param assets amount of assets deposited by user. + * @param receiver address to receive the shares. + * @return shares amount of shares given for deposit. + */ + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. + ( + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + + // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. + // Check for rounding error since we round down in previewDeposit. + // NOTE for totalAssets, we add the delta between assetsConvertedToAsset, and assetsConvertedToAssetWithFeeRemoved, so that the fee the caller pays + // to join with the alternative asset is factored into share price calculation. + if ( + (shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + )) == 0 + ) revert Cellar__ZeroShares(); + + if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); + + // _enter into holding position but passing in actual assets. + _enter(depositAsset, position, assets, shares, receiver); + } + + /** + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. + */ + function _getMultiAssetDepositData( + ERC20 depositAsset, + uint256 assets + ) + internal + view + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) + { + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; + } +} diff --git a/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol new file mode 100644 index 000000000..c5e917803 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit } from "src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol"; + +contract CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport is + CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit +{ + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _aavePool + ) + CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _aavePool + ) + {} + + /** + * @notice Implement receive so Cellar can accept native transfers. + */ + receive() external payable {} +} diff --git a/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol new file mode 100644 index 000000000..b09672c45 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithBalancerFlashLoans } from "src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol"; + +contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit is CellarWithOracleWithBalancerFlashLoans { + using Math for uint256; + using SafeTransferLib for ERC20; + using Address for address; + + // ========================================= STRUCTS ========================================= + + /** + * @notice Stores data needed for multi-asset deposits into this cellar. + * @param isSupported bool indicating that mapped asset is supported + * @param holdingPosition the holding position to deposit alternative assets into + * @param depositFee fee taken for depositing this alternative asset + */ + struct AlternativeAssetData { + bool isSupported; + uint32 holdingPosition; + uint32 depositFee; + } + + // ========================================= CONSTANTS ========================================= + + /** + * @notice The max possible fee that can be charged for an alternative asset deposit. + */ + uint32 internal constant MAX_ALTERNATIVE_ASSET_FEE = 0.1e8; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice Maps alternative assets to alternative asset data. + */ + mapping(ERC20 => AlternativeAssetData) public alternativeAssetData; + + //============================== ERRORS =============================== + + error CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + error CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + error CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); + + //============================== EVENTS =============================== + + /** + * @notice Emitted when an alternative asset is added or updated. + */ + event AlternativeAssetUpdated(address asset, uint32 holdingPosition, uint32 depositFee); + + /** + * @notice Emitted when an alternative asser is removed. + */ + event AlternativeAssetDropped(address asset); + + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _balancerVault + ) + CellarWithOracleWithBalancerFlashLoans( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _balancerVault + ) + {} + + //============================== OWNER FUNCTIONS =============================== + + /** + * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the ERC20 alternative asset that can be deposited + * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to + * @param _alternativeAssetFee the fee to charge for depositing this alternative asset + */ + function setAlternativeAssetData( + ERC20 _alternativeAsset, + uint32 _alternativeHoldingPosition, + uint32 _alternativeAssetFee + ) external { + _onlyOwner(); + if (!isPositionUsed[_alternativeHoldingPosition]) revert Cellar__PositionNotUsed(_alternativeHoldingPosition); + if (_assetOf(_alternativeHoldingPosition) != _alternativeAsset) + revert Cellar__AssetMismatch(address(_alternativeAsset), address(_assetOf(_alternativeHoldingPosition))); + if (getPositionData[_alternativeHoldingPosition].isDebt) + revert Cellar__InvalidHoldingPosition(_alternativeHoldingPosition); + if (_alternativeAssetFee > MAX_ALTERNATIVE_ASSET_FEE) + revert CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + + alternativeAssetData[_alternativeAsset] = AlternativeAssetData( + true, + _alternativeHoldingPosition, + _alternativeAssetFee + ); + + emit AlternativeAssetUpdated(address(_alternativeAsset), _alternativeHoldingPosition, _alternativeAssetFee); + } + + /** + * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore + */ + function dropAlternativeAssetData(ERC20 _alternativeAsset) external { + _onlyOwner(); + delete alternativeAssetData[_alternativeAsset]; + + emit AlternativeAssetDropped(address(_alternativeAsset)); + } + + /** + * @notice Deposits assets into the cellar, and returns shares to receiver. + * @param assets amount of assets deposited by user. + * @param receiver address to receive the shares. + * @return shares amount of shares given for deposit. + */ + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. + ( + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + + // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. + // Check for rounding error since we round down in previewDeposit. + // NOTE for totalAssets, we add the delta between assetsConvertedToAsset, and assetsConvertedToAssetWithFeeRemoved, so that the fee the caller pays + // to join with the alternative asset is factored into share price calculation. + if ( + (shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + )) == 0 + ) revert Cellar__ZeroShares(); + + if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); + + // _enter into holding position but passing in actual assets. + _enter(depositAsset, position, assets, shares, receiver); + } + + /** + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. + */ + function _getMultiAssetDepositData( + ERC20 depositAsset, + uint256 assets + ) + internal + view + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) + { + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; + } +} diff --git a/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDepositWithNativeSupport.sol b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDepositWithNativeSupport.sol new file mode 100644 index 000000000..2cc3657c0 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDepositWithNativeSupport.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit } from "src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol"; + +contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDepositWithNativeSupport is + CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit +{ + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _balancerVault + ) + CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _balancerVault + ) + {} + + /** + * @notice Implement receive so Cellar can accept native transfers. + */ + receive() external payable {} +} diff --git a/src/interfaces/external/ICompound.sol b/src/interfaces/external/ICompound.sol index 686f3fd1a..adeb20005 100644 --- a/src/interfaces/external/ICompound.sol +++ b/src/interfaces/external/ICompound.sol @@ -7,6 +7,15 @@ interface ComptrollerG7 { function markets(address market) external view returns (bool, uint256, bool); function compAccrued(address user) external view returns (uint256); + + // Functions from ComptrollerInterface.sol to supply collateral that enable open borrows + function enterMarkets(address[] calldata cTokens) external returns (uint[] memory); + + function exitMarket(address cToken) external returns (uint); + + function getAssetsIn(address account) external view returns (CErc20[] memory); + + function oracle() external view returns (PriceOracle oracle); } interface CErc20 { @@ -16,9 +25,45 @@ interface CErc20 { function exchangeRateStored() external view returns (uint256); + function borrowBalanceCurrent(address account) external view returns (uint); + function mint(uint256 mintAmount) external returns (uint256); function redeemUnderlying(uint256 redeemAmount) external returns (uint256); function redeem(uint256 redeemTokens) external returns (uint256); + + function borrow(uint borrowAmount) external returns (uint); + + function repayBorrow(uint repayAmount) external returns (uint); + + function accrueInterest() external returns (uint); + + function borrowBalanceStored(address account) external view returns (uint); + + function balanceOfUnderlying(address account) external view returns (uint); + + /** + * @notice Get a snapshot of the account's balances, and the cached exchange rate + * @dev This is used by comptroller to more efficiently perform liquidity checks. + * @param account Address of the account to snapshot + * @return (possible error, token balance, borrow balance, exchange rate mantissa) + */ + function getAccountSnapshot(address account) external view returns (uint, uint, uint, uint); + + /** + * @notice Get the liquidity within a specific CErc20 market + */ + function getCash() external view returns (uint); +} + +interface PriceOracle { + /** + * @notice Get the underlying price of a cToken asset + * @param cToken The cToken to get the underlying price of + * @return The underlying asset price mantissa (scaled by 1e18). + * Zero means the price is unavailable. + * TODO: param is originally CToken, in general since we are going to work with native ETH too we may want to bring in CToken vs bringing in just CErc20 + */ + function getUnderlyingPrice(CErc20 cToken) external view returns (uint); } diff --git a/src/interfaces/external/Morpho/MorphoBlue/interfaces/IIrm.sol b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IIrm.sol new file mode 100644 index 000000000..db2aaf873 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IIrm.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import {MarketParams, Market} from "./IMorpho.sol"; + +/// @title IIrm +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Interface that Interest Rate Models (IRMs) used by Morpho must implement. +interface IIrm { + /// @notice Returns the borrow rate of the market `marketParams`. + /// @dev Assumes that `market` corresponds to `marketParams`. + function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256); + + /// @notice Returns the borrow rate of the market `marketParams` without modifying any storage. + /// @dev Assumes that `market` corresponds to `marketParams`. + function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256); +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol new file mode 100644 index 000000000..aa249d7ec --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +type Id is bytes32; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +/// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest +/// accrual. +struct Position { + uint256 supplyShares; + uint128 borrowShares; + uint128 collateral; +} + +/// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalSupplyShares` does not contain the additional shares accrued by `feeRecipient` since the last +/// interest accrual. +struct Market { + uint128 totalSupplyAssets; + uint128 totalSupplyShares; + uint128 totalBorrowAssets; + uint128 totalBorrowShares; + uint128 lastUpdate; + uint128 fee; +} + +struct Authorization { + address authorizer; + address authorized; + bool isAuthorized; + uint256 nonce; + uint256 deadline; +} + +struct Signature { + uint8 v; + bytes32 r; + bytes32 s; +} + +/// @dev This interface is used for factorizing IMorphoStaticTyping and IMorpho. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoBase { + /// @notice The EIP-712 domain separator. + /// @dev Warning: Every EIP-712 signed message based on this domain separator can be reused on another chain sharing + /// the same chain id because the domain separator would be the same. + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice The owner of the contract. + /// @dev It has the power to change the owner. + /// @dev It has the power to set fees on markets and set the fee recipient. + /// @dev It has the power to enable but not disable IRMs and LLTVs. + function owner() external view returns (address); + + /// @notice The fee recipient of all markets. + /// @dev The recipient receives the fees of a given market through a supply position on that market. + function feeRecipient() external view returns (address); + + /// @notice Whether the `irm` is enabled. + function isIrmEnabled(address irm) external view returns (bool); + + /// @notice Whether the `lltv` is enabled. + function isLltvEnabled(uint256 lltv) external view returns (bool); + + /// @notice Whether `authorized` is authorized to modify `authorizer`'s positions. + /// @dev Anyone is authorized to modify their own positions, regardless of this variable. + function isAuthorized(address authorizer, address authorized) external view returns (bool); + + /// @notice The `authorizer`'s current nonce. Used to prevent replay attacks with EIP-712 signatures. + function nonce(address authorizer) external view returns (uint256); + + /// @notice Sets `newOwner` as `owner` of the contract. + /// @dev Warning: No two-step transfer ownership. + /// @dev Warning: The owner can be set to the zero address. + function setOwner(address newOwner) external; + + /// @notice Enables `irm` as a possible IRM for market creation. + /// @dev Warning: It is not possible to disable an IRM. + function enableIrm(address irm) external; + + /// @notice Enables `lltv` as a possible LLTV for market creation. + /// @dev Warning: It is not possible to disable a LLTV. + function enableLltv(uint256 lltv) external; + + /// @notice Sets the `newFee` for the given market `marketParams`. + /// @dev Warning: The recipient can be the zero address. + function setFee(MarketParams memory marketParams, uint256 newFee) external; + + /// @notice Sets `newFeeRecipient` as `feeRecipient` of the fee. + /// @dev Warning: If the fee recipient is set to the zero address, fees will accrue there and will be lost. + /// @dev Modifying the fee recipient will allow the new recipient to claim any pending fees not yet accrued. To + /// ensure that the current recipient receives all due fees, accrue interest manually prior to making any changes. + function setFeeRecipient(address newFeeRecipient) external; + + /// @notice Creates the market `marketParams`. + /// @dev Here is the list of assumptions on the market's dependencies (tokens, IRM and oracle) that guarantees + /// Morpho behaves as expected: + /// - The token should be ERC-20 compliant, except that it can omit return values on `transfer` and `transferFrom`. + /// - The token balance of Morpho should only decrease on `transfer` and `transferFrom`. In particular, tokens with + /// burn functions are not supported. + /// - The token should not re-enter Morpho on `transfer` nor `transferFrom`. + /// - The token balance of the sender (resp. receiver) should decrease (resp. increase) by exactly the given amount + /// on `transfer` and `transferFrom`. In particular, tokens with fees on transfer are not supported. + /// - The IRM should not re-enter Morpho. + /// - The oracle should return a price with the correct scaling. + /// @dev Here is a list of properties on the market's dependencies that could break Morpho's liveness properties + /// (funds could get stuck): + /// - The token can revert on `transfer` and `transferFrom` for a reason other than an approval or balance issue. + /// - A very high amount of assets (~1e35) supplied or borrowed can make the computation of `toSharesUp` and + /// `toSharesDown` overflow. + /// - The IRM can revert on `borrowRate`. + /// - A very high borrow rate returned by the IRM can make the computation of `interest` in `_accrueInterest` + /// overflow. + /// - The oracle can revert on `price`. Note that this can be used to prevent `borrow`, `withdrawCollateral` and + /// `liquidate` from being used under certain market conditions. + /// - A very high price returned by the oracle can make the computation of `maxBorrow` in `_isHealthy` overflow, or + /// the computation of `assetsRepaid` in `liquidate` overflow. + /// @dev The borrow share price of a market with less than 1e4 assets borrowed can be decreased by manipulations, to + /// the point where `totalBorrowShares` is very large and borrowing overflows. + function createMarket(MarketParams memory marketParams) external; + + /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupply` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller + /// is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific amount + /// of shares is given for full compatibility and precision. + /// @dev If the supply of a market gets depleted, the supply share price instantly resets to + /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. + /// @dev Supplying a large amount can revert for overflow. + /// @param marketParams The market to supply assets to. + /// @param assets The amount of assets to supply. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased supply position. + /// @param data Arbitrary data to pass to the `onMorphoSupply` callback. Pass empty data if not needed. + /// @return assetsSupplied The amount of assets supplied. + /// @return sharesSupplied The amount of shares minted. + function supply( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsSupplied, uint256 sharesSupplied); + + /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` to `receiver`. + /// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow. + /// @dev It is advised to use the `shares` input when withdrawing the full position to avoid reverts due to + /// conversion roundings between shares and assets. + /// @param marketParams The market to withdraw assets from. + /// @param assets The amount of assets to withdraw. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the supply position. + /// @param receiver The address that will receive the withdrawn assets. + /// @return assetsWithdrawn The amount of assets withdrawn. + /// @return sharesWithdrawn The amount of shares burned. + function withdraw( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn); + + /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` to `receiver`. + /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the caller + /// is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is given for + /// full compatibility and precision. + /// @dev If the borrow of a market gets depleted, the borrow share price instantly resets to + /// `VIRTUAL_ASSETS`:`VIRTUAL_SHARES`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Borrowing a large amount can revert for overflow. + /// @param marketParams The market to borrow assets from. + /// @param assets The amount of assets to borrow. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased borrow position. + /// @param receiver The address that will receive the borrowed assets. + /// @return assetsBorrowed The amount of assets borrowed. + /// @return sharesBorrowed The amount of shares minted. + function borrow( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed); + + /// @notice Repays `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoReplay` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. To repay max, pass the `shares`'s balance of `onBehalf`. + /// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow. + /// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion + /// roundings between shares and assets. + /// @param marketParams The market to repay assets to. + /// @param assets The amount of assets to repay. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the debt position. + /// @param data Arbitrary data to pass to the `onMorphoRepay` callback. Pass empty data if not needed. + /// @return assetsRepaid The amount of assets repaid. + /// @return sharesRepaid The amount of shares burned. + function repay(MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes calldata data + ) external returns (uint256 assetsRepaid, uint256 sharesRepaid); + + /// @notice Supplies `assets` of collateral on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupplyCollateral` function with the given `data`. + /// @dev Interest are not accrued since it's not required and it saves gas. + /// @dev Supplying a large amount can revert for overflow. + /// @param marketParams The market to supply collateral to. + /// @param assets The amount of collateral to supply. + /// @param onBehalf The address that will own the increased collateral position. + /// @param data Arbitrary data to pass to the `onMorphoSupplyCollateral` callback. Pass empty data if not needed. + function supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes memory data) + external; + + /// @notice Withdraws `assets` of collateral on behalf of `onBehalf` to `receiver`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more collateral than supplied will revert for underflow. + /// @param marketParams The market to withdraw collateral from. + /// @param assets The amount of collateral to withdraw. + /// @param onBehalf The address of the owner of the collateral position. + /// @param receiver The address that will receive the collateral assets. + function withdrawCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, address receiver) + external; + + /// @notice Liquidates the given `repaidShares` of debt asset or seize the given `seizedAssets` of collateral on the + /// given market `marketParams` of the given `borrower`'s position, optionally calling back the caller's + /// `onMorphoLiquidate` function with the given `data`. + /// @dev Either `seizedAssets` or `repaidShares` should be zero. + /// @dev Seizing more than the collateral balance will underflow and revert without any error message. + /// @dev Repaying more than the borrow balance will underflow and revert without any error message. + /// @param marketParams The market of the position. + /// @param borrower The owner of the position. + /// @param seizedAssets The amount of collateral to seize. + /// @param repaidShares The amount of shares to repay. + /// @param data Arbitrary data to pass to the `onMorphoLiquidate` callback. Pass empty data if not needed. + /// @return The amount of assets seized. + /// @return The amount of assets repaid. + function liquidate( + MarketParams memory marketParams, + address borrower, + uint256 seizedAssets, + uint256 repaidShares, + bytes memory data + ) external returns (uint256, uint256); + + /// @notice Executes a flash loan. + /// @dev Flash loans have access to the whole balance of the contract (the liquidity and deposited collateral of all + /// markets combined, plus donations). + /// @dev Warning: Not ERC-3156 compliant but compatibility is easily reached: + /// - `flashFee` is zero. + /// - `maxFlashLoan` is the token's balance of this contract. + /// - The receiver of `assets` is the caller. + /// @param token The token to flash loan. + /// @param assets The amount of assets to flash loan. + /// @param data Arbitrary data to pass to the `onMorphoFlashLoan` callback. + function flashLoan(address token, uint256 assets, bytes calldata data) external; + + /// @notice Sets the authorization for `authorized` to manage `msg.sender`'s positions. + /// @param authorized The authorized address. + /// @param newIsAuthorized The new authorization status. + function setAuthorization(address authorized, bool newIsAuthorized) external; + + /// @notice Sets the authorization for `authorization.authorized` to manage `authorization.authorizer`'s positions. + /// @dev Warning: Reverts if the signature has already been submitted. + /// @dev The signature is malleable, but it has no impact on the security here. + /// @dev The nonce is passed as argument to be able to revert with a different error message. + /// @param authorization The `Authorization` struct. + /// @param signature The signature. + function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; + + /// @notice Accrues interest for the given market `marketParams`. + function accrueInterest(MarketParams memory marketParams) external; + + /// @notice Returns the data stored on the different `slots`. + function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); +} + +/// @dev This interface is inherited by Morpho so that function signatures are checked by the compiler. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoStaticTyping is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position(Id id, address user) + external + view + returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last interest + /// accrual. + function market(Id id) + external + view + returns ( + uint128 totalSupplyAssets, + uint128 totalSupplyShares, + uint128 totalBorrowAssets, + uint128 totalBorrowShares, + uint128 lastUpdate, + uint128 fee + ); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams(Id id) + external + view + returns (address loanToken, address collateralToken, address oracle, address irm, uint256 lltv); +} + +/// @title IMorpho +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @dev Use this interface for Morpho to have access to all the functions with the appropriate function signatures. +interface IMorpho is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `p.supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position(Id id, address user) external view returns (Position memory p); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `m.totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last + /// interest accrual. + function market(Id id) external view returns (Market memory m); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams(Id id) external view returns (MarketParams memory); +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/interfaces/IOracle.sol b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IOracle.sol new file mode 100644 index 000000000..482737ef9 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/interfaces/IOracle.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title IOracle +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Interface that oracles used by Morpho must implement. +/// @dev It is the user's responsibility to select markets with safe oracles. +interface IOracle { + /// @notice Returns the price of 1 asset of collateral token quoted in 1 asset of loan token, scaled by 1e36. + /// @dev It corresponds to the price of 10**(collateral token decimals) assets of collateral token quoted in + /// 10**(loan token decimals) assets of loan token with `36 + loan token decimals - collateral token decimals` + /// decimals of precision. + function price() external view returns (uint256); +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/interfaces/LICENSE b/src/interfaces/external/Morpho/MorphoBlue/interfaces/LICENSE new file mode 100644 index 000000000..aec4e2aca --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/interfaces/LICENSE @@ -0,0 +1,389 @@ +This software is available under your choice of the GNU General Public +License, version 2 or later, or the Business Source License, as set +forth below. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Morpho Association + +Licensed Work: Morpho Blue Core + The Licensed Work is (c) 2023 Morpho Association + +Additional Use Grant: Any uses listed and defined at + morpho-blue-core-license-grants.morpho.eth + +Change Date: The earlier of (i) 2026-01-01, or (ii) a date specified + at morpho-blue-core-license-date.morpho.eth, or (iii) + upon the activation of the setFee function of the + Licensed Work’s applicable protocol smart contracts + deployed for production use. + +Change License: GNU General Public License v2.0 or later + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/ErrorsLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/ErrorsLib.sol new file mode 100644 index 000000000..893a6b32d --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/ErrorsLib.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title ErrorsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Library exposing error messages. +library ErrorsLib { + /// @notice Thrown when the caller is not the owner. + string internal constant NOT_OWNER = "not owner"; + + /// @notice Thrown when the LLTV to enable exceeds the maximum LLTV. + string internal constant MAX_LLTV_EXCEEDED = "max LLTV exceeded"; + + /// @notice Thrown when the fee to set exceeds the maximum fee. + string internal constant MAX_FEE_EXCEEDED = "max fee exceeded"; + + /// @notice Thrown when the value is already set. + string internal constant ALREADY_SET = "already set"; + + /// @notice Thrown when the IRM is not enabled at market creation. + string internal constant IRM_NOT_ENABLED = "IRM not enabled"; + + /// @notice Thrown when the LLTV is not enabled at market creation. + string internal constant LLTV_NOT_ENABLED = "LLTV not enabled"; + + /// @notice Thrown when the market is already created. + string internal constant MARKET_ALREADY_CREATED = "market already created"; + + /// @notice Thrown when the market is not created. + string internal constant MARKET_NOT_CREATED = "market not created"; + + /// @notice Thrown when not exactly one of the input amount is zero. + string internal constant INCONSISTENT_INPUT = "inconsistent input"; + + /// @notice Thrown when zero assets is passed as input. + string internal constant ZERO_ASSETS = "zero assets"; + + /// @notice Thrown when a zero address is passed as input. + string internal constant ZERO_ADDRESS = "zero address"; + + /// @notice Thrown when the caller is not authorized to conduct an action. + string internal constant UNAUTHORIZED = "unauthorized"; + + /// @notice Thrown when the collateral is insufficient to `borrow` or `withdrawCollateral`. + string internal constant INSUFFICIENT_COLLATERAL = "insufficient collateral"; + + /// @notice Thrown when the liquidity is insufficient to `withdraw` or `borrow`. + string internal constant INSUFFICIENT_LIQUIDITY = "insufficient liquidity"; + + /// @notice Thrown when the position to liquidate is healthy. + string internal constant HEALTHY_POSITION = "position is healthy"; + + /// @notice Thrown when the authorization signature is invalid. + string internal constant INVALID_SIGNATURE = "invalid signature"; + + /// @notice Thrown when the authorization signature is expired. + string internal constant SIGNATURE_EXPIRED = "signature expired"; + + /// @notice Thrown when the nonce is invalid. + string internal constant INVALID_NONCE = "invalid nonce"; + + /// @notice Thrown when a token transfer reverted. + string internal constant TRANSFER_REVERTED = "transfer reverted"; + + /// @notice Thrown when a token transfer returned false. + string internal constant TRANSFER_RETURNED_FALSE = "transfer returned false"; + + /// @notice Thrown when a token transferFrom reverted. + string internal constant TRANSFER_FROM_REVERTED = "transferFrom reverted"; + + /// @notice Thrown when a token transferFrom returned false + string internal constant TRANSFER_FROM_RETURNED_FALSE = "transferFrom returned false"; + + /// @notice Thrown when the maximum uint128 is exceeded. + string internal constant MAX_UINT128_EXCEEDED = "max uint128 exceeded"; +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/LICENSE b/src/interfaces/external/Morpho/MorphoBlue/libraries/LICENSE new file mode 100644 index 000000000..aec4e2aca --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/LICENSE @@ -0,0 +1,389 @@ +This software is available under your choice of the GNU General Public +License, version 2 or later, or the Business Source License, as set +forth below. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Morpho Association + +Licensed Work: Morpho Blue Core + The Licensed Work is (c) 2023 Morpho Association + +Additional Use Grant: Any uses listed and defined at + morpho-blue-core-license-grants.morpho.eth + +Change Date: The earlier of (i) 2026-01-01, or (ii) a date specified + at morpho-blue-core-license-date.morpho.eth, or (iii) + upon the activation of the setFee function of the + Licensed Work’s applicable protocol smart contracts + deployed for production use. + +Change License: GNU General Public License v2.0 or later + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol new file mode 100644 index 000000000..456b0e17e --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Id, MarketParams} from "../interfaces/IMorpho.sol"; + +/// @title MarketParamsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Library to convert a market to its id. +library MarketParamsLib { + /// @notice The length of the data used to compute the id of a market. + /// @dev The length is 5 * 32 because `MarketParams` has 5 variables of 32 bytes each. + uint256 internal constant MARKET_PARAMS_BYTES_LENGTH = 5 * 32; + + /// @notice Returns the id of the market `marketParams`. + function id(MarketParams memory marketParams) internal pure returns (Id marketParamsId) { + assembly ("memory-safe") { + marketParamsId := keccak256(marketParams, MARKET_PARAMS_BYTES_LENGTH) + } + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/MathLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/MathLib.sol new file mode 100644 index 000000000..653db4f87 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/MathLib.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +uint256 constant WAD = 1e18; + +/// @title MathLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Library to manage fixed-point arithmetic. +library MathLib { + /// @dev Returns (`x` * `y`) / `WAD` rounded down. + function wMulDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); + } + + /// @dev Returns (`x` * `WAD`) / `y` rounded down. + function wDivDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); + } + + /// @dev Returns (`x` * `WAD`) / `y` rounded up. + function wDivUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); + } + + /// @dev Returns (`x` * `y`) / `d` rounded down. + function mulDivDown(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y) / d; + } + + /// @dev Returns (`x` * `y`) / `d` rounded up. + function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y + (d - 1)) / d; + } + + /// @dev Returns the sum of the first three non-zero terms of a Taylor expansion of e^(nx) - 1, to approximate a + /// continuous compound interest rate. + function wTaylorCompounded(uint256 x, uint256 n) internal pure returns (uint256) { + uint256 firstTerm = x * n; + uint256 secondTerm = mulDivDown(firstTerm, firstTerm, 2 * WAD); + uint256 thirdTerm = mulDivDown(secondTerm, firstTerm, 3 * WAD); + + return firstTerm + secondTerm + thirdTerm; + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol new file mode 100644 index 000000000..514760698 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {MathLib} from "./MathLib.sol"; + +/// @title SharesMathLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Shares management library. +/// @dev This implementation mitigates share price manipulations, using OpenZeppelin's method of virtual shares: +/// https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack. +library SharesMathLib { + using MathLib for uint256; + + /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure + /// high precision computations. + uint256 internal constant VIRTUAL_SHARES = 1e6; + + /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is + /// empty. + uint256 internal constant VIRTUAL_ASSETS = 1; + + /// @dev Calculates the value of `assets` quoted in shares, rounding down. + function toSharesDown(uint256 assets, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { + return assets.mulDivDown(totalShares + VIRTUAL_SHARES, totalAssets + VIRTUAL_ASSETS); + } + + /// @dev Calculates the value of `shares` quoted in assets, rounding down. + function toAssetsDown(uint256 shares, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { + return shares.mulDivDown(totalAssets + VIRTUAL_ASSETS, totalShares + VIRTUAL_SHARES); + } + + /// @dev Calculates the value of `assets` quoted in shares, rounding up. + function toSharesUp(uint256 assets, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { + return assets.mulDivUp(totalShares + VIRTUAL_SHARES, totalAssets + VIRTUAL_ASSETS); + } + + /// @dev Calculates the value of `shares` quoted in assets, rounding up. + function toAssetsUp(uint256 shares, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { + return shares.mulDivUp(totalAssets + VIRTUAL_ASSETS, totalShares + VIRTUAL_SHARES); + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/UtilsLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/UtilsLib.sol new file mode 100644 index 000000000..066043d13 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/UtilsLib.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {ErrorsLib} from "../libraries/ErrorsLib.sol"; + +/// @title UtilsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Library exposing helpers. +/// @dev Inspired by https://github.com/morpho-org/morpho-utils. +library UtilsLib { + /// @dev Returns true if there is exactly one zero among `x` and `y`. + function exactlyOneZero(uint256 x, uint256 y) internal pure returns (bool z) { + assembly { + z := xor(iszero(x), iszero(y)) + } + } + + /// @dev Returns the min of `x` and `y`. + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + /// @dev Returns `x` safely cast to uint128. + function toUint128(uint256 x) internal pure returns (uint128) { + require(x <= type(uint128).max, ErrorsLib.MAX_UINT128_EXCEEDED); + return uint128(x); + } + + /// @dev Returns max(x - y, 0). + function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := mul(gt(x, y), sub(x, y)) + } + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoBalancesLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoBalancesLib.sol new file mode 100644 index 000000000..3afabfd5f --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoBalancesLib.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Id, MarketParams, Market, IMorpho} from "../../interfaces/IMorpho.sol"; +import {IIrm} from "../../interfaces/IIrm.sol"; + +import {MathLib} from "../MathLib.sol"; +import {UtilsLib} from "../UtilsLib.sol"; +import {MorphoLib} from "./MorphoLib.sol"; +import {SharesMathLib} from "../SharesMathLib.sol"; +import {MarketParamsLib} from "../MarketParamsLib.sol"; + +/// @title MorphoBalancesLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Helper library exposing getters with the expected value after interest accrual. +/// @dev This library is not used in Morpho itself and is intended to be used by integrators. +/// @dev The getter to retrieve the expected total borrow shares is not exposed because interest accrual does not apply +/// to it. The value can be queried directly on Morpho using `totalBorrowShares`. +library MorphoBalancesLib { + using MathLib for uint256; + using MathLib for uint128; + using UtilsLib for uint256; + using MorphoLib for IMorpho; + using SharesMathLib for uint256; + using MarketParamsLib for MarketParams; + + /// @notice Returns the expected market balances of a market after having accrued interest. + /// @return The expected total supply assets. + /// @return The expected total supply shares. + /// @return The expected total borrow assets. + /// @return The expected total borrow shares. + function expectedMarketBalances(IMorpho morpho, MarketParams memory marketParams) + internal + view + returns (uint256, uint256, uint256, uint256) + { + Id id = marketParams.id(); + + Market memory market = morpho.market(id); + + uint256 elapsed = block.timestamp - market.lastUpdate; + + // Skipped if elapsed == 0 of if totalBorrowAssets == 0 because interest would be null. + if (elapsed != 0 && market.totalBorrowAssets != 0) { + uint256 borrowRate = IIrm(marketParams.irm).borrowRateView(marketParams, market); + uint256 interest = market.totalBorrowAssets.wMulDown(borrowRate.wTaylorCompounded(elapsed)); + market.totalBorrowAssets += interest.toUint128(); + market.totalSupplyAssets += interest.toUint128(); + + if (market.fee != 0) { + uint256 feeAmount = interest.wMulDown(market.fee); + // The fee amount is subtracted from the total supply in this calculation to compensate for the fact + // that total supply is already updated. + uint256 feeShares = + feeAmount.toSharesDown(market.totalSupplyAssets - feeAmount, market.totalSupplyShares); + market.totalSupplyShares += feeShares.toUint128(); + } + } + + return (market.totalSupplyAssets, market.totalSupplyShares, market.totalBorrowAssets, market.totalBorrowShares); + } + + /// @notice Returns the expected total supply assets of a market after having accrued interest. + function expectedTotalSupplyAssets(IMorpho morpho, MarketParams memory marketParams) + internal + view + returns (uint256 totalSupplyAssets) + { + (totalSupplyAssets,,,) = expectedMarketBalances(morpho, marketParams); + } + + /// @notice Returns the expected total borrow assets of a market after having accrued interest. + function expectedTotalBorrowAssets(IMorpho morpho, MarketParams memory marketParams) + internal + view + returns (uint256 totalBorrowAssets) + { + (,, totalBorrowAssets,) = expectedMarketBalances(morpho, marketParams); + } + + /// @notice Returns the expected total supply shares of a market after having accrued interest. + function expectedTotalSupplyShares(IMorpho morpho, MarketParams memory marketParams) + internal + view + returns (uint256 totalSupplyShares) + { + (, totalSupplyShares,,) = expectedMarketBalances(morpho, marketParams); + } + + /// @notice Returns the expected supply assets balance of `user` on a market after having accrued interest. + /// @dev Warning: Wrong for `feeRecipient` because their supply shares increase is not taken into account. + /// @dev Warning: Withdrawing a supply position using the expected assets balance can lead to a revert due to + /// conversion roundings between shares and assets. + function expectedSupplyAssets(IMorpho morpho, MarketParams memory marketParams, address user) + internal + view + returns (uint256) + { + Id id = marketParams.id(); + uint256 supplyShares = morpho.supplyShares(id, user); + (uint256 totalSupplyAssets, uint256 totalSupplyShares,,) = expectedMarketBalances(morpho, marketParams); + + return supplyShares.toAssetsDown(totalSupplyAssets, totalSupplyShares); + } + + /// @notice Returns the expected borrow assets balance of `user` on a market after having accrued interest. + /// @dev Warning: repaying a borrow position using the expected assets balance can lead to a revert due to + /// conversion roundings between shares and assets. + function expectedBorrowAssets(IMorpho morpho, MarketParams memory marketParams, address user) + internal + view + returns (uint256) + { + Id id = marketParams.id(); + uint256 borrowShares = morpho.borrowShares(id, user); + (,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = expectedMarketBalances(morpho, marketParams); + + return borrowShares.toAssetsUp(totalBorrowAssets, totalBorrowShares); + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol new file mode 100644 index 000000000..c366d1a6b --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {IMorpho, Id} from "../../interfaces/IMorpho.sol"; +import {MorphoStorageLib} from "./MorphoStorageLib.sol"; + +/// @title MorphoLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Helper library to access Morpho storage variables. +/// @dev Warning: Supply and borrow getters may return outdated values that do not include accrued interest. +library MorphoLib { + function supplyShares(IMorpho morpho, Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.positionSupplySharesSlot(id, user)); + return uint256(morpho.extSloads(slot)[0]); + } + + function borrowShares(IMorpho morpho, Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.positionBorrowSharesAndCollateralSlot(id, user)); + return uint128(uint256(morpho.extSloads(slot)[0])); + } + + function collateral(IMorpho morpho, Id id, address user) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.positionBorrowSharesAndCollateralSlot(id, user)); + return uint256(morpho.extSloads(slot)[0] >> 128); + } + + function totalSupplyAssets(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketTotalSupplyAssetsAndSharesSlot(id)); + return uint128(uint256(morpho.extSloads(slot)[0])); + } + + function totalSupplyShares(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketTotalSupplyAssetsAndSharesSlot(id)); + return uint256(morpho.extSloads(slot)[0] >> 128); + } + + function totalBorrowAssets(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketTotalBorrowAssetsAndSharesSlot(id)); + return uint128(uint256(morpho.extSloads(slot)[0])); + } + + function totalBorrowShares(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketTotalBorrowAssetsAndSharesSlot(id)); + return uint256(morpho.extSloads(slot)[0] >> 128); + } + + function lastUpdate(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketLastUpdateAndFeeSlot(id)); + return uint128(uint256(morpho.extSloads(slot)[0])); + } + + function fee(IMorpho morpho, Id id) internal view returns (uint256) { + bytes32[] memory slot = _array(MorphoStorageLib.marketLastUpdateAndFeeSlot(id)); + return uint256(morpho.extSloads(slot)[0] >> 128); + } + + function _array(bytes32 x) private pure returns (bytes32[] memory) { + bytes32[] memory res = new bytes32[](1); + res[0] = x; + return res; + } +} diff --git a/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoStorageLib.sol b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoStorageLib.sol new file mode 100644 index 000000000..07e390088 --- /dev/null +++ b/src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoStorageLib.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Id} from "../../interfaces/IMorpho.sol"; + +/// @title MorphoStorageLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Helper library exposing getters to access Morpho storage variables' slot. +/// @dev This library is not used in Morpho itself and is intended to be used by integrators. +library MorphoStorageLib { + /* SLOTS */ + + uint256 internal constant OWNER_SLOT = 0; + uint256 internal constant FEE_RECIPIENT_SLOT = 1; + uint256 internal constant POSITION_SLOT = 2; + uint256 internal constant MARKET_SLOT = 3; + uint256 internal constant IS_IRM_ENABLED_SLOT = 4; + uint256 internal constant IS_LLTV_ENABLED_SLOT = 5; + uint256 internal constant IS_AUTHORIZED_SLOT = 6; + uint256 internal constant NONCE_SLOT = 7; + uint256 internal constant ID_TO_MARKET_PARAMS_SLOT = 8; + + /* SLOT OFFSETS */ + + uint256 internal constant LOAN_TOKEN_OFFSET = 0; + uint256 internal constant COLLATERAL_TOKEN_OFFSET = 1; + uint256 internal constant ORACLE_OFFSET = 2; + uint256 internal constant IRM_OFFSET = 3; + uint256 internal constant LLTV_OFFSET = 4; + + uint256 internal constant SUPPLY_SHARES_OFFSET = 0; + uint256 internal constant BORROW_SHARES_AND_COLLATERAL_OFFSET = 1; + + uint256 internal constant TOTAL_SUPPLY_ASSETS_AND_SHARES_OFFSET = 0; + uint256 internal constant TOTAL_BORROW_ASSETS_AND_SHARES_OFFSET = 1; + uint256 internal constant LAST_UPDATE_AND_FEE_OFFSET = 2; + + /* GETTERS */ + + function ownerSlot() internal pure returns (bytes32) { + return bytes32(OWNER_SLOT); + } + + function feeRecipientSlot() internal pure returns (bytes32) { + return bytes32(FEE_RECIPIENT_SLOT); + } + + function positionSupplySharesSlot(Id id, address user) internal pure returns (bytes32) { + return bytes32( + uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, POSITION_SLOT))))) + SUPPLY_SHARES_OFFSET + ); + } + + function positionBorrowSharesAndCollateralSlot(Id id, address user) internal pure returns (bytes32) { + return bytes32( + uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, POSITION_SLOT))))) + + BORROW_SHARES_AND_COLLATERAL_OFFSET + ); + } + + function marketTotalSupplyAssetsAndSharesSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, MARKET_SLOT))) + TOTAL_SUPPLY_ASSETS_AND_SHARES_OFFSET); + } + + function marketTotalBorrowAssetsAndSharesSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, MARKET_SLOT))) + TOTAL_BORROW_ASSETS_AND_SHARES_OFFSET); + } + + function marketLastUpdateAndFeeSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, MARKET_SLOT))) + LAST_UPDATE_AND_FEE_OFFSET); + } + + function isIrmEnabledSlot(address irm) internal pure returns (bytes32) { + return keccak256(abi.encode(irm, IS_IRM_ENABLED_SLOT)); + } + + function isLltvEnabledSlot(uint256 lltv) internal pure returns (bytes32) { + return keccak256(abi.encode(lltv, IS_LLTV_ENABLED_SLOT)); + } + + function isAuthorizedSlot(address authorizer, address authorizee) internal pure returns (bytes32) { + return keccak256(abi.encode(authorizee, keccak256(abi.encode(authorizer, IS_AUTHORIZED_SLOT)))); + } + + function nonceSlot(address authorizer) internal pure returns (bytes32) { + return keccak256(abi.encode(authorizer, NONCE_SLOT)); + } + + function idToLoanTokenSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, ID_TO_MARKET_PARAMS_SLOT))) + LOAN_TOKEN_OFFSET); + } + + function idToCollateralTokenSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, ID_TO_MARKET_PARAMS_SLOT))) + COLLATERAL_TOKEN_OFFSET); + } + + function idToOracleSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, ID_TO_MARKET_PARAMS_SLOT))) + ORACLE_OFFSET); + } + + function idToIrmSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, ID_TO_MARKET_PARAMS_SLOT))) + IRM_OFFSET); + } + + function idToLltvSlot(Id id) internal pure returns (bytes32) { + return bytes32(uint256(keccak256(abi.encode(id, ID_TO_MARKET_PARAMS_SLOT))) + LLTV_OFFSET); + } +} diff --git a/src/mocks/CellarWithViewFunctions.sol b/src/mocks/CellarWithViewFunctions.sol new file mode 100644 index 000000000..7a7e7e92f --- /dev/null +++ b/src/mocks/CellarWithViewFunctions.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Cellar, Registry, ERC20 } from "src/base/Cellar.sol"; + +contract CellarWithViewFunctions is Cellar { + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap + ) + Cellar( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap + ) + {} + + function getCreditPosition(uint256 index) external view returns (uint32 position) { + return creditPositions[index]; + } + + function getPositionDataView( + uint32 position + ) external view returns (address adaptor, bool isDebt, bytes memory adaptorData, bytes memory configurationData) { + Registry.PositionData memory data = getPositionData[position]; + return (data.adaptor, data.isDebt, data.adaptorData, data.configurationData); + } +} diff --git a/src/mocks/IrmMock.sol b/src/mocks/IrmMock.sol new file mode 100644 index 000000000..cdc957257 --- /dev/null +++ b/src/mocks/IrmMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import { IIrm } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IIrm.sol"; +import { MarketParams, Market } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; + +import { MathLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MathLib.sol"; + +contract IrmMock is IIrm { + using MathLib for uint128; + + function borrowRateView(MarketParams memory, Market memory market) public pure returns (uint256) { + if (market.totalSupplyAssets == 0) return 0; + + uint256 utilization = market.totalBorrowAssets.wDivDown(market.totalSupplyAssets); + + // Divide by the number of seconds in a year. + // This is a very simple model where x% utilization corresponds to x% APR. + return utilization / 365 days; + } + + function borrowRate(MarketParams memory marketParams, Market memory market) external pure returns (uint256) { + return borrowRateView(marketParams, market); + } +} diff --git a/src/mocks/MockCCIPRouter.sol b/src/mocks/MockCCIPRouter.sol new file mode 100644 index 000000000..915af008c --- /dev/null +++ b/src/mocks/MockCCIPRouter.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +contract MockCCIPRouter { + ERC20 public immutable LINK; + + constructor(address _link) { + LINK = ERC20(_link); + } + + uint256 public messageCount; + + uint256 public currentFee = 1e18; + + uint64 public constant SOURCE_SELECTOR = 6101244977088475029; + uint64 public constant DESTINATION_SELECTOR = 16015286601757825753; + + mapping(bytes32 => Client.Any2EVMMessage) public messages; + + bytes32 public lastMessageId; + + function setFee(uint256 newFee) external { + currentFee = newFee; + } + + function getLastMessage() external view returns (Client.Any2EVMMessage memory) { + return messages[lastMessageId]; + } + + function getFee(uint64, Client.EVM2AnyMessage memory) external view returns (uint256) { + return currentFee; + } + + function ccipSend(uint64 chainSelector, Client.EVM2AnyMessage memory message) external returns (bytes32 messageId) { + LINK.transferFrom(msg.sender, address(this), currentFee); + messageId = bytes32(messageCount); + messageCount++; + lastMessageId = messageId; + messages[messageId].messageId = messageId; + messages[messageId].sourceChainSelector = chainSelector == SOURCE_SELECTOR + ? DESTINATION_SELECTOR + : SOURCE_SELECTOR; + messages[messageId].sender = abi.encode(msg.sender); + messages[messageId].data = message.data; + } +} diff --git a/src/mocks/MockDataFeedForMorphoBlue.sol b/src/mocks/MockDataFeedForMorphoBlue.sol new file mode 100644 index 000000000..e283848a3 --- /dev/null +++ b/src/mocks/MockDataFeedForMorphoBlue.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { IChainlinkAggregator } from "src/interfaces/external/IChainlinkAggregator.sol"; +import { IOracle } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IOracle.sol"; +import { MainnetAddresses } from "test/resources/MainnetAddresses.sol"; +import { Math } from "src/utils/Math.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +contract MockDataFeedForMorphoBlue is IOracle { + using Math for uint256; + + int256 public mockAnswer; + uint256 public mockUpdatedAt; + uint256 public price; + uint256 constant ORACLE_PRICE_DECIMALS = 36; // from MorphoBlue + uint256 constant CHAINLINK_PRICE_SCALE = 1e8; + + IChainlinkAggregator public immutable realFeed; + + constructor(address _realFeed) { + realFeed = IChainlinkAggregator(_realFeed); + } + + function aggregator() external view returns (address) { + return realFeed.aggregator(); + } + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + (roundId, answer, startedAt, updatedAt, answeredInRound) = realFeed.latestRoundData(); + if (mockAnswer != 0) answer = mockAnswer; + if (mockUpdatedAt != 0) updatedAt = mockUpdatedAt; + } + + function latestAnswer() external view returns (int256 answer) { + answer = realFeed.latestAnswer(); + if (mockAnswer != 0) answer = mockAnswer; + } + + function setMockAnswer(int256 ans, ERC20 _collateralToken, ERC20 _loanToken) external { + mockAnswer = ans; + uint256 collateralDecimals = _collateralToken.decimals(); + uint256 loanTokenDecimals = _loanToken.decimals(); + _setPrice(uint256(ans), collateralDecimals, loanTokenDecimals); + } + + function setMockUpdatedAt(uint256 at) external { + mockUpdatedAt = at; + } + + /** + * @dev Takes the chainlink price, scales it down, then applies the appropriate scalar needed for morpho blue calcs. + * NOTE: Recall from IOracle.sol that the units will be 10 ** (36 - collateralUnits + borrowUnits) + */ + function _setPrice(uint256 _newPrice, uint256 _collateralDecimals, uint256 _loanTokenDecimals) internal { + price = + (_newPrice / CHAINLINK_PRICE_SCALE) * + (10 ** (ORACLE_PRICE_DECIMALS - _collateralDecimals + _loanTokenDecimals)); // BU / CU + } +} diff --git a/src/modules/adaptors/Compound/CTokenAdaptor.sol b/src/modules/adaptors/Compound/CTokenAdaptor.sol index c250e1ff3..1db4d5a47 100644 --- a/src/modules/adaptors/Compound/CTokenAdaptor.sol +++ b/src/modules/adaptors/Compound/CTokenAdaptor.sol @@ -3,13 +3,19 @@ pragma solidity 0.8.21; import { BaseAdaptor, ERC20, SafeTransferLib, Math } from "src/modules/adaptors/BaseAdaptor.sol"; import { ComptrollerG7 as Comptroller, CErc20 } from "src/interfaces/external/ICompound.sol"; +import { CompoundV2HelperLogic } from "src/modules/adaptors/Compound/CompoundV2HelperLogic.sol"; +import { IWETH9 } from "src/interfaces/external/IWETH9.sol"; +import { console } from "@forge-std/Test.sol"; + +// TODO to handle ETH based markets /** * @title Compound CToken Adaptor - * @notice Allows Cellars to interact with Compound CToken positions. - * @author crispymangoes + * @notice Allows Cellars to interact with CompoundV2 CToken positions AND enter compound markets such that the calling cellar has an active collateral position (enabling the cellar to borrow). + * @dev As of December 2023, this is the newer version of `CTokenAdaptor.sol` whereas the prior version had no functionality for marking lent assets as supplied Collateral for open borrow positions using the `CompoundV2DebtAdaptor.sol` + * @author crispymangoes, 0xEinCodes */ -contract CTokenAdaptor is BaseAdaptor { +contract CTokenAdaptor is CompoundV2HelperLogic, BaseAdaptor { using SafeTransferLib for ERC20; using Math for uint256; @@ -18,14 +24,15 @@ contract CTokenAdaptor is BaseAdaptor { // Where: // `cToken` is the cToken position this adaptor is working with //================= Configuration Data Specification ================= - // NOT USED - // **************************** IMPORTANT **************************** - // There is no way for a Cellar to take out loans on Compound, so there - // are NO health factor checks done for `withdraw` or `withdrawableFrom` - // In the future if a Compound debt adaptor is created, then this adaptor - // must be changed to include some health factor checks like the - // Aave aToken adaptor. - //==================================================================== + // configurationData = abi.encode(bool isLiquid) + // Where: + // `isLiquid` dictates whether the position is liquid or not + // If true: + // position can support use withdraws + // else: + // position can not support user withdraws + // + // /** @notice Compound action returned a non zero error code. @@ -37,6 +44,21 @@ contract CTokenAdaptor is BaseAdaptor { */ error CTokenAdaptor__MarketNotListed(address market); + /** + * @notice Strategist attempted to enter a market but failed + */ + error CTokenAdaptor__UnsuccessfulEnterMarket(address market); + + /** + * @notice Attempted tx that results in unhealthy cellar + */ + error CTokenAdaptor__HealthFactorTooLow(address market); + + /** + * @notice Attempted tx that results in unhealthy cellar + */ + error CTokenAdaptor__AlreadyInMarket(address market); + /** * @notice The Compound V2 Comptroller contract on current network. * @dev For mainnet use 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B. @@ -49,9 +71,24 @@ contract CTokenAdaptor is BaseAdaptor { */ ERC20 public immutable COMP; - constructor(address v2Comptroller, address comp) { + /** + * @notice Minimum Health Factor enforced after every removeCollateral() strategist function call. + * @notice Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + /** + * @notice The wrapper contract for the primitive/native asset. + */ + IWETH9 public immutable wrappedPrimitive; + + + constructor(address _wrappedPrimitive, address v2Comptroller, address comp, uint256 _healthFactor) CompoundV2HelperLogic(_healthFactor) { + _verifyConstructorMinimumHealthFactor(_healthFactor); + wrappedPrimitive = IWETH9(_wrappedPrimitive); comptroller = Comptroller(v2Comptroller); COMP = ERC20(comp); + minimumHealthFactor = _healthFactor; } //============================================ Global Functions =========================================== @@ -62,21 +99,24 @@ contract CTokenAdaptor is BaseAdaptor { * of the adaptor is more difficult. */ function identifier() public pure override returns (bytes32) { - return keccak256(abi.encode("Compound cToken Adaptor V 1.1")); + return keccak256(abi.encode("CompoundV2 cToken AdaptorV2 V 0.0")); } //============================================ Implement Base Functions =========================================== + /** * @notice Cellar must approve market to spend its assets, then call mint to lend its assets. * @param assets the amount of assets to lend on Compound * @param adaptorData adaptor data containing the abi encoded cToken * @dev configurationData is NOT used + * @dev strategist function `enterMarket()` is used to mark cTokens as collateral provision for cellar. `exitMarket()` removes compound-internal toggle marking and thus marks this position's assets no longer as collateral. */ function deposit(uint256 assets, bytes memory adaptorData, bytes memory) public override { // Deposit assets to Compound. CErc20 cToken = abi.decode(adaptorData, (CErc20)); - _validateMarketInput(address(cToken)); ERC20 token = ERC20(cToken.underlying()); + + // TODO - how to handle if underlying is ETH? token.safeApprove(address(cToken), assets); uint256 errorCode = cToken.mint(assets); @@ -88,23 +128,30 @@ contract CTokenAdaptor is BaseAdaptor { } /** - @notice Cellars must withdraw from Compound. + @notice Allows users to withdraw from Compound through interacting with the cellar IF cellar is not using this position to "provide collateral" * @dev Important to verify that external receivers are allowed if receiver is not Cellar address. * @param assets the amount of assets to withdraw from Compound * @param receiver the address to send withdrawn assets to * @param adaptorData adaptor data containing the abi encoded cToken - * @dev configurationData is NOT used - * @dev There are NO health factor checks done in `withdraw`, or `withdrawableFrom`. - * If cellars ever take on Compound Debt it is crucial these checks are added, - * see "IMPORTANT" above. + * @param configurationData abi encoded bool indicating whether the position is liquid or not. + * @dev Conditional logic with`marketJoinCheck` ensures that any withdrawal does not affect health factor. */ - function withdraw(uint256 assets, address receiver, bytes memory adaptorData, bytes memory) public override { + function withdraw( + uint256 assets, + address receiver, + bytes memory adaptorData, + bytes memory configurationData + ) public override { + bool isLiquid = abi.decode(configurationData, (bool)); + if (!isLiquid) revert BaseAdaptor__UserWithdrawsNotAllowed(); + + CErc20 cToken = abi.decode(adaptorData, (CErc20)); // Run external receiver check. _externalReceiverCheck(receiver); + if (_checkMarketsEntered(cToken)) revert CTokenAdaptor__AlreadyInMarket(address(cToken)); // we could allow withdraws but that would add gas and overcomplicates things (HF checks, etc.). It is ideal for a strategist to be strategic on having a market position used as collateral (recall that compoundV2 allows multiple underlying assets to collateralize different assets being borrowed). + // Withdraw assets from Compound. - CErc20 cToken = abi.decode(adaptorData, (CErc20)); - _validateMarketInput(address(cToken)); uint256 errorCode = cToken.redeemUnderlying(assets); // Check for errors. @@ -112,18 +159,29 @@ contract CTokenAdaptor is BaseAdaptor { // Transfer assets to receiver. ERC20(cToken.underlying()).safeTransfer(receiver, assets); + + // TODO: need to figure out how to handle native ETH if that is the underlying asset } /** - * @notice Identical to `balanceOf`. - * @dev There are NO health factor checks done in `withdraw`, or `withdrawableFrom`. - * If cellars ever take on Compound Debt it is crucial these checks are added, - * see "IMPORTANT" above. + * @notice Returns balanceOf underlying assets for cToken, regardless of if they are used as supplied collateral or only as lent out assets. */ - function withdrawableFrom(bytes memory adaptorData, bytes memory) public view override returns (uint256) { - CErc20 cToken = abi.decode(adaptorData, (CErc20)); - uint256 cTokenBalance = cToken.balanceOf(msg.sender); - return cTokenBalance.mulDivDown(cToken.exchangeRateStored(), 1e18); + function withdrawableFrom( + bytes memory adaptorData, + bytes memory configurationData + ) public view override returns (uint256 withdrawableSupply) { + bool isLiquid = abi.decode(configurationData, (bool)); + + if (isLiquid) { + CErc20 cToken = abi.decode(adaptorData, (CErc20)); + if (_checkMarketsEntered(cToken)) return 0; + uint256 liquidSupply = cToken.getCash(); + uint256 cellarSuppliedBalance = (cToken.balanceOf(msg.sender)).mulDivDown( + cToken.exchangeRateStored(), + 1e18 + ); + withdrawableSupply = cellarSuppliedBalance > liquidSupply ? liquidSupply : cellarSuppliedBalance; + } } /** @@ -168,14 +226,12 @@ contract CTokenAdaptor is BaseAdaptor { //============================================ Strategist Functions =========================================== /** - * @notice Allows strategists to lend assets on Compound. + * @notice Allows strategists to lend assets on Compound or add to existing collateral supply for cellar wrt specified market. * @dev Uses `_maxAvailable` helper function, see BaseAdaptor.sol * @param market the market to deposit to. * @param amountToDeposit the amount of `tokenToDeposit` to lend on Compound. */ function depositToCompound(CErc20 market, uint256 amountToDeposit) public { - _validateMarketInput(address(market)); - ERC20 tokenToDeposit = ERC20(market.underlying()); amountToDeposit = _maxAvailable(tokenToDeposit, amountToDeposit); tokenToDeposit.safeApprove(address(market), amountToDeposit); @@ -192,16 +248,56 @@ contract CTokenAdaptor is BaseAdaptor { * @notice Allows strategists to withdraw assets from Compound. * @param market the market to withdraw from. * @param amountToWithdraw the amount of `market.underlying()` to withdraw from Compound + * NOTE: `redeem()` is used for redeeming a specified amount of cToken, whereas `redeemUnderlying()` is used for obtaining a specified amount of underlying tokens no matter what amount of cTokens required. + * NOTE: Purposely allowed withdrawals even while 'IN' market for this strategist function. */ function withdrawFromCompound(CErc20 market, uint256 amountToWithdraw) public { - _validateMarketInput(address(market)); - uint256 errorCode; if (amountToWithdraw == type(uint256).max) errorCode = market.redeem(market.balanceOf(address(this))); else errorCode = market.redeemUnderlying(amountToWithdraw); + // Check for errors. if (errorCode != 0) revert CTokenAdaptor__NonZeroCompoundErrorCode(errorCode); + + uint256 hf = _getHealthFactor(address(this), comptroller); + console.log("HealthFactor_Withdraw: %s", hf); + + // Check new HF from redemption + if (minimumHealthFactor > (_getHealthFactor(address(this), comptroller))) { + + revert CTokenAdaptor__HealthFactorTooLow(address(market)); + } + } + + /** + * @notice Allows strategists to enter the compound market and thus mark its assets as supplied collateral that can support an open borrow position. + * @param market the market to mark alotted assets as supplied collateral. + * @dev NOTE: this must be called in order to support for a CToken in order to open a borrow position within that market. + */ + function enterMarket(CErc20 market) public { + if (_checkMarketsEntered(market)) revert CTokenAdaptor__AlreadyInMarket(address(market)); // so as to not waste gas + + address[] memory cToken = new address[](1); + uint256[] memory result = new uint256[](1); + cToken[0] = address(market); + result = comptroller.enterMarkets(cToken); // enter the market + + if (result[0] > 0) revert CTokenAdaptor__NonZeroCompoundErrorCode(result[0]); + } + + /** + * @notice Allows strategists to exit the compound market and unmark its assets as supplied collateral; thus no longer supporting an open borrow position. + * @param market the market to unmark alotted assets as supplied collateral. + * @dev This function is not needed to be called if redeeming cTokens, but it is available if Strategists want to toggle a `CTokenAdaptor` position w/ a specific cToken as "not supporting an open-borrow position" for w/e reason. + */ + function exitMarket(CErc20 market) public { + uint256 errorCode = comptroller.exitMarket(address(market)); // exit the market as supplied collateral (still in lending position though) + if (errorCode != 0) revert CTokenAdaptor__NonZeroCompoundErrorCode(errorCode); + + if (minimumHealthFactor > (_getHealthFactor(address(this), comptroller))) { + revert CTokenAdaptor__HealthFactorTooLow(address(market)); + } // when we exit the market, compound toggles the collateral off and thus checks it in the hypothetical liquidity check etc. } /** @@ -214,11 +310,18 @@ contract CTokenAdaptor is BaseAdaptor { //============================================ Helper Functions ============================================ /** - * @notice Helper function that reverts if market is not listed in Comptroller. + * @notice Helper function that checks if passed market is within list of markets that the cellar is in. + * @return inCTokenMarket bool that is true if position has entered the market already */ - function _validateMarketInput(address input) internal view { - (bool isListed, , ) = comptroller.markets(input); - - if (!isListed) revert CTokenAdaptor__MarketNotListed(input); + function _checkMarketsEntered(CErc20 cToken) internal view returns (bool inCTokenMarket) { + // Check cellar has entered the market and thus is illiquid (used for open-borrows possibly) + CErc20[] memory marketsEntered = comptroller.getAssetsIn(address(this)); + uint256 marketsEnteredLength = marketsEntered.length; + for (uint256 i = 0; i < marketsEnteredLength; i++) { + // check if cToken is one of the markets cellar position is in. + if (marketsEntered[i] == cToken) { + inCTokenMarket = true; + } + } } } diff --git a/src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol b/src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol new file mode 100644 index 000000000..6e14c201d --- /dev/null +++ b/src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, PriceRouter, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { ComptrollerG7 as Comptroller, CErc20 } from "src/interfaces/external/ICompound.sol"; +import { CompoundV2HelperLogic } from "src/modules/adaptors/Compound/CompoundV2HelperLogic.sol"; +import { console } from "@forge-std/Test.sol"; + +/** + * @title CompoundV2 Debt Token Adaptor + * @notice Allows Cellars to borrow assets from Compound V2 markets. + * @author crispymangoes, 0xEinCodes + * NOTE: CTokenAdaptorV2.sol is used to "enter" CompoundV2 Markets as Collateral Providers. Collateral Provision from a cellar is needed before they can borrow from CompoundV2 using this adaptor. + */ +contract CompoundV2DebtAdaptor is BaseAdaptor, CompoundV2HelperLogic { + using SafeTransferLib for ERC20; + using Math for uint256; + + //============================================ Notice =========================================== + // NOTE - `accrueInterest()` seems to be very expensive, public tx. That said it is similar to other lending protocols where it is called for every mutative function call that the CompoundV2 adaptors are implementing. Thus we are leaving it up to the strategist to coordinate as needed in calling it similar to MorphoBlue adaptors and how they can diverge if the contract is not kicked. + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(CERC20 cToken) + // Where: + // `cToken` is the cToken position this adaptor is working with + //================= Configuration Data Specification ================= + // NOT USED + //==================================================================== + + /** + * @notice Strategist attempted to interact with a market that is not listed. + */ + error CTokenAdaptorV2__MarketNotListed(address market); + + /** + * @notice Attempted to interact with an market the Cellar is not using. + */ + error CompoundV2DebtAdaptor__CompoundV2PositionsMustBeTracked(address market); + + /** + * @notice Attempted tx that results in unhealthy cellar + */ + error CompoundV2DebtAdaptor__HealthFactorTooLow(address market); + + /** + * @notice Attempted repayment when no debt position in market for cellar + */ + error CompoundV2DebtAdaptor__CannotRepayNoDebt(address market); + + /** + @notice Compound action returned a non zero error code. + */ + error CompoundV2DebtAdaptor__NonZeroCompoundErrorCode(uint256 errorCode); + + /** + * @notice The Compound V2 Comptroller contract on current network. + * @dev For mainnet use 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B. + */ + Comptroller public immutable comptroller; + + /** + * @notice Address of the COMP token. + * @notice For mainnet use 0xc00e94Cb662C3520282E6f5717214004A7f26888. + */ + ERC20 public immutable COMP; + + /** + * @notice This bool determines how this adaptor accounts for interest. + * True: Account for pending interest to be paid when calling `balanceOf` or `withdrawableFrom`. + * False: Do not account for pending interest to be paid when calling `balanceOf` or `withdrawableFrom`. + */ + bool public immutable ACCOUNT_FOR_INTEREST; + + /** + * @notice Minimum Health Factor enforced after every borrow. + * @notice Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + // NOTE: comptroller is a proxy so there may be times that the implementation is updated, although it is rare and would come up for governance vote. + constructor( + bool _accountForInterest, + address _v2Comptroller, + address _comp, + uint256 _healthFactor + ) CompoundV2HelperLogic(_healthFactor) { + _verifyConstructorMinimumHealthFactor(_healthFactor); + ACCOUNT_FOR_INTEREST = _accountForInterest; + comptroller = Comptroller(_v2Comptroller); + COMP = ERC20(_comp); + minimumHealthFactor = _healthFactor; + } + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure virtual override returns (bytes32) { + return keccak256(abi.encode("CompoundV2 Debt Adaptor V 0.0")); + } + + //============================================ Implement Base Functions =========================================== + + /** + * @notice User deposits are NOT allowed into this position. + */ + function deposit(uint256, bytes memory, bytes memory) public pure override { + revert BaseAdaptor__UserDepositsNotAllowed(); + } + + /** + * @notice User withdraws are NOT allowed from this position. + */ + function withdraw(uint256, address, bytes memory, bytes memory) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice This position is a debt position, and user withdraws are not allowed so + * this position must return 0 for withdrawableFrom. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Returns the cellar's amount owing (debt) to CompoundV2 market + * @param adaptorData encoded CompoundV2 market (cToken) for this position + * NOTE: this queries `borrowBalanceCurrent(address account)` to get current borrow amount per compoundV2 market WITHOUT interest. `borrowBalanceCurrent` calls accrueInterest, so it changes state and thus won't work for this view function. Thus we are using `borrowBalanceStored`. See NOTE at beginning about `accrueInterest()` + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + CErc20 cToken = abi.decode(adaptorData, (CErc20)); + return cToken.borrowBalanceStored(msg.sender); + } + + /** + * @notice Returns the underlying asset for respective CompoundV2 market (cToken) + */ + function assetOf(bytes memory adaptorData) public view override returns (ERC20) { + CErc20 cToken = abi.decode(adaptorData, (CErc20)); + return ERC20(cToken.underlying()); + } + + /** + * @notice This adaptor reports values in terms of debt. + */ + function isDebt() public pure override returns (bool) { + return true; + } + + //============================================ Strategist Functions =========================================== + + // `borrowAsset` + /** + * @notice Allows strategists to borrow assets from CompoundV2 markets. + * @param market the CompoundV2 market to borrow from underlying assets from + * @param amountToBorrow the amount of `debtTokenToBorrow` to borrow on this CompoundV2 market. This is in the decimals of the underlying asset being borrowed. + */ + function borrowFromCompoundV2(CErc20 market, uint256 amountToBorrow) public { + _validateMarketInput(address(market)); + + // borrow underlying asset from compoundV2 + uint256 errorCode = market.borrow(amountToBorrow); + if (errorCode != 0) revert CompoundV2DebtAdaptor__NonZeroCompoundErrorCode(errorCode); + + uint256 hf = _getHealthFactor(address(this), comptroller); + console.log("HealthFactor_Borrow: %s", hf); + + if (minimumHealthFactor > (_getHealthFactor(address(this), comptroller))) { + revert CompoundV2DebtAdaptor__HealthFactorTooLow(address(market)); + } + } + + // `repayDebt` + + /** + * @notice Allows strategists to repay loan debt on CompoundV2 market. + * @dev Uses `_maxAvailable` helper function, see BaseAdaptor.sol + * @param _market the CompoundV2 market to borrow from underlying assets from + * @param _debtTokenRepayAmount the amount of `debtToken` to repay with. + * NOTE: Events should be emitted to show how much debt is remaining + */ + function repayCompoundV2Debt(CErc20 _market, uint256 _debtTokenRepayAmount) public { + _validateMarketInput(address(_market)); + ERC20 tokenToRepay = ERC20(_market.underlying()); + uint256 debtTokenToRepay = _maxAvailable(tokenToRepay, _debtTokenRepayAmount); + tokenToRepay.safeApprove(address(_market), type(uint256).max); + + uint256 errorCode = _market.repayBorrow(debtTokenToRepay); + if (errorCode != 0) revert CompoundV2DebtAdaptor__NonZeroCompoundErrorCode(errorCode); + + _revokeExternalApproval(tokenToRepay, address(_market)); + } + + /** + * @notice Helper function that reverts if market is not listed in Comptroller AND checks that it is setup in the Cellar. + */ + function _validateMarketInput(address _market) internal view { + (bool isListed, , ) = comptroller.markets(_market); + if (!isListed) revert CTokenAdaptorV2__MarketNotListed(_market); + bytes32 positionHash = keccak256(abi.encode(identifier(), true, abi.encode(_market))); + uint32 positionId = Cellar(address(this)).registry().getPositionHashToPositionId(positionHash); + if (!Cellar(address(this)).isPositionUsed(positionId)) + revert CompoundV2DebtAdaptor__CompoundV2PositionsMustBeTracked(address(_market)); + } +} diff --git a/src/modules/adaptors/Compound/CompoundV2HelperLogic.sol b/src/modules/adaptors/Compound/CompoundV2HelperLogic.sol new file mode 100644 index 000000000..2d88b4ce4 --- /dev/null +++ b/src/modules/adaptors/Compound/CompoundV2HelperLogic.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Math } from "src/utils/Math.sol"; +import { ComptrollerG7 as Comptroller, CErc20, PriceOracle } from "src/interfaces/external/ICompound.sol"; +import { Test, stdStorage, StdStorage, stdError } from "lib/forge-std/src/Test.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { Math } from "src/utils/Math.sol"; +import { console } from "@forge-std/Test.sol"; + +/** + * @title CompoundV2 Helper Logic Contract Option A. + * @notice Implements health factor logic used by both + * the CTokenAdaptorV2 && CompoundV2DebtAdaptor + * @author crispymangoes, 0xEinCodes + * NOTE: This version reduces some precision but helps simplify the health factor calculation by not using the `cToken.underlying.Decimals()` as a scalar throughout the health factor calculations. The 'lossy-ness' would amount to fractions of pennies when comparing the health factor calculations to the reported `getHypotheticalAccountLiquidityInternal()` results from CompoundV2 `getHypotheticalAccountLiquidityInternal()`. This is deemed negligible but needs to be proven via testing. + * Option B, in `CompoundV2HelperLogicVersionB.sol` is the version of the health factor logic that follows CompoundV2's scaling factors used within the Comptroller.sol + */ +contract CompoundV2HelperLogic is Test { + using Math for uint256; + + // vars to resolve stack too deep error + // CErc20[] internal marketsEntered; + + /** + @notice Compound action returned a non zero error code. + */ + error CompoundV2HelperLogic__NonZeroCompoundErrorCode(uint256 errorCode); + + /** + @notice Compound oracle returned a zero oracle value. + @param asset that oracle query is associated to + */ + error CompoundV2HelperLogic__OracleCannotBeZero(CErc20 asset); + + /** + * @notice Default healthFactor value returned. + * @notice Specified by child contracts (see `CTokenAdaptor.sol`, and `CompoundV2DebtAdaptor.sol`)) + */ + uint256 public immutable defaultHealthFactor; + + constructor(uint256 _healthFactor) { + defaultHealthFactor = _healthFactor; + } + + /** + * @notice The ```_getHealthFactor``` function returns the current health factor + */ + function _getHealthFactor(address _account, Comptroller comptroller) public view returns (uint256 healthFactor) { + // get the array of markets currently being used + CErc20[] memory marketsEntered; + + marketsEntered = comptroller.getAssetsIn(address(_account)); + PriceOracle oracle = comptroller.oracle(); + uint256 sumCollateral; + uint256 sumBorrow; + uint256 marketsEnteredLength = marketsEntered.length; + // Loop to calculate total collateral & total borrow for HF calcs w/ assets we're in. + for (uint256 i = 0; i < marketsEnteredLength; i++) { + CErc20 asset = marketsEntered[i]; + (uint256 oErr, uint256 cTokenBalance, uint256 borrowBalance, uint256 exchangeRate) = asset + .getAccountSnapshot(_account); + if (oErr != 0) revert CompoundV2HelperLogic__NonZeroCompoundErrorCode(oErr); + uint256 oraclePrice = oracle.getUnderlyingPrice(asset); + if (oraclePrice == 0) revert CompoundV2HelperLogic__OracleCannotBeZero(asset); + // get collateral factor from markets + (, uint256 collateralFactor, ) = comptroller.markets(address(asset)); // always scaled by 18 decimals + console.log("collateralFactor: %s",collateralFactor); + uint256 actualCollateralBacking = cTokenBalance.mulDivDown(exchangeRate, 1e18); // NOTE - this is the 1st key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(oraclePrice, 1e18); // NOTE - this is the 2nd key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(collateralFactor, 1e18); // scaling factor for collateral factor is always 1e18. + uint256 additionalBorrowBalance = borrowBalance.mulDivDown(oraclePrice, 1e18); // converts cToken underlying borrow to USD + sumCollateral = sumCollateral + actualCollateralBacking; + sumBorrow = additionalBorrowBalance + sumBorrow; + } + // now we can calculate health factor with sumCollateral and sumBorrow + if (sumBorrow == 0) { + return defaultHealthFactor; + } + healthFactor = sumCollateral.mulDivDown(1e18, sumBorrow); + } +} diff --git a/src/modules/adaptors/Frax/DebtFTokenAdaptor.sol b/src/modules/adaptors/Frax/DebtFTokenAdaptor.sol index 639066b9c..dc4c8f59e 100644 --- a/src/modules/adaptors/Frax/DebtFTokenAdaptor.sol +++ b/src/modules/adaptors/Frax/DebtFTokenAdaptor.sol @@ -273,7 +273,7 @@ contract DebtFTokenAdaptor is BaseAdaptor, FraxlendHealthFactorLogic { * @param _fraxlendPair The specified Fraxlend Pair */ function _borrowAsset(uint256 _borrowAmount, IFToken _fraxlendPair) internal virtual { - _fraxlendPair.borrowAsset(_borrowAmount, 0, address(this)); // NOTE: explitly have the collateral var as zero so Strategists must do collateral increasing tx via the CollateralFTokenAdaptor for this fraxlendPair + _fraxlendPair.borrowAsset(_borrowAmount, 0, address(this)); // NOTE: explicitly have the collateral var as zero so Strategists must do collateral increasing tx via the CollateralFTokenAdaptor for this fraxlendPair } /** diff --git a/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueCollateralAdaptor.sol b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueCollateralAdaptor.sol new file mode 100644 index 000000000..6829db3c1 --- /dev/null +++ b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueCollateralAdaptor.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, PriceRouter, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { MorphoBlueHelperLogic } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol"; +import { IMorpho, MarketParams, Id } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { MarketParamsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol"; + +/** + * @title Morpho Blue Collateral Adaptor + * @notice Allows addition and removal of collateralAssets to Morpho Blue pairs for a Cellar. + * @dev This adaptor is specifically for Morpho Blue Primitive contracts. + * To interact with a different version or custom market, a new + * adaptor will inherit from this adaptor + * and override the interface helper functions. MB refers to Morpho + * Blue throughout code. + * @author 0xEinCodes, crispymangoes + */ +contract MorphoBlueCollateralAdaptor is BaseAdaptor, MorphoBlueHelperLogic { + using SafeTransferLib for ERC20; + using Math for uint256; + using MarketParamsLib for MarketParams; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(MarketParams market) + // Where: + // `market` is the respective market used within Morpho Blue + //================= Configuration Data Specification ================= + // NA + //==================================================================== + + /** + * @notice Attempted to interact with an Morpho Blue Lending Market the Cellar is not using. + */ + error MorphoBlueCollateralAdaptor__MarketPositionsMustBeTracked(MarketParams market); + + /** + * @notice Removal of collateral causes Cellar Health Factor below what is required + */ + error MorphoBlueCollateralAdaptor__HealthFactorTooLow(MarketParams market); + + /** + * @notice Minimum Health Factor enforced after every removeCollateral() strategist function call. + * @dev Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + /** + * @param _morphoBlue immutable Morpho Blue contract (called `Morpho.sol` within Morpho Blue repo). + * @param _healthFactor Minimum Health Factor that replaces minimumHealthFactor. If using new _healthFactor, it must be greater than minimumHealthFactor. See `BaseAdaptor.sol`. + */ + constructor(address _morphoBlue, uint256 _healthFactor) MorphoBlueHelperLogic(_morphoBlue) { + _verifyConstructorMinimumHealthFactor(_healthFactor); + morphoBlue = IMorpho(_morphoBlue); + minimumHealthFactor = _healthFactor; + } + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure virtual override returns (bytes32) { + return keccak256(abi.encode("Morpho Blue Collateral Adaptor V 0.1")); + } + + //============================================ Implement Base Functions =========================================== + /** + * @notice User deposits collateralToken to Morpho Blue market. + * @param assets the amount of assets to provide as collateral on Morpho Blue. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @dev configurationData is NOT used. + */ + function deposit(uint256 assets, bytes memory adaptorData, bytes memory) public override { + // Deposit assets to Morpho Blue. + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + _validateMBMarket(market); + ERC20 collateralToken = ERC20(market.collateralToken); + _addCollateral(market, assets, collateralToken); + } + + /** + * @notice User withdraws are NOT allowed from this position. + * NOTE: collateral withdrawal calls directly from users disallowed for now. + */ + function withdraw(uint256, address, bytes memory, bytes memory) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice This position is a debt position, and user withdraws are not allowed so + * this position must return 0 for withdrawableFrom. + * NOTE: collateral withdrawal calls directly from users disallowed for now. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Returns the cellar's balance of the collateralAsset position in corresponding Morpho Blue market. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return Cellar's balance of provided collateral to specified MB market. + * @dev normal static call, thus msg.sender for most-likely Sommelier usecase is the calling cellar. + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + Id id = MarketParamsLib.id(market); + return _userCollateralBalance(id, msg.sender); + } + + /** + * @notice Returns collateral asset. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return The collateral asset in ERC20 type. + */ + function assetOf(bytes memory adaptorData) public pure override returns (ERC20) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + return ERC20(market.collateralToken); + } + + /** + * @notice This adaptor returns collateral, and not debt. + * @return Whether or not this position is a debt position. + */ + function isDebt() public pure override returns (bool) { + return false; + } + + //============================================ Strategist Functions =========================================== + + /** + * @notice Allows strategists to add collateral to the respective cellar position on specified MB Market, enabling borrowing. + * @param _market identifier of a Morpho Blue market. + * @param _collateralToDeposit The amount of `collateralToken` to add to specified MB market position. + */ + function addCollateral(MarketParams memory _market, uint256 _collateralToDeposit) public { + _validateMBMarket(_market); + ERC20 collateralToken = ERC20(_market.collateralToken); + uint256 amountToDeposit = _maxAvailable(collateralToken, _collateralToDeposit); + _addCollateral(_market, amountToDeposit, collateralToken); + } + + /** + * @notice Allows strategists to remove collateral from the respective cellar position on specified MB Market. + * @param _market identifier of a Morpho Blue market. + * @param _collateralAmount The amount of collateral to remove from specified MB market position. + */ + function removeCollateral(MarketParams memory _market, uint256 _collateralAmount) public { + _validateMBMarket(_market); + Id id = MarketParamsLib.id(_market); + if (_collateralAmount == type(uint256).max) { + _collateralAmount = _userCollateralBalance(id, address(this)); + } + _removeCollateral(_market, _collateralAmount); + if (minimumHealthFactor > (_getHealthFactor(id, _market))) { + revert MorphoBlueCollateralAdaptor__HealthFactorTooLow(_market); + } + } + + /** + * @notice Allows a strategist to call `accrueInterest()` on a MB Market cellar is using. + * @dev A strategist might want to do this if a MB market has not been interacted with + * in a while, and the strategist does not plan on interacting with it during a + * rebalance. + * @dev Calling this can increase the share price during the rebalance, + * so a strategist should consider moving some assets into reserves. + */ + function accrueInterest(MarketParams memory market) public { + _validateMBMarket(market); + _accrueInterest(market); + } + + //============================================ Helper Functions =========================================== + + /** + * @notice Validates that a given market is set up as a position in the Cellar. + * @dev This function uses `address(this)` as the address of the Cellar. + * @param _market MarketParams struct for a specific Morpho Blue market. + */ + function _validateMBMarket(MarketParams memory _market) internal view { + bytes32 positionHash = keccak256(abi.encode(identifier(), false, abi.encode(_market))); + uint32 positionId = Cellar(address(this)).registry().getPositionHashToPositionId(positionHash); + if (!Cellar(address(this)).isPositionUsed(positionId)) + revert MorphoBlueCollateralAdaptor__MarketPositionsMustBeTracked(_market); + } + + //============================== Interface Details ============================== + // General message on interface and virtual functions below: The Morpho Blue protocol is meant to be a primitive layer to DeFi, and so other projects may build atop of MB. These possible future projects may implement the same interface to simply interact with MB, and thus this adaptor is implementing a design that allows for future adaptors to simply inherit this "Base Morpho Adaptor" and override what they need appropriately to work with whatever project. Aspects that may be adjusted include using the flexible `bytes` param within `morphoBlue.supplyCollateral()` for example. + + // Current versions in use are just for the primitive Morpho Blue deployments. + // IMPORTANT: Going forward, other versions will be renamed w/ descriptive titles for new projects extending off of these primitive contracts. + //=============================================================================== + + /** + * @notice Increment collateral amount in cellar account within specified MB Market. + * @param _market The specified MB market. + * @param _assets The amount of collateral to add to MB Market position. + */ + function _addCollateral(MarketParams memory _market, uint256 _assets, ERC20 _collateralToken) internal virtual { + // pass in collateralToken because we check maxAvailable beforehand to get assets, then approve ERC20 + _collateralToken.safeApprove(address(morphoBlue), _assets); + morphoBlue.supplyCollateral(_market, _assets, address(this), hex""); + // Zero out approvals if necessary. + _revokeExternalApproval(_collateralToken, address(morphoBlue)); + } + + /** + * @notice Decrement collateral amount in cellar account within Morpho Blue lending market + * @param _market The specified MB market. + * @param _assets The amount of collateral to remove from MB Market position. + */ + function _removeCollateral(MarketParams memory _market, uint256 _assets) internal virtual { + morphoBlue.withdrawCollateral(_market, _assets, address(this), address(this)); + } +} diff --git a/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueDebtAdaptor.sol b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueDebtAdaptor.sol new file mode 100644 index 000000000..1e3ae777b --- /dev/null +++ b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueDebtAdaptor.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, PriceRouter, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { MorphoBlueHelperLogic } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol"; +import { IMorpho, MarketParams, Id } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { SharesMathLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol"; +import { MorphoLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol"; +import { MarketParamsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol"; + +/** + * @title Morpho Blue Debt Token Adaptor + * @notice Allows Cellars to borrow assets from Morpho Blue pairs. + * @dev * To interact with a different version or custom market, a new + * adaptor will inherit from this adaptor + * and override the interface helper functions. MB refers to Morpho + * Blue. + * @author 0xEinCodes, crispymangoes + */ +contract MorphoBlueDebtAdaptor is BaseAdaptor, MorphoBlueHelperLogic { + using SafeTransferLib for ERC20; + using Math for uint256; + using SharesMathLib for uint256; + using MorphoLib for IMorpho; + using MarketParamsLib for MarketParams; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(MarketParams market) + // Where: + // `market` is the respective market used within Morpho Blue + //================= Configuration Data Specification ================= + // NA + //==================================================================== + + /** + * @notice Attempted to interact with an Morpho Blue Lending Market the Cellar is not using. + */ + error MorphoBlueDebtAdaptor__MarketPositionsMustBeTracked(MarketParams market); + + /** + * @notice Attempted tx that results in unhealthy cellar. + */ + error MorphoBlueDebtAdaptor__HealthFactorTooLow(MarketParams market); + + /** + * @notice Minimum Health Factor enforced after every borrow. + * @notice Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + /** + * @param _morphoBlue immutable Morpho Blue contract (called `Morpho.sol` within Morpho Blue repo). + * @param _healthFactor Minimum Health Factor that replaces minimumHealthFactor. If using new _healthFactor, it must be greater than minimumHealthFactor. See `BaseAdaptor.sol`. + */ + constructor(address _morphoBlue, uint256 _healthFactor) MorphoBlueHelperLogic(_morphoBlue) { + _verifyConstructorMinimumHealthFactor(_healthFactor); + morphoBlue = IMorpho(_morphoBlue); + minimumHealthFactor = _healthFactor; + } + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + * @return Identifier unique to this adaptor for a shared registry. + */ + function identifier() public pure virtual override returns (bytes32) { + return keccak256(abi.encode("Morpho Blue Debt Adaptor V 0.1")); + } + + //============================================ Implement Base Functions =========================================== + + /** + * @notice User deposits are NOT allowed into this position. + */ + function deposit(uint256, bytes memory, bytes memory) public pure override { + revert BaseAdaptor__UserDepositsNotAllowed(); + } + + /** + * @notice User withdraws are NOT allowed from this position. + */ + function withdraw(uint256, address, bytes memory, bytes memory) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice This position is a debt position, and user withdraws are not allowed so + * this position must return 0 for withdrawableFrom. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Returns the cellar's balance of the respective MB market loanToken calculated from cellar borrow shares according to MB prod contracts. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return Cellar's balance of the respective MB market loanToken. + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + Id id = MarketParamsLib.id(market); + return _userBorrowBalance(id, msg.sender); + } + + /** + * @notice Returns `loanToken` from respective MB market. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return `loanToken` from respective MB market. + */ + function assetOf(bytes memory adaptorData) public pure override returns (ERC20) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + return ERC20(market.loanToken); + } + + /** + * @notice This adaptor reports values in terms of debt. + * @return Whether or not this adaptor is in terms of debt. + */ + function isDebt() public pure override returns (bool) { + return true; + } + + //============================================ Strategist Functions =========================================== + + /** + * @notice Allows strategists to borrow assets from Morpho Blue. + * @param _market identifier of a Morpho Blue market. + * @param _amountToBorrow the amount of `loanToken` to borrow on the specified MB market. + */ + function borrowFromMorphoBlue(MarketParams memory _market, uint256 _amountToBorrow) public { + _validateMBMarket(_market); + Id id = MarketParamsLib.id(_market); + _borrowAsset(_market, _amountToBorrow, address(this)); + if (minimumHealthFactor > (_getHealthFactor(id, _market))) { + revert MorphoBlueDebtAdaptor__HealthFactorTooLow(_market); + } + } + + /** + * @notice Allows strategists to repay loan debt on Morph Blue Lending Market. Make sure to call addInterest() beforehand to ensure we are repaying what is required. + * @dev Uses `_maxAvailable` helper function, see `BaseAdaptor.sol`. + * @param _market identifier of a Morpho Blue market. + * @param _debtTokenRepayAmount The amount of `loanToken` to repay. + * NOTE - MorphoBlue reverts w/ underflow/overflow error if trying to repay with more than what cellar has. That said, we will accomodate for times that strategists tries to pass in type(uint256).max. + */ + function repayMorphoBlueDebt(MarketParams memory _market, uint256 _debtTokenRepayAmount) public { + _validateMBMarket(_market); + Id id = MarketParamsLib.id(_market); + _accrueInterest(_market); + ERC20 tokenToRepay = ERC20(_market.loanToken); + uint256 debtAmountToRepay = _maxAvailable(tokenToRepay, _debtTokenRepayAmount); + tokenToRepay.safeApprove(address(morphoBlue), debtAmountToRepay); + + uint256 totalBorrowAssets = morphoBlue.totalBorrowAssets(id); + uint256 totalBorrowShares = morphoBlue.totalBorrowShares(id); + uint256 sharesToRepay = morphoBlue.borrowShares(id, address(this)); + uint256 assetsMax = sharesToRepay.toAssetsUp(totalBorrowAssets, totalBorrowShares); + + if (debtAmountToRepay >= assetsMax) { + _repayAsset(_market, sharesToRepay, 0, address(this)); + } else { + _repayAsset(_market, 0, debtAmountToRepay, address(this)); + } + + _revokeExternalApproval(tokenToRepay, address(morphoBlue)); + } + + /** + * @notice Allows a strategist to call `accrueInterest()` on a MB Market cellar is using. + * @dev A strategist might want to do this if a MB market has not been interacted with + * in a while, and the strategist does not plan on interacting with it during a + * rebalance. + * @dev Calling this can increase the share price during the rebalance, + * so a strategist should consider moving some assets into reserves. + * @param _market identifier of a Morpho Blue market. + */ + function accrueInterest(MarketParams memory _market) public { + _validateMBMarket(_market); + _accrueInterest(_market); + } + + //============================================ Helper Functions =========================================== + + /** + * @notice Validates that a given market is set up as a position in the Cellar. + * @dev This function uses `address(this)` as the address of the Cellar. + * @param _market MarketParams struct for a specific Morpho Blue market. + */ + function _validateMBMarket(MarketParams memory _market) internal view { + bytes32 positionHash = keccak256(abi.encode(identifier(), true, abi.encode(_market))); + uint32 positionId = Cellar(address(this)).registry().getPositionHashToPositionId(positionHash); + if (!Cellar(address(this)).isPositionUsed(positionId)) + revert MorphoBlueDebtAdaptor__MarketPositionsMustBeTracked(_market); + } + + //============================== Interface Details ============================== + // General message on interface and virtual functions below: The Morpho Blue protocol is meant to be a primitive layer to DeFi, and so other projects may build atop of MB. These possible future projects may implement the same interface to simply interact with MB, and thus this adaptor is implementing a design that allows for future adaptors to simply inherit this "Base Morpho Adaptor" and override what they need appropriately to work with whatever project. Aspects that may be adjusted include using the flexible `bytes` param within `morphoBlue.supplyCollateral()` for example. + + // Current versions in use are just for the primitive Morpho Blue deployments. + // IMPORTANT: Going forward, other versions will be renamed w/ descriptive titles for new projects extending off of these primitive contracts. + //=============================================================================== + + /** + * @notice Helper function to borrow specific amount of `loanToken` in cellar account within specific MB market. + * @param _market The specified MB market. + * @param _borrowAmount The amount of borrowAsset to borrow. + * @param _onBehalf The receiver of the amount of `loanToken` borrowed and receiver of debt accounting-wise. + */ + function _borrowAsset(MarketParams memory _market, uint256 _borrowAmount, address _onBehalf) internal virtual { + morphoBlue.borrow(_market, _borrowAmount, 0, _onBehalf, _onBehalf); + } + + /** + * @notice Helper function to repay specific MB market debt by an amount. + * @param _market The specified MB market. + * @param _sharesToRepay The amount of borrowShares to repay. + * @param _onBehalf The address of the debt-account reduced due to this repayment within MB market. + * @param _debtAmountToRepay The amount of debt asset to repay. + * NOTE - See IMorpho.sol for more detail, but within the external function call to the Morpho Blue contract, repayment amount param can only be either in borrowToken or borrowShares. Users need to choose btw repaying specifying amount of borrowAsset, or borrowShares, which is reflected in this helper. + */ + function _repayAsset( + MarketParams memory _market, + uint256 _sharesToRepay, + uint256 _debtAmountToRepay, + address _onBehalf + ) internal virtual { + if (_sharesToRepay != 0) { + morphoBlue.repay(_market, 0, _sharesToRepay, _onBehalf, hex""); + } else { + morphoBlue.repay(_market, _debtAmountToRepay, 0, _onBehalf, hex""); + } + } +} diff --git a/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol new file mode 100644 index 000000000..1fc4cb2a4 --- /dev/null +++ b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { IMorpho, MarketParams, Market, Id } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { MathLib, WAD } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MathLib.sol"; +import { SharesMathLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol"; +import { IOracle } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IOracle.sol"; +import { UtilsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/UtilsLib.sol"; +import { MorphoLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol"; + +/** + * @title Morpho Blue Helper contract. + * @notice Helper implementation including health factor logic used by Morpho Blue Adaptors. + * @author 0xEinCodes, crispymangoes + * NOTE: helper functions made virtual in case future Morpho Blue Pair versions require different implementation logic. + */ +contract MorphoBlueHelperLogic { + // Using libraries from Morpho Blue codebase to ensure same mathematic methods + using MathLib for uint128; + using MathLib for uint256; + using UtilsLib for uint256; + using SharesMathLib for uint256; + using MorphoLib for IMorpho; + + /** + * @notice The Morpho Blue contract on current network. + */ + IMorpho public immutable morphoBlue; + + // Constant from Morpho Blue + uint256 constant ORACLE_PRICE_SCALE = 1e36; + + constructor(address _morphoBlue) { + morphoBlue = IMorpho(_morphoBlue); + } + + /** + * @notice The ```_getHealthFactor``` function returns the current health factor of a respective position given an exchange rate. + * @param _id The specified Morpho Blue market Id. + * @param _market The specified Morpho Blue market. + * @return currentHF The health factor of the position atm. + */ + function _getHealthFactor(Id _id, MarketParams memory _market) internal view virtual returns (uint256 currentHF) { + uint256 borrowAmount = _userBorrowBalance(_id, address(this)); + if (borrowAmount == 0) return type(uint256).max; + uint256 collateralPrice = IOracle(_market.oracle).price(); // recall from IOracle.sol that the units will be 10 ** (36 - collateralUnits + borrowUnits) BUT collateralPrice is in units of borrow. + + // get collateralAmount in borrowAmount for LTV calculations + uint256 collateralAmount = _userCollateralBalance(_id, address(this)); + uint256 collateralAmountInBorrowUnits = collateralAmount.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE); + currentHF = _market.lltv.mulDivDown(collateralAmountInBorrowUnits, borrowAmount); + } + + /** + * @dev helper function that returns actual supply position amount for specified `_user` according to MB market accounting. This is alternative to using the MB periphery libraries that simulate accrued interest balances. + * @param _id identifier of a Morpho Blue market. + * @param _user address that this function will query Morpho Blue market for. + * @return Actual supply amount for the `_user` + * NOTE: make sure to call `accrueInterest()` on respective market before calling these helpers. + */ + function _userSupplyBalance(Id _id, address _user) internal view returns (uint256) { + Market memory market = morphoBlue.market(_id); + return (morphoBlue.supplyShares(_id, _user).toAssetsDown(market.totalSupplyAssets, market.totalSupplyShares)); + } + + /** + * @dev helper function that returns actual supply position shares amount for specified `_user` according to MB market accounting. + * @param _id identifier of a Morpho Blue market. + * @param _user address that this function will query Morpho Blue market for. + * @return Actual supply share amount for the `_user` + */ + function _userSupplyShareBalance(Id _id, address _user) internal view returns (uint256) { + return (morphoBlue.supplyShares(_id, _user)); + } + + /** + * @dev helper function that returns actual collateral position amount for specified `_user` according to MB market accounting. + */ + function _userCollateralBalance(Id _id, address _user) internal view virtual returns (uint256) { + return morphoBlue.collateral(_id, _user); + } + + /** + * @dev helper function that returns actual borrow position amount for specified `_user` according to MB market accounting. This is alternative to using the MB periphery libraries that simulate accrued interest balances. + * @param _id identifier of a Morpho Blue market. + * @param _user address that this function will query Morpho Blue market for. + * NOTE: make sure to call `accrueInterest()` on respective market before calling these helpers. + */ + function _userBorrowBalance(Id _id, address _user) internal view returns (uint256) { + Market memory market = morphoBlue.market(_id); + return morphoBlue.borrowShares(_id, _user).toAssetsUp(market.totalBorrowAssets, market.totalBorrowShares); + } + + /** + * @notice Caller calls `accrueInterest` on specified MB market. + * @param _market The specified MB market. + */ + function _accrueInterest(MarketParams memory _market) internal virtual { + morphoBlue.accrueInterest(_market); + } +} diff --git a/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol new file mode 100644 index 000000000..f61edbdae --- /dev/null +++ b/src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { BaseAdaptor, ERC20, SafeTransferLib, Cellar, PriceRouter, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { IMorpho, MarketParams, Id } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { MorphoBalancesLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoBalancesLib.sol"; +import { MorphoLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol"; +import { MorphoBlueHelperLogic } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol"; +import { MarketParamsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol"; + +/** + * @title Morpho Blue Supply Adaptor + * @notice Allows Cellars to lend loanToken to respective Morpho Blue Lending Markets. + * @dev adaptorData is the MarketParams struct, not Id. This is to test with market as the adaptorData. + * @dev This adaptor is specifically for Morpho Blue Primitive contracts. + * To interact with a different version or custom market, a new + * adaptor will inherit from this adaptor + * and override the interface helper functions. MB refers to Morpho + * Blue throughout code. + * @author 0xEinCodes, crispymangoes + */ +contract MorphoBlueSupplyAdaptor is BaseAdaptor, MorphoBlueHelperLogic { + using SafeTransferLib for ERC20; + using Math for uint256; + using MorphoLib for IMorpho; + using MorphoBalancesLib for IMorpho; + using MarketParamsLib for MarketParams; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(MarketParams market) + // Where: + // `market` is the respective market used within Morpho Blue + //================= Configuration Data Specification ================= + // configurationData = abi.encode(bool isLiquid) + // Where: + // `isLiquid` dictates whether the position is liquid or not + // If true: + // position can support use withdraws + // else: + // position can not support user withdraws + // + //==================================================================== + + /** + * @notice Attempted to interact with a Morpho Blue Lending Market that the Cellar is not using. + */ + error MorphoBlueSupplyAdaptor__MarketPositionsMustBeTracked(MarketParams market); + + /** + * @param _morphoBlue immutable Morpho Blue contract (called `Morpho.sol` within Morpho Blue repo). + */ + constructor(address _morphoBlue) MorphoBlueHelperLogic(_morphoBlue) { + morphoBlue = IMorpho(_morphoBlue); + } + + // ============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure virtual override returns (bytes32) { + return keccak256(abi.encode("Morpho Blue Supply Adaptor V 0.1")); + } + + //============================================ Implement Base Functions =========================================== + + /** + * @notice Allows user to deposit into MB markets, only if Cellar has a MBSupplyAdaptorPosition as its holding position. + * @dev Cellar must approve Morpho Blue to spend its assets, then call deposit to lend its assets. + * @param assets the amount of assets to lend on Morpho Blue. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @dev configurationData is NOT used. + */ + function deposit(uint256 assets, bytes memory adaptorData, bytes memory) public override { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + ERC20 loanToken = ERC20(market.loanToken); + loanToken.safeApprove(address(morphoBlue), assets); + _deposit(market, assets, address(this)); + + // Zero out approvals if necessary. + _revokeExternalApproval(loanToken, address(morphoBlue)); + } + + /** + * @notice Cellars must withdraw from Morpho Blue lending market, then transfer assets to receiver. + * @dev Important to verify that external receivers are allowed if receiver is not Cellar address. + * @param assets the amount of assets to withdraw from Morpho Blue lending market. + * @param receiver the address to send withdrawn assets to. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @param configurationData abi encoded bool indicating whether the position is liquid or not. + */ + function withdraw( + uint256 assets, + address receiver, + bytes memory adaptorData, + bytes memory configurationData + ) public override { + bool isLiquid = abi.decode(configurationData, (bool)); + if (!isLiquid) revert BaseAdaptor__UserWithdrawsNotAllowed(); + + // Run external receiver check. + _externalReceiverCheck(receiver); + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + // Withdraw assets from Morpho Blue. + _validateMBMarket(market); + _withdraw(market, assets, receiver); + } + + /** + * @notice Returns the amount of loanToken that can be withdrawn. + * @dev Compares loanToken supplied to loanToken borrowed to check for liquidity. + * - If loanToken balance is greater than liquidity available, it returns the amount available. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @param configurationData abi encoded bool indicating whether the position is liquid or not. + * @return withdrawableSupply liquid amount of `loanToken` cellar has lent to specified MB market. + */ + function withdrawableFrom( + bytes memory adaptorData, + bytes memory configurationData + ) public view override returns (uint256 withdrawableSupply) { + bool isLiquid = abi.decode(configurationData, (bool)); + + if (isLiquid) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + (uint256 totalSupplyAssets, , uint256 totalBorrowAssets, ) = morphoBlue.expectedMarketBalances(market); + if (totalBorrowAssets >= totalSupplyAssets) return 0; + uint256 liquidSupply = totalSupplyAssets - totalBorrowAssets; + uint256 cellarSuppliedBalance = morphoBlue.expectedSupplyAssets(market, msg.sender); + withdrawableSupply = cellarSuppliedBalance > liquidSupply ? liquidSupply : cellarSuppliedBalance; + } else return 0; + } + + /** + * @notice Returns the cellar's balance of the supplyToken position. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return Cellar's balance of the supplyToken position. + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + Id id = MarketParamsLib.id(market); + return _userSupplyBalance(id, msg.sender); + } + + /** + * @notice Returns loanToken. + * @param adaptorData adaptor data containing the abi encoded Morpho Blue market. + * @return ERC20 loanToken. + */ + function assetOf(bytes memory adaptorData) public pure override returns (ERC20) { + MarketParams memory market = abi.decode(adaptorData, (MarketParams)); + return ERC20(market.loanToken); + } + + /** + * @notice This adaptor returns collateral, and not debt. + * @return Whether or not this position is a debt position. + */ + function isDebt() public pure override returns (bool) { + return false; + } + + //============================================ Strategist Functions =========================================== + + /** + * @notice Allows strategists to lend a specific amount for an asset on Morpho Blue market. + * @param _market identifier of a Morpho Blue market. + * @param _assets the amount of loanToken to lend on specified MB market. + */ + function lendToMorphoBlue(MarketParams memory _market, uint256 _assets) public { + _validateMBMarket(_market); + ERC20 loanToken = ERC20(_market.loanToken); + _assets = _maxAvailable(loanToken, _assets); + loanToken.safeApprove(address(morphoBlue), _assets); + _deposit(_market, _assets, address(this)); + // Zero out approvals if necessary. + _revokeExternalApproval(loanToken, address(morphoBlue)); + } + + /** + * @notice Allows strategists to withdraw underlying asset plus interest. + * @param _market identifier of a Morpho Blue market. + * @param _assets the amount of loanToken to withdraw from MB market + */ + function withdrawFromMorphoBlue(MarketParams memory _market, uint256 _assets) public { + // Run external receiver check. + _externalReceiverCheck(address(this)); + _validateMBMarket(_market); + Id _id = MarketParamsLib.id(_market); + if (_assets == type(uint256).max) { + uint256 _shares = _userSupplyShareBalance(_id, address(this)); + _withdrawShares(_market, _shares, address(this)); + } else { + // Withdraw assets from Morpho Blue. + _withdraw(_market, _assets, address(this)); + } + } + + /** + * @notice Allows a strategist to call `accrueInterest()` on a MB Market that the cellar is using. + * @dev A strategist might want to do this if a MB market has not been interacted with + * in a while, and the strategist does not plan on interacting with it during a + * rebalance. + * @dev Calling this can increase the share price during the rebalance, + * so a strategist should consider moving some assets into reserves. + * @param _market identifier of a Morpho Blue market. + */ + function accrueInterest(MarketParams memory _market) public { + _validateMBMarket(_market); + _accrueInterest(_market); + } + + /** + * @notice Validates that a given market is set up as a position in the Cellar. + * @dev This function uses `address(this)` as the address of the Cellar. + * @param _market MarketParams struct for a specific Morpho Blue market. + */ + function _validateMBMarket(MarketParams memory _market) internal view { + bytes32 positionHash = keccak256(abi.encode(identifier(), false, abi.encode(_market))); + uint32 positionId = Cellar(address(this)).registry().getPositionHashToPositionId(positionHash); + if (!Cellar(address(this)).isPositionUsed(positionId)) + revert MorphoBlueSupplyAdaptor__MarketPositionsMustBeTracked(_market); + } + + //============================================ Interface Helper Functions =========================================== + + //============================== Interface Details ============================== + // General message on interface and virtual functions below: The Morpho Blue protocol is meant to be a primitive layer to DeFi, and so other projects may build atop of MB. These possible future projects may implement the same interface to simply interact with MB, and thus this adaptor is implementing a design that allows for future adaptors to simply inherit this "Base Morpho Adaptor" and override what they need appropriately to work with whatever project. Aspects that may be adjusted include using the flexible `bytes` param within `morphoBlue.supplyCollateral()` for example. + + // Current versions in use are just for the primitive Morpho Blue deployments. + // IMPORTANT: Going forward, other versions will be renamed w/ descriptive titles for new projects extending off of these primitive contracts. + //=============================================================================== + + /** + * @notice Deposit loanToken into specified MB lending market. + * @param _market The specified MB market. + * @param _assets The amount of `loanToken` to transfer to MB market. + * @param _onBehalf The address that MB market records as having supplied this amount of `loanToken` as a lender. + * @dev The mutative functions for supplying and withdrawing have params for share amounts of asset amounts, where one of these respective params must be zero. + */ + function _deposit(MarketParams memory _market, uint256 _assets, address _onBehalf) internal virtual { + morphoBlue.supply(_market, _assets, 0, _onBehalf, hex""); + } + + /** + * @notice Withdraw loanToken from specified MB lending market by specifying amount of assets to withdraw. + * @param _market The specified MB Market. + * @param _assets The amount to withdraw. + * @param _onBehalf The address to which the Asset Tokens will be transferred. + * @dev The mutative functions for supplying and withdrawing have params for share amounts of asset amounts, where one of these respective params must be zero. + */ + function _withdraw(MarketParams memory _market, uint256 _assets, address _onBehalf) internal virtual { + morphoBlue.withdraw(_market, _assets, 0, address(this), _onBehalf); + } + + /** + * @notice Withdraw loanToken from specified MB lending market by specifying amount of shares to redeem. + * @param _market The specified MB Market. + * @param _shares The shares to redeem. + * @param _onBehalf The address to which the Asset Tokens will be transferred. + * @dev The mutative functions for supplying and withdrawing have params for share amounts of asset amounts, where one of these respective params must be zero. + */ function _withdrawShares(MarketParams memory _market, uint256 _shares, address _onBehalf) internal virtual { + morphoBlue.withdraw(_market, 0, _shares, address(this), _onBehalf); + } +} diff --git a/src/modules/multi-chain-share/DestinationMinter.sol b/src/modules/multi-chain-share/DestinationMinter.sol new file mode 100644 index 000000000..6b6e927b4 --- /dev/null +++ b/src/modules/multi-chain-share/DestinationMinter.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; +import { IRouterClient } from "@ccip/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol"; + +/** + * @title DestinationMinter + * @notice Receives CCIP messages from SourceLocker, to mint ERC20 shares that + * represent ERC4626 shares locked on source chain. + * @author crispymangoes + */ +contract DestinationMinter is ERC20, CCIPReceiver { + using SafeTransferLib for ERC20; + + //============================== ERRORS =============================== + + error DestinationMinter___SourceChainNotAllowlisted(uint64 sourceChainSelector); + error DestinationMinter___SenderNotAllowlisted(address sender); + error DestinationMinter___InvalidTo(); + error DestinationMinter___FeeTooHigh(); + + //============================== EVENTS =============================== + + event BridgeToSource(uint256 amount, address to); + event BridgeFromSource(uint256 amount, address to); + + //============================== MODIFIERS =============================== + + modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) { + if (_sourceChainSelector != sourceChainSelector) + revert DestinationMinter___SourceChainNotAllowlisted(_sourceChainSelector); + if (_sender != targetSource) revert DestinationMinter___SenderNotAllowlisted(_sender); + _; + } + + //============================== IMMUTABLES =============================== + + /** + * @notice The address of the SourceLocker on source chain. + */ + address public immutable targetSource; + + /** + * @notice The CCIP source chain selector. + */ + uint64 public immutable sourceChainSelector; + + /** + * @notice The CCIP destination chain selector. + */ + uint64 public immutable destinationChainSelector; + + /** + * @notice This networks LINK contract. + */ + ERC20 public immutable LINK; + + /** + * @notice The message gas limit to use for CCIP messages. + */ + uint256 public immutable messageGasLimit; + + constructor( + address _router, + address _targetSource, + string memory _name, + string memory _symbol, + uint8 _decimals, + uint64 _sourceChainSelector, + uint64 _destinationChainSelector, + address _link, + uint256 _messageGasLimit + ) ERC20(_name, _symbol, _decimals) CCIPReceiver(_router) { + targetSource = _targetSource; + sourceChainSelector = _sourceChainSelector; + destinationChainSelector = _destinationChainSelector; + LINK = ERC20(_link); + messageGasLimit = _messageGasLimit; + } + + //============================== BRIDGE =============================== + + /** + * @notice Bridge shares back to source chain. + * @dev Caller should approve LINK to be spent by this contract. + * @param amount Number of shares to burn on destination network and unlock/transfer on source network. + * @param to Specified address to burn destination network `share` tokens, and receive unlocked `share` tokens on source network. + * @param maxLinkToPay Specified max amount of LINK fees to pay as per this contract. + * @return messageId Resultant CCIP messageId. + */ + function bridgeToSource(uint256 amount, address to, uint256 maxLinkToPay) external returns (bytes32 messageId) { + if (to == address(0)) revert DestinationMinter___InvalidTo(); + _burn(msg.sender, amount); + + Client.EVM2AnyMessage memory message = _buildMessage(amount, to); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(sourceChainSelector, message); + + if (fees > maxLinkToPay) revert DestinationMinter___FeeTooHigh(); + + LINK.safeTransferFrom(msg.sender, address(this), fees); + + LINK.safeApprove(address(router), fees); + + messageId = router.ccipSend(sourceChainSelector, message); + emit BridgeToSource(amount, to); + } + + //============================== VIEW FUNCTIONS =============================== + + /** + * @notice Preview fee required to bridge shares back to source. + * @param amount Specified amount of `share` tokens to bridge to source network. + * @param to Specified address to receive bridged shares on source network. + * @return fee required to bridge shares. + */ + function previewFee(uint256 amount, address to) public view returns (uint256 fee) { + Client.EVM2AnyMessage memory message = _buildMessage(amount, to); + + IRouterClient router = IRouterClient(this.getRouter()); + + fee = router.getFee(sourceChainSelector, message); + } + + //============================== CCIP RECEIVER =============================== + + /** + * @notice Implement internal _ccipRecevie function logic. + * @param any2EvmMessage CCIP encoded message specifying details to use to 'mint' `share` tokens to a specified address `to` on destination network. + */ + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) + internal + override + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + (uint256 amount, address to) = abi.decode(any2EvmMessage.data, (uint256, address)); + _mint(to, amount); + emit BridgeFromSource(amount, to); + } + + //============================== INTERNAL HELPER =============================== + + /** + * @notice Build the CCIP message to send to source locker. + * @param amount number of `share` token to bridge. + * @param to Specified address to receive unlocked bridged shares on source network. + * @return message the CCIP message to send to source locker. + */ + function _buildMessage(uint256 amount, address to) internal view returns (Client.EVM2AnyMessage memory message) { + message = Client.EVM2AnyMessage({ + receiver: abi.encode(targetSource), + data: abi.encode(amount, to), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit and non-strict sequencing mode + Client.EVMExtraArgsV1({ gasLimit: messageGasLimit /*, strict: false*/ }) + ), + feeToken: address(LINK) + }); + } +} diff --git a/src/modules/multi-chain-share/DestinationMinterFactory.sol b/src/modules/multi-chain-share/DestinationMinterFactory.sol new file mode 100644 index 000000000..a34e897e4 --- /dev/null +++ b/src/modules/multi-chain-share/DestinationMinterFactory.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Owned } from "@solmate/auth/Owned.sol"; +import { Math } from "src/utils/Math.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; +import { DestinationMinter } from "./DestinationMinter.sol"; +import { IRouterClient } from "@ccip/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; + +/** + * @title DestinationMinterFactory + * @notice Works with SourceLockerFactory to create pairs of Source Lockers & Destination Minters for new bridgeable ERC4626 Shares + * @dev Source Lockers lock up shares to bridge a mint request to paired Destination Minters, where the representation of the Source Network Shares is minted on Destination Network. + * @author crispymangoes + */ +contract DestinationMinterFactory is Owned, CCIPReceiver { + using SafeTransferLib for ERC20; + + // ========================================= GLOBAL STATE ========================================= + /** + * @notice Mapping to keep track of failed CCIP messages, and retry them at a later time. + */ + mapping(bytes32 => bool) public canRetryFailedMessage; + + /** + * @notice The message gas limit to use for CCIP messages. + */ + uint256 public messageGasLimit; + + /** + * @notice The message gas limit DestinationMinter's will use to send messages to their SourceLockers. + */ + uint256 public minterMessageGasLimit; + + //============================== ERRORS =============================== + + error DestinationMinterFactory___SourceChainNotAllowlisted(uint64 sourceChainSelector); + error DestinationMinterFactory___SenderNotAllowlisted(address sender); + error DestinationMinterFactory___NotEnoughLink(); + error DestinationMinterFactory___CanNotRetryCallback(); + + //============================== EVENTS =============================== + + event CallBackMessageId(bytes32 id); + event FailedToSendCallBack(address source, address minter); + + //============================== MODIFIERS =============================== + + modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) { + if (_sourceChainSelector != sourceChainSelector) + revert DestinationMinterFactory___SourceChainNotAllowlisted(_sourceChainSelector); + if (_sender != sourceLockerFactory) revert DestinationMinterFactory___SenderNotAllowlisted(_sender); + _; + } + + //============================== IMMUTABLES =============================== + + /** + * @notice The address of the SourceLockerFactory. + */ + address public immutable sourceLockerFactory; + + /** + * @notice The CCIP source chain selector. + */ + uint64 public immutable sourceChainSelector; + + /** + * @notice The CCIP destination chain selector. + */ + uint64 public immutable destinationChainSelector; + + /** + * @notice This networks LINK contract. + */ + ERC20 public immutable LINK; + + constructor( + address _owner, + address _router, + address _sourceLockerFactory, + uint64 _sourceChainSelector, + uint64 _destinationChainSelector, + address _link, + uint256 _messageGasLimit, + uint256 _minterMessageGasLimit + ) Owned(_owner) CCIPReceiver(_router) { + sourceLockerFactory = _sourceLockerFactory; + sourceChainSelector = _sourceChainSelector; + destinationChainSelector = _destinationChainSelector; + LINK = ERC20(_link); + messageGasLimit = _messageGasLimit; + minterMessageGasLimit = _minterMessageGasLimit; + } + + //============================== ADMIN FUNCTIONS =============================== + + /** + * @notice Allows admin to withdraw ERC20s from this factory contract. + * @param token specified ERC20 to withdraw. + * @param amount number of ERC20 token to withdraw. + * @param to receiver of the respective ERC20 tokens. + */ + function adminWithdraw(ERC20 token, uint256 amount, address to) external onlyOwner { + token.safeTransfer(to, amount); + } + + /** + * @notice Allows admin to set this factories callback CCIP message gas limit. + * @dev Note Owner can set a gas limit that is too low, and cause the callback messages to run out of gas. + * If this happens the owner should raise gas limit, and call `deploy` on SourceLockerFactory again. + * @param limit Specified CCIP message gas limit. + */ + function setMessageGasLimit(uint256 limit) external onlyOwner { + messageGasLimit = limit; + } + + /** + * @notice Allows admin to set newly deployed DestinationMinter message gas limits + * @dev Note This only effects newly deployed DestinationMinters. + * @param limit Specified CCIP message gas limit. + */ + function setMinterMessageGasLimit(uint256 limit) external onlyOwner { + minterMessageGasLimit = limit; + } + + //============================== RETRY FUNCTIONS =============================== + + /** + * @notice Allows anyone to retry sending callback to source locker factory. + * @param targetSource The Source Locker (on source network). + * @param targetMinter The Destination Minter (on this Destination Network). + */ + function retryCallback(address targetSource, address targetMinter) external { + bytes32 messageDataHash = keccak256(abi.encode(targetSource, targetMinter)); + if (!canRetryFailedMessage[messageDataHash]) revert DestinationMinterFactory___CanNotRetryCallback(); + + canRetryFailedMessage[messageDataHash] = false; + + Client.EVM2AnyMessage memory message = _buildMessage(targetSource, targetMinter); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(sourceChainSelector, message); + + if (fees > LINK.balanceOf(address(this))) revert DestinationMinterFactory___NotEnoughLink(); + + LINK.safeApprove(address(router), fees); + + bytes32 messageId = router.ccipSend(sourceChainSelector, message); + emit CallBackMessageId(messageId); + } + + //============================== CCIP RECEIVER =============================== + + /** + * @notice Implement internal _ccipReceive function logic. + * @param any2EvmMessage CCIP encoded message specifying details to use to create paired DestinationMinter. + */ + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) + internal + override + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + (address targetSource, string memory name, string memory symbol, uint8 decimals) = abi.decode( + any2EvmMessage.data, + (address, string, string, uint8) + ); + + IRouterClient router = IRouterClient(this.getRouter()); + + address targetMinter = address( + new DestinationMinter( + address(router), + targetSource, + name, + symbol, + decimals, + sourceChainSelector, + destinationChainSelector, + address(LINK), + minterMessageGasLimit + ) + ); + // CCIP sends message back to SourceLockerFactory with new DestinationMinter address, and corresponding source locker + Client.EVM2AnyMessage memory message = _buildMessage(targetSource, targetMinter); + + uint256 fees = router.getFee(sourceChainSelector, message); + + if (fees > LINK.balanceOf(address(this))) { + // Fees is larger than the LINK in contract, so update `canRetryFailedMessage`, and return. + bytes32 messageDataHash = keccak256(abi.encode(targetSource, targetMinter)); + canRetryFailedMessage[messageDataHash] = true; + emit FailedToSendCallBack(targetSource, targetMinter); + return; + } + + LINK.safeApprove(address(router), fees); + + bytes32 messageId = router.ccipSend(sourceChainSelector, message); + emit CallBackMessageId(messageId); + } + + //============================== INTERNAL HELPER FUNCTIONS =============================== + + /** + * @notice Build the CCIP message to send to source locker factory. + * @param targetSource The Source Locker (on source network). + * @param targetMinter The Destination Minter (on this Destination Network). + * @return message the CCIP message to send to source locker factory. + */ + function _buildMessage( + address targetSource, + address targetMinter + ) internal view returns (Client.EVM2AnyMessage memory message) { + message = Client.EVM2AnyMessage({ + receiver: abi.encode(sourceLockerFactory), + data: abi.encode(targetSource, targetMinter), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit and non-strict sequencing mode + Client.EVMExtraArgsV1({ gasLimit: messageGasLimit /*, strict: false*/ }) + ), + feeToken: address(LINK) + }); + } +} diff --git a/src/modules/multi-chain-share/SourceLocker.sol b/src/modules/multi-chain-share/SourceLocker.sol new file mode 100644 index 000000000..c3ae1a05c --- /dev/null +++ b/src/modules/multi-chain-share/SourceLocker.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { IRouterClient } from "@ccip/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol"; + +/** + * @title SourceLocker + * @notice Sends and receives CCIP messages to/from DestinationMinter to lock&mint / redeem&release ERC4626 shares from destination chain. + * @author crispymangoes + */ +contract SourceLocker is CCIPReceiver { + using SafeTransferLib for ERC20; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice The Destination Minter on destination chain. + */ + address public targetDestination; + + //============================== ERRORS =============================== + + error SourceLocker___SourceChainNotAllowlisted(uint64 sourceChainSelector); + error SourceLocker___SenderNotAllowlisted(address sender); + error SourceLocker___OnlyFactory(); + error SourceLocker___TargetDestinationAlreadySet(); + error SourceLocker___InvalidTo(); + error SourceLocker___FeeTooHigh(); + error SourceLocker___TargetDestinationNotSet(); + + //============================== EVENTS =============================== + + event BridgeToDestination(uint256 amount, address to); + event BridgeFromDestination(uint256 amount, address to); + + //============================== MODIFIERS =============================== + + modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) { + if (_sourceChainSelector != destinationChainSelector) + revert SourceLocker___SourceChainNotAllowlisted(_sourceChainSelector); + if (_sender != targetDestination) revert SourceLocker___SenderNotAllowlisted(_sender); + _; + } + + //============================== IMMUTABLES =============================== + + /** + * @notice ERC20 share token to bridge. + */ + ERC20 public immutable shareToken; + + /** + * @notice The address of the SourceLockerFactory. + */ + address public immutable factory; + + /** + * @notice The CCIP source chain selector. + */ + uint64 public immutable sourceChainSelector; + + /** + * @notice The CCIP destination chain selector. + */ + uint64 public immutable destinationChainSelector; + + /** + * @notice This networks LINK contract. + */ + ERC20 public immutable LINK; + + /** + * @notice The message gas limit to use for CCIP messages. + */ + uint256 public immutable messageGasLimit; + + constructor( + address _router, + address _shareToken, + address _factory, + uint64 _sourceChainSelector, + uint64 _destinationChainSelector, + address _link, + uint256 _messageGasLimit + ) CCIPReceiver(_router) { + shareToken = ERC20(_shareToken); + factory = _factory; + sourceChainSelector = _sourceChainSelector; + destinationChainSelector = _destinationChainSelector; + LINK = ERC20(_link); + messageGasLimit = _messageGasLimit; + } + + //============================== ONLY FACTORY =============================== + + /** + * @notice Allows factory to set target destination. + * @param _targetDestination The Destination Minter to pair with this Source Locker. + */ + function setTargetDestination(address _targetDestination) external { + if (msg.sender != factory) revert SourceLocker___OnlyFactory(); + if (targetDestination != address(0)) revert SourceLocker___TargetDestinationAlreadySet(); + + targetDestination = _targetDestination; + } + + //============================== BRIDGE =============================== + + /** + * @notice Bridge shares to destination chain. + * @notice Reverts if target destination is not yet set. + * @param amount number of `share` token to bridge. + * @param to Specified address to receive newly minted bridged shares on destination network. + * @param maxLinkToPay Specified max amount of LINK fees to pay. + * @return messageId Resultant CCIP messageId. + */ + function bridgeToDestination( + uint256 amount, + address to, + uint256 maxLinkToPay + ) external returns (bytes32 messageId) { + if (to == address(0)) revert SourceLocker___InvalidTo(); + shareToken.safeTransferFrom(msg.sender, address(this), amount); + + Client.EVM2AnyMessage memory message = _buildMessage(amount, to); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(destinationChainSelector, message); + + if (fees > maxLinkToPay) revert SourceLocker___FeeTooHigh(); + + LINK.safeTransferFrom(msg.sender, address(this), fees); + + LINK.safeApprove(address(router), fees); + + messageId = router.ccipSend(destinationChainSelector, message); + emit BridgeToDestination(amount, to); + } + + //============================== VIEW FUNCTIONS =============================== + + /** + * @notice Preview fee required to bridge shares to destination. + * @param amount Specified amount of `share` tokens to bridge to destination network. + * @param to Specified address to receive newly minted bridged shares on destination network. + * @return fee required to bridge shares. + */ + function previewFee(uint256 amount, address to) public view returns (uint256 fee) { + Client.EVM2AnyMessage memory message = _buildMessage(amount, to); + + IRouterClient router = IRouterClient(this.getRouter()); + + fee = router.getFee(destinationChainSelector, message); + } + + //============================== CCIP RECEIVER =============================== + + /** + * @notice Implement internal _ccipReceive function logic. + * @param any2EvmMessage CCIP encoded message specifying details to use to 'unlock' `share` tokens to transfer to specified address `to`. + */ + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) + internal + override + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + (uint256 amount, address to) = abi.decode(any2EvmMessage.data, (uint256, address)); + shareToken.safeTransfer(to, amount); + emit BridgeFromDestination(amount, to); + } + + //============================== INTERNAL HELPER =============================== + + /** + * @notice Build the CCIP message to enact minting of bridged `share` tokens via destination minter on destination network. + * @notice Reverts if target destination is not yet set. + * @param amount number of `share` token to bridge. + * @param to Specified address to receive newly minted bridged shares on destination network. + * @return message the CCIP message to send to destination minter. + */ + function _buildMessage(uint256 amount, address to) internal view returns (Client.EVM2AnyMessage memory message) { + address _targetDestination = targetDestination; + if (_targetDestination == address(0)) revert SourceLocker___TargetDestinationNotSet(); + message = Client.EVM2AnyMessage({ + receiver: abi.encode(_targetDestination), + data: abi.encode(amount, to), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit and non-strict sequencing mode + Client.EVMExtraArgsV1({ gasLimit: messageGasLimit /*, strict: false*/ }) + ), + feeToken: address(LINK) + }); + } +} diff --git a/src/modules/multi-chain-share/SourceLockerFactory.sol b/src/modules/multi-chain-share/SourceLockerFactory.sol new file mode 100644 index 000000000..f321141c8 --- /dev/null +++ b/src/modules/multi-chain-share/SourceLockerFactory.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Owned } from "@solmate/auth/Owned.sol"; +import { SourceLocker } from "./SourceLocker.sol"; +import { ERC4626 } from "@solmate/mixins/ERC4626.sol"; +import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; +import { IRouterClient } from "@ccip/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; + +/** + * @title SourceLockerFactory + * @notice Works with DestinationMinterFactory to create pairs of Source Lockers & Destination Minters for new bridgeable ERC4626 Shares + * @dev SourceLockerFactory `deploy()` function is used to enact the creation of SourceLocker and DestinationMinter pairs. + * @dev Source Lockers lock up shares to bridge a mint request to paired Destination Minters, where the representation of the Source Network Shares is minted on Destination Network. + * @author crispymangoes + */ +contract SourceLockerFactory is Owned, CCIPReceiver { + using SafeTransferLib for ERC20; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice Destination Minter Factory. + */ + address public destinationMinterFactory; + + /** + * @notice The message gas limit to use for CCIP messages. + */ + uint256 public messageGasLimit; + + /** + * @notice The message gas limit SourceLockers's will use to send messages to their DestinationMinters. + */ + uint256 public lockerMessageGasLimit; + + //============================== ERRORS =============================== + + error SourceLockerFactory___SourceChainNotAllowlisted(uint64 sourceChainSelector); + error SourceLockerFactory___SenderNotAllowlisted(address sender); + error SourceLockerFactory___NotEnoughLink(); + error SourceLockerFactory___FactoryAlreadySet(); + + //============================== EVENTS =============================== + + event DeploySuccess(address share, address locker, address minter); + + //============================== MODIFIERS =============================== + + modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) { + if (_sourceChainSelector != destinationChainSelector) + revert SourceLockerFactory___SourceChainNotAllowlisted(_sourceChainSelector); + if (_sender != destinationMinterFactory) revert SourceLockerFactory___SenderNotAllowlisted(_sender); + _; + } + + //============================== IMMUTABLES =============================== + + /** + * @notice The CCIP source chain selector. + */ + uint64 public immutable sourceChainSelector; + + /** + * @notice The CCIP destination chain selector. + */ + uint64 public immutable destinationChainSelector; + + /** + * @notice This network's LINK contract. + */ + ERC20 public immutable LINK; + + constructor( + address _owner, + address _router, + uint64 _sourceChainSelector, + uint64 _destinationChainSelector, + address _link, + uint256 _messageGasLimit, + uint256 _lockerMessageGasLimit + ) Owned(_owner) CCIPReceiver(_router) { + sourceChainSelector = _sourceChainSelector; + destinationChainSelector = _destinationChainSelector; + LINK = ERC20(_link); + messageGasLimit = _messageGasLimit; + lockerMessageGasLimit = _lockerMessageGasLimit; + } + + //============================== ADMIN FUNCTIONS =============================== + + /** + * @notice Allows admin to withdraw ERC20s from this factory contract. + * @param token specified ERC20 to withdraw. + * @param amount number of ERC20 token to withdraw. + * @param to receiver of the respective ERC20 tokens. + */ + function adminWithdraw(ERC20 token, uint256 amount, address to) external onlyOwner { + token.safeTransfer(to, amount); + } + + /** + * @notice Allows admin to link DestinationMinterFactory to this factory. + * @param _destinationMinterFactory The specified DestinationMinterFactory to pair with this SourceLockerFactory. + */ + function setDestinationMinterFactory(address _destinationMinterFactory) external onlyOwner { + if (destinationMinterFactory != address(0)) revert SourceLockerFactory___FactoryAlreadySet(); + destinationMinterFactory = _destinationMinterFactory; + } + + /** + * @notice Allows admin to set this factories CCIP message gas limit. + * @dev Note Owner can set a gas limit that is too low, and cause the message to run out of gas. + * If this happens the owner should raise gas limit, and call `deploy` on SourceLockerFactory again. + * @param limit Specified CCIP message gas limit. + */ + function setMessageGasLimit(uint256 limit) external onlyOwner { + messageGasLimit = limit; + } + + /** + * @notice Allows admin to set newly deployed SourceLocker message gas limits + * @dev Note This only effects newly deployed SourceLockers. + * @param limit Specified CCIP message gas limit. + */ + function setLockerMessageGasLimit(uint256 limit) external onlyOwner { + lockerMessageGasLimit = limit; + } + + /** + * @notice Allows admin to deploy a new SourceLocker and DestinationMinter, for a given `share`. + * @dev Note Owner can set a gas limit that is too low, and cause the message to run out of gas. + * If this happens the owner should raise gas limit, and call `deploy` on SourceLockerFactory again. + * @param target Specified `share` token for a ERC4626 vault. + * @return messageId Resultant CCIP messageId. + * @return newLocker Newly deployed Source Locker for specified `target` + */ + function deploy(ERC20 target) external onlyOwner returns (bytes32 messageId, address newLocker) { + // Deploy a new Source Target + SourceLocker locker = new SourceLocker( + this.getRouter(), + address(target), + address(this), + sourceChainSelector, + destinationChainSelector, + address(LINK), + lockerMessageGasLimit + ); + // CCIP Send new Source Target address, target.name(), target.symbol(), target.decimals() to DestinationMinterFactory. + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(destinationMinterFactory), + data: abi.encode(address(locker), target.name(), target.symbol(), target.decimals()), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit and non-strict sequencing mode + Client.EVMExtraArgsV1({ gasLimit: messageGasLimit /*, strict: false*/ }) + ), + feeToken: address(LINK) + }); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(destinationChainSelector, message); + + if (fees > LINK.balanceOf(address(this))) revert SourceLockerFactory___NotEnoughLink(); + + LINK.safeApprove(address(router), fees); + + messageId = router.ccipSend(destinationChainSelector, message); + newLocker = address(locker); + } + + //============================== CCIP RECEIVER =============================== + + /** + * @notice Implement internal _ccipReceive function logic. + * @param any2EvmMessage CCIP encoded message specifying details to use to `setTargetDestination()` && finish creating pair of Source Locker & Destination Minter for specified `share`. + */ + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) + internal + override + onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) + { + (address targetLocker, address targetDestination) = abi.decode(any2EvmMessage.data, (address, address)); + + SourceLocker locker = SourceLocker(targetLocker); + + locker.setTargetDestination(targetDestination); + + emit DeploySuccess(address(locker.shareToken()), targetLocker, targetDestination); + } +} diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index a41149649..af4fa85ad 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -5,6 +5,7 @@ import { ReentrancyERC4626 } from "src/mocks/ReentrancyERC4626.sol"; import { CellarAdaptor } from "src/modules/adaptors/Sommelier/CellarAdaptor.sol"; import { ERC20DebtAdaptor } from "src/mocks/ERC20DebtAdaptor.sol"; import { MockDataFeed } from "src/mocks/MockDataFeed.sol"; +import { CellarWithViewFunctions } from "src/mocks/CellarWithViewFunctions.sol"; // Import Everything from Starter file. import "test/resources/MainnetStarter.t.sol"; @@ -16,10 +17,10 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { using Math for uint256; using stdStorage for StdStorage; - Cellar private cellar; - Cellar private usdcCLR; - Cellar private wethCLR; - Cellar private wbtcCLR; + CellarWithViewFunctions private cellar; + CellarWithViewFunctions private usdcCLR; + CellarWithViewFunctions private wethCLR; + CellarWithViewFunctions private wbtcCLR; CellarAdaptor private cellarAdaptor; @@ -96,19 +97,40 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { uint256 initialDeposit = 1e6; uint64 platformCut = 0.75e18; - usdcCLR = _createCellar(cellarName, USDC, usdcPosition, abi.encode(true), initialDeposit, platformCut); + usdcCLR = _createCellarWithViewFunctions( + cellarName, + USDC, + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut + ); vm.label(address(usdcCLR), "usdcCLR"); cellarName = "Dummy Cellar V0.1"; initialDeposit = 1e12; platformCut = 0.75e18; - wethCLR = _createCellar(cellarName, WETH, wethPosition, abi.encode(true), initialDeposit, platformCut); + wethCLR = _createCellarWithViewFunctions( + cellarName, + WETH, + wethPosition, + abi.encode(true), + initialDeposit, + platformCut + ); vm.label(address(wethCLR), "wethCLR"); cellarName = "Dummy Cellar V0.2"; initialDeposit = 1e4; platformCut = 0.75e18; - wbtcCLR = _createCellar(cellarName, WBTC, wbtcPosition, abi.encode(true), initialDeposit, platformCut); + wbtcCLR = _createCellarWithViewFunctions( + cellarName, + WBTC, + wbtcPosition, + abi.encode(true), + initialDeposit, + platformCut + ); vm.label(address(wbtcCLR), "wbtcCLR"); // Add Cellar Positions to the registry. @@ -119,7 +141,14 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { cellarName = "Cellar V0.0"; initialDeposit = 1e6; platformCut = 0.75e18; - cellar = _createCellar(cellarName, USDC, usdcPosition, abi.encode(true), initialDeposit, platformCut); + cellar = _createCellarWithViewFunctions( + cellarName, + USDC, + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut + ); // Set up remaining cellar positions. cellar.addPositionToCatalogue(usdcCLRPosition); @@ -183,7 +212,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { for (uint256 i = 0; i < 6; i++) { assertEq(positions[i], expectedPositions[i], "Positions should have been written to Cellar."); uint32 position = positions[i]; - (address adaptor, bool isDebt, bytes memory adaptorData, ) = cellar.getPositionData(position); + (address adaptor, bool isDebt, bytes memory adaptorData, ) = cellar.getPositionDataView(position); assertEq(adaptor, expectedAdaptor[i], "Position adaptor not initialized properly."); assertEq(isDebt, false, "There should be no debt positions."); assertEq(adaptorData, expectedAdaptorData[i], "Position adaptor data not initialized properly."); @@ -309,7 +338,14 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { string memory cellarName = "Dummy Cellar V0.3"; uint256 initialDeposit = 1e12; uint64 platformCut = 0.75e18; - Cellar wethVault = _createCellar(cellarName, WETH, wethPosition, abi.encode(true), initialDeposit, platformCut); + Cellar wethVault = _createCellarWithViewFunctions( + cellarName, + WETH, + wethPosition, + abi.encode(true), + initialDeposit, + platformCut + ); uint32 newWETHPosition = 10; registry.trustPosition(newWETHPosition, address(cellarAdaptor), abi.encode(wethVault)); @@ -478,7 +514,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { ); assertFalse(cellar.isPositionUsed(wethPosition), "`isPositionUsed` should be false for WETH."); - (address zeroAddressAdaptor, , , ) = cellar.getPositionData(wethPosition); + (address zeroAddressAdaptor, , , ) = cellar.getPositionDataView(wethPosition); assertEq(zeroAddressAdaptor, address(0), "Removing position should have deleted position data."); // Check that adding a credit position as debt reverts. vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__DebtMismatch.selector, wethPosition))); @@ -493,7 +529,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { "Cellar positions array should be equal to previous length." ); - assertEq(cellar.creditPositions(4), wethPosition, "`positions[4]` should be WETH."); + assertEq(cellar.getCreditPosition(4), wethPosition, "`positions[4]` should be WETH."); assertTrue(cellar.isPositionUsed(wethPosition), "`isPositionUsed` should be true for WETH."); // Check that `addPosition` reverts if position is already used. @@ -531,7 +567,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { // Check that addPosition sets position data. cellar.addPosition(4, wethPosition, abi.encode(true), false); (address adaptor, bool isDebt, bytes memory adaptorData, bytes memory configurationData) = cellar - .getPositionData(wethPosition); + .getPositionDataView(wethPosition); assertEq(adaptor, address(erc20Adaptor), "Adaptor should be the ERC20 adaptor."); assertTrue(!isDebt, "Position should not be debt."); assertEq(adaptorData, abi.encode((WETH)), "Adaptor data should be abi encoded WETH."); @@ -539,8 +575,8 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { // Check that `swapPosition` works as expected. cellar.swapPositions(4, 2, false); - assertEq(cellar.creditPositions(4), wethCLRPosition, "`positions[4]` should be wethCLR."); - assertEq(cellar.creditPositions(2), wethPosition, "`positions[2]` should be WETH."); + assertEq(cellar.getCreditPosition(4), wethCLRPosition, "`positions[4]` should be wethCLR."); + assertEq(cellar.getCreditPosition(2), wethPosition, "`positions[2]` should be WETH."); // Try setting the holding position to an unused position. uint32 invalidPositionId = 100; @@ -644,13 +680,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { // Max rebalance deviation value is 10%. uint256 deviation = 0.2e18; vm.expectRevert( - bytes( - abi.encodeWithSelector( - Cellar.Cellar__InvalidRebalanceDeviation.selector, - deviation, - cellar.MAX_REBALANCE_DEVIATION() - ) - ) + bytes(abi.encodeWithSelector(Cellar.Cellar__InvalidRebalanceDeviation.selector, deviation, 0.1e18)) ); cellar.setRebalanceDeviation(deviation); } @@ -957,7 +987,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { uint256 initialDeposit = 1e6; uint64 platformCut = 0.75e18; - Cellar debtCellar = _createCellar( + CellarWithViewFunctions debtCellar = _createCellarWithViewFunctions( cellarName, USDC, usdcPosition, @@ -971,7 +1001,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { debtCellar.addPosition(0, debtWethPosition, abi.encode(0), true); //constructor should set isDebt - (, bool isDebt, , ) = debtCellar.getPositionData(debtWethPosition); + (, bool isDebt, , ) = debtCellar.getPositionDataView(debtWethPosition); assertTrue(isDebt, "Constructor should have set WETH as a debt position."); assertEq(debtCellar.getDebtPositions().length, 1, "Cellar should have 1 debt position"); @@ -980,7 +1010,7 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { debtCellar.addPosition(0, debtWbtcPosition, abi.encode(0), true); assertEq(debtCellar.getDebtPositions().length, 2, "Cellar should have 2 debt positions"); - (, isDebt, , ) = debtCellar.getPositionData(debtWbtcPosition); + (, isDebt, , ) = debtCellar.getPositionDataView(debtWbtcPosition); assertTrue(isDebt, "Constructor should have set WBTC as a debt position."); assertEq(debtCellar.getDebtPositions().length, 2, "Cellar should have 2 debt positions"); @@ -1013,7 +1043,14 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { string memory cellarName = "Cellar B V0.0"; uint256 initialDeposit = 1e6; uint64 platformCut = 0.75e18; - Cellar cellarB = _createCellar(cellarName, USDC, usdcPosition, abi.encode(true), initialDeposit, platformCut); + Cellar cellarB = _createCellarWithViewFunctions( + cellarName, + USDC, + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut + ); uint32 cellarBPosition = 10; registry.trustPosition(cellarBPosition, address(cellarAdaptor), abi.encode(cellarB)); @@ -1022,7 +1059,14 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { cellarName = "Cellar A V0.0"; initialDeposit = 1e6; platformCut = 0.75e18; - Cellar cellarA = _createCellar(cellarName, USDC, usdcPosition, abi.encode(true), initialDeposit, platformCut); + Cellar cellarA = _createCellarWithViewFunctions( + cellarName, + USDC, + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut + ); cellarA.addPositionToCatalogue(cellarBPosition); cellarA.addPosition(0, cellarBPosition, abi.encode(true), false); @@ -1045,41 +1089,41 @@ contract CellarTest is MainnetStarterTest, AdaptorHelperFunctions { // Specify a zero length Adaptor Call array. Cellar.AdaptorCall[] memory data; - address automationActions = vm.addr(5); - registry.register(automationActions); - cellar.setAutomationActions(3, automationActions); + // address automationActions = vm.addr(5); + // registry.register(automationActions); + // cellar.setAutomationActions(3, automationActions); // Only owner and automation actions can call `callOnAdaptor`. cellar.callOnAdaptor(data); - vm.prank(automationActions); - cellar.callOnAdaptor(data); + // vm.prank(automationActions); + // cellar.callOnAdaptor(data); - // Update Automation Actions contract to zero address. - cellar.setAutomationActions(4, address(0)); + // // Update Automation Actions contract to zero address. + // cellar.setAutomationActions(4, address(0)); - // Call now reverts. - vm.startPrank(automationActions); - vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__CallerNotApprovedToRebalance.selector))); - cellar.callOnAdaptor(data); - vm.stopPrank(); + // // Call now reverts. + // vm.startPrank(automationActions); + // vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__CallerNotApprovedToRebalance.selector))); + // cellar.callOnAdaptor(data); + // vm.stopPrank(); // Owner can still call callOnAdaptor. cellar.callOnAdaptor(data); - registry.setAddress(3, automationActions); + // registry.setAddress(3, automationActions); - // Governance tries to set automation actions to registry address 3, but malicious multisig changes it after prop passes. - registry.setAddress(3, address(this)); + // // Governance tries to set automation actions to registry address 3, but malicious multisig changes it after prop passes. + // registry.setAddress(3, address(this)); - vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__ExpectedAddressDoesNotMatchActual.selector))); - cellar.setAutomationActions(3, automationActions); + // vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__ExpectedAddressDoesNotMatchActual.selector))); + // cellar.setAutomationActions(3, automationActions); - // Try setting automation actions to registry id 0. - vm.expectRevert( - bytes(abi.encodeWithSelector(Cellar.Cellar__SettingValueToRegistryIdZeroIsProhibited.selector)) - ); - cellar.setAutomationActions(0, automationActions); + // // Try setting automation actions to registry id 0. + // vm.expectRevert( + // bytes(abi.encodeWithSelector(Cellar.Cellar__SettingValueToRegistryIdZeroIsProhibited.selector)) + // ); + // cellar.setAutomationActions(0, automationActions); } // ======================================== DEPEGGING ASSET TESTS ======================================== diff --git a/test/CellarWithMultiAssetDeposit.t.sol b/test/CellarWithMultiAssetDeposit.t.sol new file mode 100644 index 000000000..77a1b5025 --- /dev/null +++ b/test/CellarWithMultiAssetDeposit.t.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { ReentrancyERC4626 } from "src/mocks/ReentrancyERC4626.sol"; +import { CellarAdaptor } from "src/modules/adaptors/Sommelier/CellarAdaptor.sol"; +import { ERC20DebtAdaptor } from "src/mocks/ERC20DebtAdaptor.sol"; +import { MockDataFeed } from "src/mocks/MockDataFeed.sol"; +import { CellarWithMultiAssetDeposit } from "src/base/permutations/CellarWithMultiAssetDeposit.sol"; +import { ERC20DebtAdaptor } from "src/mocks/ERC20DebtAdaptor.sol"; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +// Import Everything from Starter file. +import "test/resources/MainnetStarter.t.sol"; + +import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; + +contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFunctions { + using SafeTransferLib for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + using Address for address; + + CellarWithMultiAssetDeposit private cellar; + + MockDataFeed private mockUsdcUsd; + MockDataFeed private mockWethUsd; + MockDataFeed private mockWbtcUsd; + MockDataFeed private mockUsdtUsd; + + uint32 private usdcPosition = 1; + uint32 private wethPosition = 2; + uint32 private wbtcPosition = 3; + uint32 private usdtPosition = 7; + + uint256 private initialAssets; + uint256 private initialShares; + + function setUp() external { + // Setup forked environment. + string memory rpcKey = "MAINNET_RPC_URL"; + uint256 blockNumber = 16869780; + _startFork(rpcKey, blockNumber); + // Run Starter setUp code. + _setUp(); + + mockUsdcUsd = new MockDataFeed(USDC_USD_FEED); + mockWethUsd = new MockDataFeed(WETH_USD_FEED); + mockWbtcUsd = new MockDataFeed(WBTC_USD_FEED); + mockUsdtUsd = new MockDataFeed(USDT_USD_FEED); + + // Setup pricing + PriceRouter.ChainlinkDerivativeStorage memory stor; + PriceRouter.AssetSettings memory settings; + uint256 price = uint256(mockUsdcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockUsdcUsd)); + priceRouter.addAsset(USDC, settings, abi.encode(stor), price); + price = uint256(mockWethUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWethUsd)); + priceRouter.addAsset(WETH, settings, abi.encode(stor), price); + price = uint256(mockWbtcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWbtcUsd)); + priceRouter.addAsset(WBTC, settings, abi.encode(stor), price); + price = uint256(mockUsdtUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockUsdtUsd)); + priceRouter.addAsset(USDT, settings, abi.encode(stor), price); + + // Add adaptors and ERC20 positions to the registry. + registry.trustPosition(usdcPosition, address(erc20Adaptor), abi.encode(USDC)); + registry.trustPosition(wethPosition, address(erc20Adaptor), abi.encode(WETH)); + registry.trustPosition(wbtcPosition, address(erc20Adaptor), abi.encode(WBTC)); + registry.trustPosition(usdtPosition, address(erc20Adaptor), abi.encode(USDT)); + + string memory cellarName = "Cellar V0.0"; + uint256 initialDeposit = 1e6; + deal(address(USDC), address(this), initialDeposit); + USDC.approve(0xa0Cb889707d426A7A386870A03bc70d1b0697598, initialDeposit); + uint64 platformCut = 0.75e18; + cellar = new CellarWithMultiAssetDeposit( + address(this), + registry, + USDC, + cellarName, + "POG", + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut, + type(uint192).max + ); + + // Set up remaining cellar positions. + cellar.addPositionToCatalogue(wethPosition); + cellar.addPosition(0, wethPosition, abi.encode(true), false); + cellar.addPositionToCatalogue(wbtcPosition); + cellar.addPositionToCatalogue(usdtPosition); + cellar.addPosition(0, usdtPosition, abi.encode(true), false); + + cellar.setStrategistPayoutAddress(strategist); + vm.label(address(cellar), "cellar"); + vm.label(strategist, "strategist"); + // Approve cellar to spend all assets. + USDC.approve(address(cellar), type(uint256).max); + initialAssets = cellar.totalAssets(); + initialShares = cellar.totalSupply(); + } + + // ========================================= HAPPY PATH TEST ========================================= + + // Can we accept the Curve LP tokens? I dont see why not. + + function testDeposit(uint256 assets) external { + assets = bound(assets, 1e6, 1_000_000_000e6); + + deal(address(USDC), address(this), assets); + USDC.safeApprove(address(cellar), assets); + + cellar.deposit(assets, address(this)); + + assertEq( + cellar.totalAssets(), + initialAssets + assets, + "Cellar totalAssets should equal initial + new deposit." + ); + assertEq( + cellar.totalSupply(), + initialAssets + assets, + "Cellar totalSupply should equal initial + new deposit." + ); // Because share price is 1:1. + } + + function testDepositWithAlternativeAsset(uint256 assets) external { + assets = bound(assets, 1e6, 1_000_000_000e6); + + // Setup Cellar to accept USDT deposits. + cellar.setAlternativeAssetData(USDT, usdtPosition, 0); + + deal(address(USDT), address(this), assets); + USDT.safeApprove(address(cellar), assets); + + cellar.multiAssetDeposit(USDT, assets, address(this)); + + // Since share price is 1:1, below checks should pass. + assertEq(cellar.previewRedeem(1e6), 1e6, "Cellar share price should be 1."); + } + + function testDepositWithAlternativeAssetSameAsBase(uint256 assets) external { + assets = bound(assets, 1e6, 1_000_000_000e6); + + // Setup Cellar to accept USDC deposits. + cellar.setAlternativeAssetData(USDC, usdcPosition, 0); + + deal(address(USDC), address(this), assets); + USDC.safeApprove(address(cellar), assets); + + cellar.multiAssetDeposit(USDC, assets, address(this)); + + // Since share price is 1:1, below checks should pass. + assertEq( + cellar.totalAssets(), + initialAssets + assets, + "Cellar totalAssets should equal initial + new deposit." + ); + assertEq( + cellar.totalSupply(), + initialAssets + assets, + "Cellar totalSupply should equal initial + new deposit." + ); + } + + function testAlternativeAssetFeeLogic(uint256 assets, uint32 fee) external { + assets = bound(assets, 1e6, 1_000_000_000e6); + fee = uint32(bound(fee, 0, 0.1e8)); + + address user = vm.addr(777); + deal(address(USDT), user, assets); + + // Setup Cellar to accept USDT deposits. + cellar.setAlternativeAssetData(USDT, usdtPosition, fee); + + uint256 expectedShares = cellar.previewMultiAssetDeposit(USDT, assets); + + vm.startPrank(user); + USDT.safeApprove(address(cellar), assets); + cellar.multiAssetDeposit(USDT, assets, user); + vm.stopPrank(); + + // Check preview logic. + uint256 userShareBalance = cellar.balanceOf(user); + assertApproxEqAbs(userShareBalance, expectedShares, 1, "User shares should equal expected."); + + uint256 assetsIn = priceRouter.getValue(USDT, assets, USDC); + uint256 assetsInWithFee = assetsIn.mulDivDown(1e8 - fee, 1e8); + uint256 expectedSharePrice = (initialAssets + assetsIn).mulDivDown(1e6, cellar.totalSupply()); + + assertApproxEqAbs( + cellar.previewRedeem(1e6), + expectedSharePrice, + 1, + "Cellar share price should be equal expected." + ); + + assertLe( + cellar.previewRedeem(userShareBalance), + assetsInWithFee, + "User preview redeem should under estimate or equal." + ); + + assertApproxEqRel( + cellar.previewRedeem(userShareBalance), + assetsInWithFee, + 0.000002e18, + "User preview redeem should equal assets in with fee." + ); + } + + function testDroppingAnAlternativeAsset() external { + uint256 assets = 100e6; + + cellar.setAlternativeAssetData(USDT, usdtPosition, 0); + + deal(address(USDT), address(this), assets); + USDT.safeApprove(address(cellar), assets); + + cellar.multiAssetDeposit(USDT, assets, address(this)); + + // But if USDT is dropped, deposits revert. + cellar.dropAlternativeAssetData(USDT); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CellarWithMultiAssetDeposit.CellarWithMultiAssetDeposit__AlternativeAssetNotSupported.selector + ) + ) + ); + cellar.multiAssetDeposit(USDT, assets, address(this)); + + (bool isSupported, uint32 holdingPosition, uint32 fee) = cellar.alternativeAssetData(USDT); + assertEq(isSupported, false, "USDT should not be supported."); + assertEq(holdingPosition, 0, "Holding position should be zero."); + assertEq(fee, 0, "Fee should be zero."); + } + + function testSettingAlternativeAssetDataAgain() external { + cellar.setAlternativeAssetData(USDT, usdtPosition, 0); + + // Owner decides they actually want to add a fee. + cellar.setAlternativeAssetData(USDT, usdtPosition, 0.0010e8); + + (bool isSupported, uint32 holdingPosition, uint32 fee) = cellar.alternativeAssetData(USDT); + assertEq(isSupported, true, "USDT should be supported."); + assertEq(holdingPosition, usdtPosition, "Holding position should be usdt position."); + assertEq(fee, 0.0010e8, "Fee should be 10 bps."); + } + + // ======================== Test Reverts ========================== + function testDepositReverts() external { + uint256 assets = 100e6; + + deal(address(USDT), address(this), assets); + USDT.safeApprove(address(cellar), assets); + + // Try depositing with an asset that is not setup. + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CellarWithMultiAssetDeposit.CellarWithMultiAssetDeposit__AlternativeAssetNotSupported.selector + ) + ) + ); + cellar.multiAssetDeposit(USDT, assets, address(this)); + } + + function testOwnerReverts() external { + // Owner tries to setup cellar to accept alternative deposits but messes up the inputs. + + // Tries setting up using a holding position not used by the cellar. + vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__PositionNotUsed.selector, wbtcPosition))); + cellar.setAlternativeAssetData(WBTC, wbtcPosition, 0); + + // setting up but with a mismatched underlying and position. + vm.expectRevert(bytes(abi.encodeWithSelector(Cellar.Cellar__AssetMismatch.selector, USDC, USDT))); + cellar.setAlternativeAssetData(USDC, usdtPosition, 0); + + // Setting up a debt holding position. + uint32 debtWethPosition = 8; + ERC20DebtAdaptor debtAdaptor = new ERC20DebtAdaptor(); + registry.trustAdaptor(address(debtAdaptor)); + registry.trustPosition(debtWethPosition, address(debtAdaptor), abi.encode(WETH)); + cellar.addPositionToCatalogue(debtWethPosition); + cellar.addPosition(0, debtWethPosition, abi.encode(0), true); + + vm.expectRevert( + bytes(abi.encodeWithSelector(Cellar.Cellar__InvalidHoldingPosition.selector, debtWethPosition)) + ); + cellar.setAlternativeAssetData(WETH, debtWethPosition, 0); + + // Tries setting fee to be too large. + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CellarWithMultiAssetDeposit.CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge.selector + ) + ) + ); + cellar.setAlternativeAssetData(USDT, usdtPosition, 0.10000001e8); + } +} diff --git a/test/MultiChainShare.t.sol b/test/MultiChainShare.t.sol new file mode 100644 index 000000000..54a7ca7cb --- /dev/null +++ b/test/MultiChainShare.t.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +// Import Everything from Starter file. +import "test/resources/MainnetStarter.t.sol"; + +import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; +import { SourceLockerFactory } from "src/modules/multi-chain-share/SourceLockerFactory.sol"; +import { DestinationMinterFactory } from "src/modules/multi-chain-share/DestinationMinterFactory.sol"; +import { SourceLocker } from "src/modules/multi-chain-share/SourceLocker.sol"; +import { DestinationMinter } from "src/modules/multi-chain-share/DestinationMinter.sol"; +import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol"; + +import { MockCCIPRouter } from "src/mocks/MockCCIPRouter.sol"; +import { ERC4626 } from "@solmate/mixins/ERC4626.sol"; +import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol"; + +contract MultiChainShareTest is MainnetStarterTest, AdaptorHelperFunctions { + SourceLockerFactory public sourceLockerFactory; + DestinationMinterFactory public destinationMinterFactory; + + MockCCIPRouter public router; + + // Use Real Yield USD. + ERC4626 public cellar = ERC4626(0x97e6E0a40a3D02F12d1cEC30ebfbAE04e37C119E); + + function setUp() external { + // Setup forked environment. + string memory rpcKey = "MAINNET_RPC_URL"; + uint256 blockNumber = 16869780; + _startFork(rpcKey, blockNumber); + + // Run Starter setUp code. + _setUp(); + router = new MockCCIPRouter(address(LINK)); + + sourceLockerFactory = new SourceLockerFactory( + address(this), + address(router), + router.SOURCE_SELECTOR(), + router.DESTINATION_SELECTOR(), + address(LINK), + 2_000_000, + 200_000 + ); + + destinationMinterFactory = new DestinationMinterFactory( + address(this), + address(router), + address(sourceLockerFactory), + router.SOURCE_SELECTOR(), + router.DESTINATION_SELECTOR(), + address(LINK), + 200_000, + 200_000 + ); + + sourceLockerFactory.setDestinationMinterFactory(address(destinationMinterFactory)); + + deal(address(LINK), address(sourceLockerFactory), 1_000e18); + deal(address(LINK), address(destinationMinterFactory), 1_000e18); + deal(address(LINK), address(this), 1_000e18); + } + + function testAdminWithdraw() external { + // Both factories have an admin withdraw function to withdraw LINK from them. + uint256 expectedLinkBalance = LINK.balanceOf(address(this)); + uint256 linkBalance = LINK.balanceOf(address(sourceLockerFactory)); + expectedLinkBalance += linkBalance; + sourceLockerFactory.adminWithdraw(LINK, linkBalance, address(this)); + + linkBalance = LINK.balanceOf(address(destinationMinterFactory)); + expectedLinkBalance += linkBalance; + destinationMinterFactory.adminWithdraw(LINK, linkBalance, address(this)); + + assertEq(LINK.balanceOf(address(this)), expectedLinkBalance, "Balance does not equal expected."); + + // Try calling it from a non owner address. + address nonOwner = vm.addr(1); + vm.startPrank(nonOwner); + vm.expectRevert(bytes("UNAUTHORIZED")); + sourceLockerFactory.adminWithdraw(LINK, linkBalance, address(this)); + + vm.expectRevert(bytes("UNAUTHORIZED")); + destinationMinterFactory.adminWithdraw(LINK, linkBalance, address(this)); + vm.stopPrank(); + } + + function testHappyPath(uint256 amountToDestination, uint256 amountToSource) external { + amountToDestination = bound(amountToDestination, 1e6, type(uint96).max); + amountToSource = bound(amountToSource, 0.999e6, amountToDestination); + + (SourceLocker locker, DestinationMinter minter) = _runDeploy(); + + // Try bridging shares. + deal(address(cellar), address(this), amountToDestination); + cellar.approve(address(locker), amountToDestination); + uint256 fee = locker.previewFee(amountToDestination, address(this)); + LINK.approve(address(locker), fee); + locker.bridgeToDestination(amountToDestination, address(this), fee); + + Client.Any2EVMMessage memory message = router.getLastMessage(); + + vm.prank(address(router)); + minter.ccipReceive(message); + + assertEq(amountToDestination, minter.balanceOf(address(this)), "Should have minted shares."); + assertEq(0, cellar.balanceOf(address(this)), "Should have spent Cellar shares."); + assertEq(cellar.balanceOf(address(locker)), amountToDestination, "Should have sent shares to the locker."); + + // Try bridging the shares back. + fee = minter.previewFee(amountToSource, address(this)); + LINK.approve(address(minter), 1e18); + minter.bridgeToSource(amountToSource, address(this), 1e18); + + message = router.getLastMessage(); + + vm.prank(address(router)); + locker.ccipReceive(message); + + assertEq(amountToDestination - amountToSource, minter.balanceOf(address(this)), "Should have burned shares."); + assertEq( + amountToSource, + cellar.balanceOf(address(this)), + "Should have sent Cellar shares back to this address." + ); + assertEq( + amountToDestination - amountToSource, + cellar.balanceOf(address(locker)), + "Locker should have reduced shares." + ); + } + + //---------------------------------------- REVERT TESTS ---------------------------------------- + + function testCcipReceiveChecks() external { + // Deploy a source and minter. + (SourceLocker locker, DestinationMinter minter) = _runDeploy(); + // Try calling ccipReceive function on all contracts using attacker contract. + address attacker = vm.addr(0xBAD); + uint64 badSourceChain = 1; + + Client.Any2EVMMessage memory badMessage; + badMessage.sender = abi.encode(attacker); + badMessage.sourceChainSelector = badSourceChain; + + vm.startPrank(attacker); + // Revert if caller is not CCIP Router. + vm.expectRevert(bytes(abi.encodeWithSelector(CCIPReceiver.InvalidRouter.selector, attacker))); + sourceLockerFactory.ccipReceive(badMessage); + + vm.expectRevert(bytes(abi.encodeWithSelector(CCIPReceiver.InvalidRouter.selector, attacker))); + destinationMinterFactory.ccipReceive(badMessage); + + vm.expectRevert(bytes(abi.encodeWithSelector(CCIPReceiver.InvalidRouter.selector, attacker))); + locker.ccipReceive(badMessage); + + vm.expectRevert(bytes(abi.encodeWithSelector(CCIPReceiver.InvalidRouter.selector, attacker))); + minter.ccipReceive(badMessage); + vm.stopPrank(); + + // Revert if source chain selector is wrong. + vm.startPrank(address(router)); + vm.expectRevert( + bytes( + abi.encodeWithSelector( + SourceLockerFactory.SourceLockerFactory___SourceChainNotAllowlisted.selector, + badSourceChain + ) + ) + ); + sourceLockerFactory.ccipReceive(badMessage); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + DestinationMinterFactory.DestinationMinterFactory___SourceChainNotAllowlisted.selector, + badSourceChain + ) + ) + ); + destinationMinterFactory.ccipReceive(badMessage); + + vm.expectRevert( + bytes( + abi.encodeWithSelector(SourceLocker.SourceLocker___SourceChainNotAllowlisted.selector, badSourceChain) + ) + ); + locker.ccipReceive(badMessage); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + DestinationMinter.DestinationMinter___SourceChainNotAllowlisted.selector, + badSourceChain + ) + ) + ); + minter.ccipReceive(badMessage); + + // Revert if message sender is wrong. + badMessage.sourceChainSelector = locker.destinationChainSelector(); + vm.expectRevert( + bytes( + abi.encodeWithSelector( + SourceLockerFactory.SourceLockerFactory___SenderNotAllowlisted.selector, + attacker + ) + ) + ); + sourceLockerFactory.ccipReceive(badMessage); + + vm.expectRevert( + bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___SenderNotAllowlisted.selector, attacker)) + ); + locker.ccipReceive(badMessage); + + badMessage.sourceChainSelector = locker.sourceChainSelector(); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + DestinationMinterFactory.DestinationMinterFactory___SenderNotAllowlisted.selector, + attacker + ) + ) + ); + destinationMinterFactory.ccipReceive(badMessage); + + vm.expectRevert( + bytes(abi.encodeWithSelector(DestinationMinter.DestinationMinter___SenderNotAllowlisted.selector, attacker)) + ); + minter.ccipReceive(badMessage); + + vm.stopPrank(); + } + + function testSourceLockerFactoryReverts() external { + // Trying to set factory again reverts. + vm.expectRevert( + bytes(abi.encodeWithSelector(SourceLockerFactory.SourceLockerFactory___FactoryAlreadySet.selector)) + ); + sourceLockerFactory.setDestinationMinterFactory(address(0)); + + // Calling deploy when sourceLockerFactory does not have enough Link. + deal(address(LINK), address(sourceLockerFactory), 0); + vm.expectRevert( + bytes(abi.encodeWithSelector(SourceLockerFactory.SourceLockerFactory___NotEnoughLink.selector)) + ); + sourceLockerFactory.deploy(cellar); + } + + function testDestinationMinterFactoryRetryCallback() external { + SourceLocker locker; + DestinationMinter minter; + + (, address lockerAddress) = sourceLockerFactory.deploy(cellar); + + locker = SourceLocker(lockerAddress); + + // Zero out destination minter facotry Link balance so CCIP call reverts. + deal(address(LINK), address(destinationMinterFactory), 0); + + // Simulate CCIP Message to destination factory. + Client.Any2EVMMessage memory message = router.getLastMessage(); + vm.prank(address(router)); + destinationMinterFactory.ccipReceive(message); + + address expectedMinterAddress = 0x5B0091f49210e7B2A57B03dfE1AB9D08289d9294; + + bytes32 messageDataHash = keccak256(abi.encode(address(locker), expectedMinterAddress)); + assertTrue( + destinationMinterFactory.canRetryFailedMessage(messageDataHash), + "Should have updated mapping to true." + ); + + // Try retrying callback without sending link to factory. + vm.expectRevert( + bytes(abi.encodeWithSelector(DestinationMinterFactory.DestinationMinterFactory___NotEnoughLink.selector)) + ); + destinationMinterFactory.retryCallback(address(locker), expectedMinterAddress); + + // Callback failed, send destination minter link, and retry. + deal(address(LINK), address(destinationMinterFactory), 1e18); + + destinationMinterFactory.retryCallback(address(locker), expectedMinterAddress); + + // Calling retryCallback again should revert. + vm.expectRevert( + bytes( + abi.encodeWithSelector(DestinationMinterFactory.DestinationMinterFactory___CanNotRetryCallback.selector) + ) + ); + destinationMinterFactory.retryCallback(address(locker), expectedMinterAddress); + + assertTrue( + !destinationMinterFactory.canRetryFailedMessage(messageDataHash), + "Should have updated mapping to false." + ); + + // Calll can continue as normal. + // Simulate CCIP message to source factory. + message = router.getLastMessage(); + vm.prank(address(router)); + sourceLockerFactory.ccipReceive(message); + + minter = DestinationMinter(locker.targetDestination()); + } + + function testSourceLockerReverts() external { + (SourceLocker locker, ) = _runDeploy(); + + // Only callable by source locker factory. + vm.expectRevert(bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___OnlyFactory.selector))); + locker.setTargetDestination(address(this)); + + // Can only be set once. + vm.startPrank(address(sourceLockerFactory)); + vm.expectRevert( + bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___TargetDestinationAlreadySet.selector)) + ); + locker.setTargetDestination(address(this)); + vm.stopPrank(); + + uint256 amountToDesintation = 1e18; + deal(address(cellar), address(this), amountToDesintation); + cellar.approve(address(locker), amountToDesintation); + uint256 fee = locker.previewFee(amountToDesintation, address(this)); + LINK.approve(address(locker), fee); + + vm.expectRevert(bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___InvalidTo.selector))); + locker.bridgeToDestination(amountToDesintation, address(0), fee); + + vm.expectRevert(bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___FeeTooHigh.selector))); + locker.bridgeToDestination(amountToDesintation, address(this), 0); + + (, address lockerAddress) = sourceLockerFactory.deploy(cellar); + + locker = SourceLocker(lockerAddress); + + cellar.approve(address(lockerAddress), amountToDesintation); + + vm.expectRevert(bytes(abi.encodeWithSelector(SourceLocker.SourceLocker___TargetDestinationNotSet.selector))); + locker.bridgeToDestination(amountToDesintation, address(this), fee); + } + + function testDestinationMinterReverts() external { + (, DestinationMinter minter) = _runDeploy(); + + uint256 amountToSource = 10e18; + deal(address(minter), address(this), 10e18); + uint256 fee = minter.previewFee(amountToSource, address(this)); + LINK.approve(address(minter), 1e18); + + vm.expectRevert(bytes(abi.encodeWithSelector(DestinationMinter.DestinationMinter___InvalidTo.selector))); + minter.bridgeToSource(amountToSource, address(0), fee); + + vm.expectRevert(bytes(abi.encodeWithSelector(DestinationMinter.DestinationMinter___FeeTooHigh.selector))); + minter.bridgeToSource(amountToSource, address(this), 0); + + // Try bridging more than we have. + vm.expectRevert(stdError.arithmeticError); + minter.bridgeToSource(amountToSource + 1, address(this), fee); + } + + function _runDeploy() internal returns (SourceLocker locker, DestinationMinter minter) { + (, address lockerAddress) = sourceLockerFactory.deploy(cellar); + + locker = SourceLocker(lockerAddress); + + // Simulate CCIP Message to destination factory. + Client.Any2EVMMessage memory message = router.getLastMessage(); + vm.prank(address(router)); + destinationMinterFactory.ccipReceive(message); + + // Simulate CCIP message to source factory. + message = router.getLastMessage(); + vm.prank(address(router)); + sourceLockerFactory.ccipReceive(message); + + minter = DestinationMinter(locker.targetDestination()); + } +} diff --git a/test/resources/AdaptorHelperFunctions.sol b/test/resources/AdaptorHelperFunctions.sol index dce9d77ab..1d044c468 100644 --- a/test/resources/AdaptorHelperFunctions.sol +++ b/test/resources/AdaptorHelperFunctions.sol @@ -27,6 +27,7 @@ import { BalancerPoolAdaptor } from "src/modules/adaptors/Balancer/BalancerPoolA // Compound import { CTokenAdaptor } from "src/modules/adaptors/Compound/CTokenAdaptor.sol"; import { ComptrollerG7 as Comptroller, CErc20 } from "src/interfaces/external/ICompound.sol"; +import { CompoundV2DebtAdaptor } from "src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol"; // FeesAndReserves import { FeesAndReservesAdaptor } from "src/modules/adaptors/FeesAndReserves/FeesAndReservesAdaptor.sol"; @@ -57,6 +58,12 @@ import { CollateralFTokenAdaptorV1 } from "src/modules/adaptors/Frax/CollateralF import { DebtFTokenAdaptorV1 } from "src/modules/adaptors/Frax/DebtFTokenAdaptorV1.sol"; +import { MorphoBlueDebtAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueDebtAdaptor.sol"; +import { MorphoBlueHelperLogic } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol"; +import { MorphoBlueCollateralAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueCollateralAdaptor.sol"; +import { MorphoBlueSupplyAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol"; +// import { MorphoBlueSupplyAdaptor2 } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor2.sol"; +import { Id, MarketParams, Market } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; import { ConvexCurveAdaptor } from "src/modules/adaptors/Convex/ConvexCurveAdaptor.sol"; import { CurvePool } from "src/interfaces/external/Curve/CurvePool.sol"; @@ -294,6 +301,70 @@ contract AdaptorHelperFunctions { ); } + // ========================================= Morpho Blue FUNCTIONS ========================================= + + // MorphoBlueSupplyAdaptor Functions + function _createBytesDataToLendOnMorphoBlue( + MarketParams memory _market, + uint256 _assets + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(MorphoBlueSupplyAdaptor.lendToMorphoBlue.selector, _market, _assets); + } + + function _createBytesDataToWithdrawFromMorphoBlue( + MarketParams memory _market, + uint256 _assets + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(MorphoBlueSupplyAdaptor.withdrawFromMorphoBlue.selector, _market, _assets); + } + + function _createBytesDataToAccrueInterestToMorphoBlueSupplyAdaptor( + MarketParams memory _market + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(MorphoBlueSupplyAdaptor.accrueInterest.selector, _market); + } + + // MorphoBlueCollateralAdaptor Functions + + function _createBytesDataToAddCollateralToMorphoBlue( + MarketParams memory _market, + uint256 _collateralToDeposit + ) internal pure returns (bytes memory) { + return + abi.encodeWithSelector(MorphoBlueCollateralAdaptor.addCollateral.selector, _market, _collateralToDeposit); + } + + function _createBytesDataToRemoveCollateralToMorphoBlue( + MarketParams memory _market, + uint256 _collateralAmount + ) internal pure returns (bytes memory) { + return + abi.encodeWithSelector(MorphoBlueCollateralAdaptor.removeCollateral.selector, _market, _collateralAmount); + } + + function _createBytesDataToAccrueInterestToMorphoBlue( + MarketParams memory _market + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(MorphoBlueCollateralAdaptor.accrueInterest.selector, _market); + } + + // MorphoBlueDebtAdaptor Functions + + function _createBytesDataToBorrowFromMorphoBlue( + MarketParams memory _market, + uint256 _amountToBorrow + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(MorphoBlueDebtAdaptor.borrowFromMorphoBlue.selector, _market, _amountToBorrow); + } + + function _createBytesDataToRepayDebtToMorphoBlue( + MarketParams memory _market, + uint256 _debtTokenRepayAmount + ) internal pure returns (bytes memory) { + return + abi.encodeWithSelector(MorphoBlueDebtAdaptor.repayMorphoBlueDebt.selector, _market, _debtTokenRepayAmount); + } + // ========================================= Balancer FUNCTIONS ========================================= /** @@ -375,6 +446,28 @@ contract AdaptorHelperFunctions { return abi.encodeWithSelector(CTokenAdaptor.withdrawFromCompound.selector, market, amountToWithdraw); } + function _createBytesDataToEnterMarketWithCompoundV2(CErc20 market) internal pure returns (bytes memory) { + return abi.encodeWithSelector(CTokenAdaptor.enterMarket.selector, market); + } + + function _createBytesDataToExitMarketWithCompoundV2(CErc20 market) internal pure returns (bytes memory) { + return abi.encodeWithSelector(CTokenAdaptor.exitMarket.selector, market); + } + + function _createBytesDataToBorrowWithCompoundV2( + CErc20 market, + uint256 amountToBorrow + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(CompoundV2DebtAdaptor.borrowFromCompoundV2.selector, market, amountToBorrow); + } + + function _createBytesDataToRepayWithCompoundV2( + CErc20 market, + uint256 debtTokenRepayAmount + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(CompoundV2DebtAdaptor.repayCompoundV2Debt.selector, market, debtTokenRepayAmount); + } + // ========================================= Fees And Reserves FUNCTIONS ========================================= // Make sure that if a strategists makes a huge deposit before calling log fees, it doesn't affect fee pay out diff --git a/test/resources/MainnetAddresses.sol b/test/resources/MainnetAddresses.sol index 1e906eeb3..85196b14c 100644 --- a/test/resources/MainnetAddresses.sol +++ b/test/resources/MainnetAddresses.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.21; import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { CErc20 } from "src/interfaces/external/ICompound.sol"; +import { ComptrollerG7 as Comptroller, CErc20 } from "src/interfaces/external/ICompound.sol"; contract MainnetAddresses { // Sommelier @@ -152,6 +152,7 @@ contract MainnetAddresses { CErc20 public cDAI = CErc20(0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643); CErc20 public cUSDC = CErc20(0x39AA39c021dfbaE8faC545936693aC917d5E7563); CErc20 public cTUSD = CErc20(0x12392F67bdf24faE0AF363c24aC620a2f67DAd86); + CErc20 public cWBTC = CErc20(0xccF4429DB6322D5C611ee964527D42E5d685DD6a); // Chainlink Automation Registry address public automationRegistry = 0x02777053d6764996e594c3E88AF1D58D5363a2e6; diff --git a/test/resources/MainnetStarter.t.sol b/test/resources/MainnetStarter.t.sol index 38f5a32d1..ce15493ac 100644 --- a/test/resources/MainnetStarter.t.sol +++ b/test/resources/MainnetStarter.t.sol @@ -21,6 +21,8 @@ import { BaseAdaptor } from "src/modules/adaptors/BaseAdaptor.sol"; import { ERC20Adaptor } from "src/modules/adaptors/ERC20Adaptor.sol"; import { SwapWithUniswapAdaptor } from "src/modules/adaptors/Uniswap/SwapWithUniswapAdaptor.sol"; +import { CellarWithViewFunctions } from "src/mocks/CellarWithViewFunctions.sol"; + // Import Testing Resources import { Test, stdStorage, StdStorage, stdError, console } from "@forge-std/Test.sol"; @@ -119,4 +121,36 @@ contract MainnetStarterTest is Test, MainnetAddresses { return Cellar(deployer.deployContract(cellarName, creationCode, constructorArgs, 0)); } + + function _createCellarWithViewFunctions( + string memory cellarName, + ERC20 holdingAsset, + uint32 holdingPosition, + bytes memory holdingPositionConfig, + uint256 initialDeposit, + uint64 platformCut + ) internal returns (CellarWithViewFunctions) { + // Approve new cellar to spend assets. + address cellarAddress = deployer.getAddress(cellarName); + deal(address(holdingAsset), address(this), initialDeposit); + holdingAsset.approve(cellarAddress, initialDeposit); + + bytes memory creationCode; + bytes memory constructorArgs; + creationCode = type(CellarWithViewFunctions).creationCode; + constructorArgs = abi.encode( + address(this), + registry, + holdingAsset, + cellarName, + cellarName, + holdingPosition, + holdingPositionConfig, + initialDeposit, + platformCut, + type(uint192).max + ); + + return CellarWithViewFunctions(deployer.deployContract(cellarName, creationCode, constructorArgs, 0)); + } } diff --git a/test/testAdaptors/AaveV3.t.sol b/test/testAdaptors/AaveV3.t.sol index ae70f575b..0a465e6f2 100644 --- a/test/testAdaptors/AaveV3.t.sol +++ b/test/testAdaptors/AaveV3.t.sol @@ -339,22 +339,22 @@ contract CellarAaveV3Test is MainnetStarterTest, AdaptorHelperFunctions { "Cellar should have dV3USDC worth of assets/2." ); - (ERC20[] memory tokens, uint256[] memory balances, bool[] memory isDebt) = cellar.viewPositionBalances(); - assertEq(tokens.length, 3, "Should have length of 3."); - assertEq(balances.length, 3, "Should have length of 3."); - assertEq(isDebt.length, 3, "Should have length of 3."); - - assertEq(address(tokens[0]), address(USDC), "Should be USDC."); - assertEq(address(tokens[1]), address(USDC), "Should be USDC."); - assertEq(address(tokens[2]), address(USDC), "Should be USDC."); - - assertApproxEqAbs(balances[0], assets + initialAssets, 1, "Should equal assets."); - assertEq(balances[1], assets / 2, "Should equal assets/2."); - assertEq(balances[2], assets / 2, "Should equal assets/2."); - - assertEq(isDebt[0], false, "Should not be debt."); - assertEq(isDebt[1], false, "Should not be debt."); - assertEq(isDebt[2], true, "Should be debt."); + // (ERC20[] memory tokens, uint256[] memory balances, bool[] memory isDebt) = cellar.viewPositionBalances(); + // assertEq(tokens.length, 3, "Should have length of 3."); + // assertEq(balances.length, 3, "Should have length of 3."); + // assertEq(isDebt.length, 3, "Should have length of 3."); + + // assertEq(address(tokens[0]), address(USDC), "Should be USDC."); + // assertEq(address(tokens[1]), address(USDC), "Should be USDC."); + // assertEq(address(tokens[2]), address(USDC), "Should be USDC."); + + // assertApproxEqAbs(balances[0], assets + initialAssets, 1, "Should equal assets."); + // assertEq(balances[1], assets / 2, "Should equal assets/2."); + // assertEq(balances[2], assets / 2, "Should equal assets/2."); + + // assertEq(isDebt[0], false, "Should not be debt."); + // assertEq(isDebt[1], false, "Should not be debt."); + // assertEq(isDebt[2], true, "Should be debt."); } function testTakingOutLoansInUntrackedPosition() external { diff --git a/test/testAdaptors/Compound.t.sol b/test/testAdaptors/Compound.t.sol index c2aac204c..151842b64 100644 --- a/test/testAdaptors/Compound.t.sol +++ b/test/testAdaptors/Compound.t.sol @@ -3,21 +3,24 @@ pragma solidity 0.8.21; import { CTokenAdaptor } from "src/modules/adaptors/Compound/CTokenAdaptor.sol"; import { ComptrollerG7 as Comptroller, CErc20 } from "src/interfaces/external/ICompound.sol"; - import { VestingSimple } from "src/modules/vesting/VestingSimple.sol"; import { VestingSimpleAdaptor } from "src/modules/adaptors/VestingSimpleAdaptor.sol"; - // Import Everything from Starter file. import "test/resources/MainnetStarter.t.sol"; - import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; +import { CompoundV2DebtAdaptor } from "src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol"; +import { Math } from "src/utils/Math.sol"; +/** + * @dev TODO - Extra test for supporting providing collateral && open borrow positions have been ported over to CompoundTempHFTest.t.sol for now. This was due to an error arising when trying to add a CUSDC debt position here. Once that is resolved in this test file (Compound.t.sol) we can copy over the tests from CompoundTempHFTest.t.sol if they are done. + */ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { using SafeTransferLib for ERC20; using Math for uint256; using stdStorage for StdStorage; CTokenAdaptor private cTokenAdaptor; + CompoundV2DebtAdaptor private compoundV2DebtAdaptor; VestingSimpleAdaptor private vestingAdaptor; VestingSimple private vesting; Cellar private cellar; @@ -29,6 +32,10 @@ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { uint32 private usdcPosition = 3; uint32 private cUSDCPosition = 4; uint32 private daiVestingPosition = 5; + uint32 private cDAIDebtPosition = 6; + // uint32 private cUSDCDebtPosition = 7; // TODO - commented out as mentioned at the beginning of this file due to errors arising. + + uint256 private minHealthFactor = 1.1e18; function setUp() external { // Setup forked environment. @@ -40,7 +47,9 @@ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { _setUp(); vesting = new VestingSimple(USDC, 1 days / 4, 1e6); - cTokenAdaptor = new CTokenAdaptor(address(comptroller), address(COMP)); + cTokenAdaptor = new CTokenAdaptor(address(comptroller), address(COMP), minHealthFactor); + compoundV2DebtAdaptor = new CompoundV2DebtAdaptor(false, address(comptroller), address(COMP), minHealthFactor); + vestingAdaptor = new VestingSimpleAdaptor(); PriceRouter.ChainlinkDerivativeStorage memory stor; @@ -62,6 +71,7 @@ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { // Add adaptors and positions to the registry. registry.trustAdaptor(address(cTokenAdaptor)); registry.trustAdaptor(address(vestingAdaptor)); + registry.trustAdaptor(address(compoundV2DebtAdaptor)); registry.trustPosition(daiPosition, address(erc20Adaptor), abi.encode(DAI)); registry.trustPosition(cDAIPosition, address(cTokenAdaptor), abi.encode(cDAI)); @@ -69,6 +79,10 @@ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { registry.trustPosition(cUSDCPosition, address(cTokenAdaptor), abi.encode(cUSDC)); registry.trustPosition(daiVestingPosition, address(vestingAdaptor), abi.encode(vesting)); + // trust debtAdaptor positions + registry.trustPosition(cDAIDebtPosition, address(compoundV2DebtAdaptor), abi.encode(cDAI)); + // registry.trustPosition(cUSDCDebtPosition, address(compoundV2DebtAdaptor), abi.encode(cUSDC)); + string memory cellarName = "Compound Cellar V0.0"; uint256 initialDeposit = 1e18; uint64 platformCut = 0.75e18; @@ -79,16 +93,21 @@ contract CellarCompoundTest is MainnetStarterTest, AdaptorHelperFunctions { cellar.addAdaptorToCatalogue(address(cTokenAdaptor)); cellar.addAdaptorToCatalogue(address(vestingAdaptor)); cellar.addAdaptorToCatalogue(address(swapWithUniswapAdaptor)); + cellar.addAdaptorToCatalogue(address(compoundV2DebtAdaptor)); cellar.addPositionToCatalogue(daiPosition); cellar.addPositionToCatalogue(usdcPosition); cellar.addPositionToCatalogue(cUSDCPosition); cellar.addPositionToCatalogue(daiVestingPosition); + cellar.addPositionToCatalogue(cDAIDebtPosition); + // cellar.addPositionToCatalogue(cUSDCDebtPosition); cellar.addPosition(1, daiPosition, abi.encode(0), false); - cellar.addPosition(1, usdcPosition, abi.encode(0), false); - cellar.addPosition(1, cUSDCPosition, abi.encode(0), false); - cellar.addPosition(1, daiVestingPosition, abi.encode(0), false); + cellar.addPosition(2, usdcPosition, abi.encode(0), false); + cellar.addPosition(3, cUSDCPosition, abi.encode(0), false); + cellar.addPosition(4, daiVestingPosition, abi.encode(0), false); + cellar.addPosition(5, cDAIDebtPosition, abi.encode(0), true); + // cellar.addPosition(6, cUSDCDebtPosition, abi.encode(0), true); DAI.safeApprove(address(cellar), type(uint256).max); } diff --git a/test/testAdaptors/CompoundV2AdditionalTests.t.sol b/test/testAdaptors/CompoundV2AdditionalTests.t.sol new file mode 100644 index 000000000..15d7bcfca --- /dev/null +++ b/test/testAdaptors/CompoundV2AdditionalTests.t.sol @@ -0,0 +1,1502 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { CTokenAdaptor } from "src/modules/adaptors/Compound/CTokenAdaptor.sol"; +import { ComptrollerG7 as Comptroller, CErc20, PriceOracle } from "src/interfaces/external/ICompound.sol"; +import { VestingSimple } from "src/modules/vesting/VestingSimple.sol"; +import { VestingSimpleAdaptor } from "src/modules/adaptors/VestingSimpleAdaptor.sol"; +// Import Everything from Starter file. +import "test/resources/MainnetStarter.t.sol"; +import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; +import { CompoundV2DebtAdaptor } from "src/modules/adaptors/Compound/CompoundV2DebtAdaptor.sol"; +import { Math } from "src/utils/Math.sol"; + +/** + * @dev Tests are purposely kept very single-scope in order to do better gas comparisons with gas-snapshots for typical functionalities. + * TODO - test cTokens that are using native tokens (ETH, etc.) + * NOTE - OG compoundV2 tests already account for totalAssets, deposit, withdraw w/ basic supplying and withdrawing, and claiming of comp token (see `CTokenAdaptor.sol`). So we'll have to test for each new functionality: enterMarket, exitMarket, borrowFromCompoundV2, repayCompoundV2Debt. + */ +contract CompoundV2AdditionalTests is MainnetStarterTest, AdaptorHelperFunctions { + using SafeTransferLib for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + + CTokenAdaptor private cTokenAdaptor; + CompoundV2DebtAdaptor private compoundV2DebtAdaptor; + VestingSimpleAdaptor private vestingAdaptor; + VestingSimple private vesting; + Cellar private cellar; + + Comptroller private comptroller = Comptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); + + uint32 private daiPosition = 1; + uint32 private cUSDCPosition = 2; + uint32 private usdcPosition = 3; + uint32 private cDAIPosition = 4; + uint32 private daiVestingPosition = 5; + uint32 private cDAIDebtPosition = 6; + uint32 private cUSDCDebtPosition = 7; + // TODO: add positions for ETH CTokens + + address private whaleBorrower = vm.addr(777); + + // Collateral Positions are just regular CTokenAdaptor positions but after `enterMarket()` has been called. + // Debt Positions --> these need to be setup properly. Start with a debt position on a market that is easy. + + uint256 private minHealthFactor = 1.2e18; + + function setUp() external { + // Setup forked environment. + string memory rpcKey = "MAINNET_RPC_URL"; + uint256 blockNumber = 19135027; + _startFork(rpcKey, blockNumber); + + // Run Starter setUp code. + _setUp(); + + vesting = new VestingSimple(USDC, 1 days / 4, 1e6); + cTokenAdaptor = new CTokenAdaptor(address(comptroller), address(COMP), minHealthFactor); + compoundV2DebtAdaptor = new CompoundV2DebtAdaptor(false, address(comptroller), address(COMP), minHealthFactor); + + vestingAdaptor = new VestingSimpleAdaptor(); + + PriceRouter.ChainlinkDerivativeStorage memory stor; + PriceRouter.AssetSettings memory settings; + + uint256 price = uint256(IChainlinkAggregator(USDC_USD_FEED).latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, USDC_USD_FEED); + priceRouter.addAsset(USDC, settings, abi.encode(stor), price); + + price = uint256(IChainlinkAggregator(DAI_USD_FEED).latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, DAI_USD_FEED); + priceRouter.addAsset(DAI, settings, abi.encode(stor), price); + + price = uint256(IChainlinkAggregator(COMP_USD_FEED).latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, COMP_USD_FEED); + priceRouter.addAsset(COMP, settings, abi.encode(stor), price); + + // Setup Cellar: + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(cTokenAdaptor)); + registry.trustAdaptor(address(vestingAdaptor)); + registry.trustAdaptor(address(compoundV2DebtAdaptor)); + + bool isLiquid = true; + + registry.trustPosition(daiPosition, address(erc20Adaptor), abi.encode(DAI)); + registry.trustPosition(cDAIPosition, address(cTokenAdaptor), abi.encode(cDAI)); + registry.trustPosition(usdcPosition, address(erc20Adaptor), abi.encode(USDC)); + registry.trustPosition(cUSDCPosition, address(cTokenAdaptor), abi.encode(cUSDC)); + registry.trustPosition(daiVestingPosition, address(vestingAdaptor), abi.encode(vesting)); + + // trust debtAdaptor positions + registry.trustPosition(cDAIDebtPosition, address(compoundV2DebtAdaptor), abi.encode(cDAI) ); + registry.trustPosition(cUSDCDebtPosition, address(compoundV2DebtAdaptor), abi.encode(cUSDC)); + + string memory cellarName = "Compound Cellar V0.0"; + uint256 initialDeposit = 1e18; + uint64 platformCut = 0.75e18; + + cellar = _createCellar(cellarName, DAI, cDAIPosition, abi.encode(isLiquid), initialDeposit, platformCut); + + cellar.setRebalanceDeviation(0.003e18); + cellar.addAdaptorToCatalogue(address(erc20Adaptor)); + cellar.addAdaptorToCatalogue(address(cTokenAdaptor)); + cellar.addAdaptorToCatalogue(address(vestingAdaptor)); + cellar.addAdaptorToCatalogue(address(swapWithUniswapAdaptor)); + cellar.addAdaptorToCatalogue(address(compoundV2DebtAdaptor)); + + cellar.addPositionToCatalogue(daiPosition); + cellar.addPositionToCatalogue(usdcPosition); + cellar.addPositionToCatalogue(cUSDCPosition); + cellar.addPositionToCatalogue(daiVestingPosition); + cellar.addPositionToCatalogue(cDAIDebtPosition); + cellar.addPositionToCatalogue(cUSDCDebtPosition); + + cellar.addPosition(1, daiPosition, abi.encode(0), false); + cellar.addPosition(2, usdcPosition, abi.encode(0), false); + cellar.addPosition(3, cUSDCPosition, abi.encode(0), false); + cellar.addPosition(4, daiVestingPosition, abi.encode(0), false); + cellar.addPosition(0, cDAIDebtPosition, abi.encode(0), true); + cellar.addPosition(1, cUSDCDebtPosition, abi.encode(0), true); + + DAI.safeApprove(address(cellar), type(uint256).max); + } + + //============================================ CTokenAdaptor Extra Tests =========================================== + + // Supply && EnterMarket + function testEnterMarket(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + // uint256 assets = 100e6; + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + bool inCTokenMarket = _checkInMarket(cDAI); + assertEq(inCTokenMarket, true, "Should be 'IN' the market"); + } + + // Ensure that default cTokenAdaptor supply position is not "in" the market + function testDefaultCheckInMarket(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + // uint256 assets = 100e6; + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // check that we aren't in market + bool inCTokenMarket = _checkInMarket(cDAI); + assertEq(inCTokenMarket, false, "Should not be 'IN' the market yet"); + } + + // Same as testTotalAssets in OG CompoundV2 tests but the supplied position is marked as `entered` in the market --> so it checks totalAssets with a position that has: lending, marking that as entered in the market, withdrawing, swaps, and lending more. + function testTotalAssetsWithJustEnterMarket(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + assets = bound(assets, 0.1e18, 1_000_000e18); + + _setupSimpleLendAndEnter(assets, initialAssets); + _totalAssetsCheck(assets, initialAssets); + } + + // checks that it reverts if the position is marked as `entered` - aka is collateral + // NOTE - without the `_checkMarketsEntered` withdrawals are possible with CompoundV2 markets even if the the position is marked as `entered` in the market, until it hits a shortfall scenario (more borrow than collateral * market collateral factor) --> see "Compound Revert Tests" at bottom of this test file. + function testWithdrawEnteredMarketPosition(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + deal(address(DAI), address(this), 0); + uint256 amountToWithdraw = 1; + vm.expectRevert( + bytes(abi.encodeWithSelector(CTokenAdaptor.CTokenAdaptor__AlreadyInMarket.selector, address(cDAI))) + ); + cellar.withdraw(amountToWithdraw, address(this), address(this)); + } + + // strategist function `withdrawFromCompound` tests but with and without exiting the market. Purposely allowed withdrawals even while 'IN' market for this strategist function. + // test withdrawing without calling `exitMarket` + // test withdrawing with calling `exitMarket` first to make sure it all works still either way + function testWithdrawFromCompound(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + + // enter market + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cUSDC); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // test strategist calling withdrawing without calling `exitMarket` - should work + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // test withdrawing with calling `exitMarket` first to make sure it all works still either way + // exit market + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cUSDC); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // withdraw from compoundV2 + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + DAI.balanceOf(address(cellar)), + assets, + 1e9, + "Check 1: All assets should have been withdrawn besides initialAssets." + ); + } + + function testWithdrawFromCompoundWithTypeUINT256Max(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + + // enter market + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // test withdrawing without calling `exitMarket` - should work + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + assertApproxEqAbs( + DAI.balanceOf(address(cellar)), + assets + initialAssets, + 1e9, + "Check 1: All assets should have been withdrawn." + ); + + // deposit again + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // test withdrawing with calling `exitMarket` first to make sure it all works still either way + // exit market + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // withdraw from compoundV2 + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + DAI.balanceOf(address(cellar)), + (2 * assets) + initialAssets, + 1e18, + "Check 2: All assets should have been withdrawn." + ); + } + + // strategist tries withdrawing more than is allowed based on adaptor specified health factor. + // NOTE: this test passes when the minHealthFactor is set at a value that is higher than the inverse of the CR for CDAI market. When it is lower than it, it does not trigger because it fails due to compound v2 internal "liquidity" checks for a respective user's set of positions (basically whether their resultant balance: CR*collateral - borrow > 0 or not). + // I've double checked the logic within the comptroller && the adaptor HF. They are doing the same thing. So not sure why I'm getting a discrepancy. + // TODO - discuss this w/ Crispy or with fresh eyes. + function testStrategistWithdrawTooLowHF() external { + uint256 assets = 1e18; + deal(address(DAI), address(this), assets); + + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(cellar), 0); + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // calculate amount needed to withdraw to have lower than minHF + uint256 amountToWithdraw = assets + 283e15; // amount to withdraw to get it lower than HF but not lower than getHypotheticalAccountLiquidityInternal() + // is this performing the way we want it to then? Hmm. Well HealthFactor + + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, amountToWithdraw); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes(abi.encodeWithSelector(CTokenAdaptor.CTokenAdaptor__HealthFactorTooLow.selector, address(cDAI))) + ); + cellar.callOnAdaptor(data); + } + + // check that exit market exits position from compoundV2 market collateral position + function testExitMarket(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](2); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cDAI); + data[1] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + bool inCTokenMarket = _checkInMarket(cDAI); + assertEq(inCTokenMarket, false, "Should not be 'IN' the market"); + } + + // same setup as testTotalAssetsWithJustEnterMarket, except after doing everything, do one more adaptor call. + function testTotalAssetsAfterExitMarket(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + assets = bound(assets, 0.1e18, 1_000_000e18); + + _setupSimpleLendAndEnter(assets, initialAssets); + + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cUSDC); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + _totalAssetsCheck(assets, initialAssets); + } + + function testCellarWithdrawTooMuch(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); + + deal(address(DAI), address(this), 0); + + vm.expectRevert(); + cellar.withdraw(assets * 2, address(this), address(this)); + } + + // if position is already in market, reverts to save on gas for unecessary call + function testAlreadyInMarket(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes(abi.encodeWithSelector(CTokenAdaptor.CTokenAdaptor__AlreadyInMarket.selector, address(cDAI))) + ); + cellar.callOnAdaptor(data); + } + + // lend assets + // prank as whale + // whale supplies to a different market as collateral, then borrows from this market all of the assets. + // test address tries to do withdrawableFrom, it doesn't work + // test address tries to do a cellar.withdraw(), it doesn't work. + // prank as whale, have them repay half of their loan + // test address calls withdrawableFrom + // test address calls cellar.withdraw() + function testWithdrawableFromAndIlliquidWithdraws(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + deal(address(USDC), address(cellar), 0); + + vm.startPrank(address(whaleBorrower)); + uint256 liquidSupply = cDAI.getCash(); + uint256 amountToBorrow = assets > liquidSupply ? assets : liquidSupply; + uint256 collateralToProvide = priceRouter.getValue(DAI, 2 * amountToBorrow, USDC); + deal(address(USDC), whaleBorrower, collateralToProvide); + USDC.approve(address(cUSDC), collateralToProvide); + cUSDC.mint(collateralToProvide); + + address[] memory cToken = new address[](1); + uint256[] memory result = new uint256[](1); + cToken[0] = address(cUSDC); + result = comptroller.enterMarkets(cToken); // enter the market + + if (result[0] > 0) revert(); + + // now borrow + cDAI.borrow(amountToBorrow); + vm.stopPrank(); + + uint256 assetsWithdrawable = cellar.totalAssetsWithdrawable(); + liquidSupply = cDAI.getCash(); + + assertEq(assetsWithdrawable, 0, "There should be no assets withdrawable."); + assertEq(assetsWithdrawable, liquidSupply, "There should be no assets withdrawable."); + + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + // try doing a strategist withdraw, it should revert because supplied assets are illiquid + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + vm.expectRevert(); // TODO - figure out what specific revert error is coming + cellar.callOnAdaptor(data); + + // Whale repays half of their debt. + vm.startPrank(whaleBorrower); + DAI.approve(address(cDAI), assets); + cDAI.repayBorrow(assets / 2); + vm.stopPrank(); + + liquidSupply = cDAI.getCash(); + assetsWithdrawable = cellar.totalAssetsWithdrawable(); + console.log("liquidSupply: %s, assetsWithdrawable: %s", liquidSupply, assetsWithdrawable); + assertEq(assetsWithdrawable, liquidSupply, "Should be able to withdraw liquid loanToken."); + // Have user withdraw the loanToken. + deal(address(DAI), address(this), 0); + cellar.withdraw(liquidSupply, address(this), address(this)); + assertEq(DAI.balanceOf(address(this)), liquidSupply, "User should have received liquid loanToken."); + } + + //============================================ CompoundV2DebtAdaptor Tests =========================================== + + // simple borrow using strategist functions + function testBorrow(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow from a different market, it should be fine because that is how compoundV2 works, it shares collateral amongst a bunch of different lending markets. + + deal(address(USDC), address(cellar), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertEq( + USDC.balanceOf(address(cellar)), + amountToBorrow, + "Requested amountToBorrow should be met from borrow tx." + ); + assertEq( + cUSDC.borrowBalanceStored(address(cellar)), + amountToBorrow, + "CompoundV2 market reflects total borrowed." + ); + } + + // simple test checking that tx will revert when strategist tries borrowing more than allowed based on adaptor specced health factor. + function testBorrowHFRevert(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(cellar), 0); + + uint256 lowerThanMinHF = 1.05e18; + uint256 amountToBorrow = _generateAmountToBorrowOptionB(lowerThanMinHF, address(cellar), USDC.decimals()); // back calculate the amount to borrow so: liquidateHF < HF < minHF, otherwise it will revert because of liquidateHF check in compound + + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__HealthFactorTooLow.selector, + address(cUSDC) + ) + ) + ); + cellar.callOnAdaptor(data); + } + + // This check stops strategists from taking on any debt in positions they do not set up properly. + function testTakingOutLoanInUntrackedPositionV2(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cTUSD, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__CompoundV2PositionsMustBeTracked.selector, + address(cTUSD) + ) + ) + ); + cellar.callOnAdaptor(data); + } + + function testBorrowWithNoEnteredMarketPositions(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + deal(address(USDC), address(cellar), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert(); + cellar.callOnAdaptor(data); + } + + function testCompoundInternalRevertFromBorrowingTooMuch(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + assets = bound(assets, 1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + deal(address(USDC), address(cellar), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets + initialAssets, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__NonZeroCompoundErrorCode.selector, + 3 + ) + ) + ); + cellar.callOnAdaptor(data); + } + + // simply test repaying and that balances make sense + function testRepayingLoans(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertEq( + USDC.balanceOf(address(cellar)), + 0, + "Cellar should have repaid USDC debt with all of its USDC balance." + ); + assertEq(cUSDC.borrowBalanceStored(address(cellar)), 0, "CompoundV2 market reflects debt being repaid fully."); + } + + // repay some + // repay all + function testMultipleRepayments(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + USDC.balanceOf(address(cellar)), + amountToBorrow / 2, + 2, + "Cellar should have repaid about half of debt." + ); + + assertApproxEqAbs( + cUSDC.borrowBalanceStored(address(cellar)), + amountToBorrow / 2, + 2, + "CompoundV2 market reflects debt being repaid partially." + ); + + // repay rest + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + assertEq(USDC.balanceOf(address(cellar)), 0, "Cellar should have repaid all of debt."); + assertEq(cUSDC.borrowBalanceStored(address(cellar)), 0, "CompoundV2 market reflects debt being repaid fully."); + } + + // CompoundV2 doesn't allow repayment over what is owed by user. This is double checking that scenario. + function testRepayMoreThanIsOwed(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // Try repaying more than what is owed. + deal(address(USDC), address(cellar), amountToBorrow +1); + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow + 1 ); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes(abi.encodeWithSelector(CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__NonZeroCompoundErrorCode.selector, 9)) + ); + cellar.callOnAdaptor(data); + + // now make sure it can be repaid for a sanity check if we specify the right amount or less. + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow ); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertEq(cUSDC.borrowBalanceStored(address(cellar)), 0, "CompoundV2 market reflects debt being repaid fully."); + assertEq(USDC.balanceOf(address(cellar)),1, "Debt should be paid."); + } + + // repay for a market that cellar is not tracking as a debt position + function testRepayingUnregisteredDebtMarket(uint256 assets) external { + uint256 price = uint256(IChainlinkAggregator(WBTC_USD_FEED).latestAnswer()); + PriceRouter.ChainlinkDerivativeStorage memory stor; + PriceRouter.AssetSettings memory settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, WBTC_USD_FEED); + priceRouter.addAsset(WBTC, settings, abi.encode(stor), price); + + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, WBTC); + + // repay + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cWBTC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__CompoundV2PositionsMustBeTracked.selector, + address(cWBTC) + ) + ) + ); + cellar.callOnAdaptor(data); + } + + //============================================ Collateral (CToken) and Debt Tests =========================================== + + // exiting market when that lowers HF past adaptor specced HF + // NOTE - not sure if this is needed because I thought Compound does a check, AND exiting completely removes the collateral position in the respective market. If anything, we ought to do a test where we have multiple compound positions, and exit one of them that has a small amount of collateral that is JUST big enough to tip the cellar health factor below the minimum. + function testStrategistExitMarketShortFallInCompoundV2(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(cellar), 0); + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes(abi.encodeWithSelector(CTokenAdaptor.CTokenAdaptor__NonZeroCompoundErrorCode.selector, 14)) + ); + cellar.callOnAdaptor(data); + } + + // test type(uint256).max removal after repays on an open borrow position + // test withdrawing without calling `exitMarket` + function testWithdrawCollateralWithTypeUINT256MaxAfterRepayWithoutExitingMarket(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // withdraw from compoundV2 + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + DAI.balanceOf(address(cellar)), + assets + initialAssets, + 1e9, + "All assets should have been withdrawn." + ); // TODO - tolerances should be lowered but will look at this later. + } + + // test type(uint256).max removal after repays on an open borrow position + // test withdrawing collateral with calling `exitMarket` first to make sure it all works still either way + function testRemoveCollateralWithTypeUINT256MaxAfterRepayWITHExitingMarket(uint256 assets) external { + uint256 initialAssets = cellar.totalAssets(); + + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // exit market + { + adaptorCalls[0] = _createBytesDataToExitMarketWithCompoundV2(cUSDC); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + // withdraw from compoundV2 + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + DAI.balanceOf(address(cellar)), + assets + initialAssets, + 10e8, + "All assets should have been withdrawn." + ); // TODO - tolerances should be lowered but will look at this later. + } + + // compare health factor calculation method options A and B to see how much accuracy is lost when doing the "less precise" way of option A. See `CompoundV2HelperLogic.sol` that uses option A. Option B's helper logic smart contract was deleted but its methodology can be seen in this test file in the helpers at the bottom. + function testHFLossyness(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(cellar), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // get HF using method A + uint256 healthFactorOptionA = _getHFOptionA(address(cellar)); + // get HF using method B + uint256 healthFactorOptionB = _getHFOptionB(address(cellar)); + + // compare method results + uint256 relativeDiff; + if (healthFactorOptionA >= healthFactorOptionB) { + relativeDiff = ((healthFactorOptionA - healthFactorOptionB) * 1e18) / healthFactorOptionA; + } else { + relativeDiff = ((healthFactorOptionB - healthFactorOptionA) * 1e18) / healthFactorOptionB; + } + + assertGt(1e16, relativeDiff, "relativeDiff cannot exceed 1bps."); // ensure that relativeDiff !> 1bps (1e16) + } + + // add collateral + // try borrowing from same market + // a borrow position will open up in the same market; cellar has a cToken position from lending underlying (DAI), and a borrow balance from borrowing DAI. + // test redeeming going up to the HF, have it revert because of HF. + // test borrowing going up to the HF, have it revert because of HF. + function testBorrowInSameCollateralMarket() external { + uint256 assets = 1e18; + // assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + uint256 initialcDaiBalance = cDAI.balanceOf(address(cellar)); + console.log("initialCDaiBalance: %s", initialcDaiBalance); + + // borrow from same market unlike other tests + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cDAI, (assets/2)); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // call should go through, and it records a borrow balance in cToken market. + assertEq( + DAI.balanceOf(address(cellar)), + assets / 2, + "Borrowing from a market that cellar has lent out to already means they are just withdrawing some of their lent out initial amount." + ); + assertEq( + cDAI.borrowBalanceStored(address(cellar)), + assets / 2, + "CompoundV2 market should show borrowed, even though cellar is also supplying said underlying asset." + ); + assertEq( + cDAI.balanceOf(address(cellar)), + initialcDaiBalance, + "CompoundV2 market should show same amount cDai for cellar." + ); + + uint256 amountToWithdraw = assets + 3e17; // iterated amount to withdraw to get it lower than HF but not lower than getHypotheticalAccountLiquidityInternal() + + console.log("assets: %s, amountToWithdraw: %s ",assets,amountToWithdraw); + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, (amountToWithdraw) ); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes(abi.encodeWithSelector(CTokenAdaptor.CTokenAdaptor__HealthFactorTooLow.selector, address(cDAI))) + ); + cellar.callOnAdaptor(data); + + uint256 amountToBorrow = 9e5; // iterated amount to borrow to get it lower than HF but not lower than getHypotheticalAccountLiquidityInternal() + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__HealthFactorTooLow.selector, + address(cUSDC) + ) + ) + ); + cellar.callOnAdaptor(data); + } + + function testRepayingDebtThatIsNotOwed(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + { + adaptorCalls[0] = _createBytesDataToBorrowWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow + 1); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__NonZeroCompoundErrorCode.selector, + 13 + ) + ) + ); + cellar.callOnAdaptor(data); + } + + // externalReceiver triggers when doing Strategist Function calls via adaptorCall. + function testBlockExternalReceiver(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist tries to withdraw USDC to their own wallet using Adaptor's `withdraw` function. + address maliciousStrategist = vm.addr(10); + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = abi.encodeWithSelector( + CTokenAdaptor.withdraw.selector, + assets, + maliciousStrategist, + abi.encode(cDAI), + abi.encode(0) + ); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + vm.expectRevert(bytes(abi.encodeWithSelector(BaseAdaptor.BaseAdaptor__UserWithdrawsNotAllowed.selector))); + cellar.callOnAdaptor(data); + } + + //============================================ Compound Revert Tests =========================================== + + // These tests are just to check that compoundV2 reverts as it is supposed to. + + // test that it reverts if trying to redeem too much --> it should revert because of CompoundV2, no need for us to worry about it. We will throw in a test though to be sure. + function testWithdrawMoreThanSupplied(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + // uint256 assets = 100e6; + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + { + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, (assets + 1e18) * 10); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + vm.expectRevert(); + cellar.callOnAdaptor(data); + } + + // repay for a market that cellar does not have a borrow position in + function testRepayingLoansWithNoBorrowPosition(uint256 assets) external { + assets = bound(assets, 0.1e18, 1_000_000e18); + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); // holding position is cDAI (w/o entering market) + + // enter market + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + { + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + // borrow + deal(address(USDC), address(this), 0); + + uint256 amountToBorrow = priceRouter.getValue(DAI, assets / 2, USDC); + + { + adaptorCalls[0] = _createBytesDataToRepayWithCompoundV2(cUSDC, amountToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(compoundV2DebtAdaptor), callData: adaptorCalls }); + } + vm.expectRevert( + bytes( + abi.encodeWithSelector( + CompoundV2DebtAdaptor.CompoundV2DebtAdaptor__NonZeroCompoundErrorCode.selector, + 13 + ) + ) + ); + cellar.callOnAdaptor(data); + } + + //============================================ Helpers =========================================== + + function _checkInMarket(CErc20 _market) internal view returns (bool inCTokenMarket) { + // check that we aren't in market + CErc20[] memory marketsEntered = comptroller.getAssetsIn(address(cellar)); + for (uint256 i = 0; i < marketsEntered.length; i++) { + // check if cToken is one of the markets cellar position is in. + if (marketsEntered[i] == _market) { + inCTokenMarket = true; + } + } + } + + + function _setupSimpleLendAndEnter(uint256 assets, uint256 initialAssets) internal { + deal(address(DAI), address(this), assets); + cellar.deposit(assets, address(this)); + assertApproxEqRel( + cellar.totalAssets(), + assets + initialAssets, + 0.0002e18, + "Total assets should equal assets deposited." + ); + + // Swap from USDC to DAI and lend DAI on Compound. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](5); + + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cDAI); + data[0] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromCompoundV2(cDAI, assets / 2); + data[1] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataForSwapWithUniv3(DAI, USDC, 100, assets / 2); + data[2] = Cellar.AdaptorCall({ adaptor: address(swapWithUniswapAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnComnpoundV2(cUSDC, type(uint256).max); + data[3] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToEnterMarketWithCompoundV2(cUSDC); + data[4] = Cellar.AdaptorCall({ adaptor: address(cTokenAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + } + + function _totalAssetsCheck(uint256 assets, uint256 initialAssets) internal { + // Account for 0.1% Swap Fee. + assets = assets - assets.mulDivDown(0.001e18, 2e18); + // Make sure Total Assets is reasonable. + assertApproxEqRel( + cellar.totalAssets(), + assets + initialAssets, + 0.001e18, + "Total assets should equal assets deposited minus swap fees." + ); + } + + // helper to produce the amountToBorrow or amountToWithdraw to get a certain health factor + // uses precision matching option A + function _generateAmountBasedOnHFOptionA( + uint256 _hfRequested, + address _account, + uint256 _borrowDecimals, + bool _borrow + ) internal view returns (uint256) { + // get the array of markets currently being used + CErc20[] memory marketsEntered; + marketsEntered = comptroller.getAssetsIn(address(_account)); + PriceOracle oracle = comptroller.oracle(); + uint256 sumCollateral; + uint256 sumBorrow; + uint256 marketsEnteredLength = marketsEntered.length; + // Loop to calculate total collateral & total borrow for HF calcs w/ assets we're in. + for (uint256 i = 0; i < marketsEnteredLength; i++) { + CErc20 asset = marketsEntered[i]; + (, uint256 cTokenBalance, uint256 borrowBalance, uint256 exchangeRate) = asset.getAccountSnapshot(_account); + uint256 oraclePrice = oracle.getUnderlyingPrice(asset); + // get collateral factor from markets + (, uint256 collateralFactor, ) = comptroller.markets(address(asset)); // always scaled by 18 decimals + uint256 actualCollateralBacking = cTokenBalance.mulDivDown(exchangeRate, 1e18); // NOTE - this is the 1st key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(oraclePrice, 1e18); // NOTE - this is the 2nd key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(collateralFactor, 1e18); // scaling factor for collateral factor is always 1e18. + uint256 additionalBorrowBalance = borrowBalance.mulDivDown(oraclePrice, 1e18); // converts cToken underlying borrow to USD + sumCollateral = sumCollateral + actualCollateralBacking; + sumBorrow = additionalBorrowBalance + sumBorrow; + } + + if (_borrow) { + uint256 borrowAmountNeeded = (sumCollateral.mulDivDown(1e18, _hfRequested) - sumBorrow) / + (10 ** (18 - _borrowDecimals)); // recall: sumBorrow = sumCollateral / healthFactor --> because specific market collateral factors are already accounted for within calcs above + return borrowAmountNeeded; + + // uint256 borrowAmountNeeded = (sumCollateral.mulDivDown(1e18, _hfRequested) - sumBorrow); + // return borrowAmountNeeded; + } else { + uint256 withdrawAmountNeeded = (sumCollateral - (sumBorrow.mulDivDown(_hfRequested, 1e18)) / (10 ** (18 - _borrowDecimals))); + console.log("sumCollateral: %s, sumBorrow: %s, hfRequested: %s", sumCollateral, sumBorrow, _hfRequested); + + // healthfactor = sumcollateral / sumborrow. + // we want the amount that needs to be withdrawn from collateral to get a certain hf + // hf * sumborrow = sumcollateral + // sumCollateral2 = sumCollateral1 - withdrawnCollateral + // sumCollateral1 - withdrawnCollateral = hf * sumborrow + // hf * sumborrow - sumCollateral1 = - withdrawnCollateral + // withdrawnCollateral = sumCollateral1 - hf*sumborrow + return withdrawAmountNeeded; + } + } + + function _getHFOptionA(address _account) internal view returns (uint256 healthFactor) { + // get the array of markets currently being used + CErc20[] memory marketsEntered; + + marketsEntered = comptroller.getAssetsIn(address(_account)); + PriceOracle oracle = comptroller.oracle(); + uint256 sumCollateral; + uint256 sumBorrow; + uint256 marketsEnteredLength = marketsEntered.length; + // Loop to calculate total collateral & total borrow for HF calcs w/ assets we're in. + for (uint256 i = 0; i < marketsEnteredLength; i++) { + CErc20 asset = marketsEntered[i]; + (, uint256 cTokenBalance, uint256 borrowBalance, uint256 exchangeRate) = asset.getAccountSnapshot(_account); + uint256 oraclePrice = oracle.getUnderlyingPrice(asset); + // get collateral factor from markets + (, uint256 collateralFactor, ) = comptroller.markets(address(asset)); // always scaled by 18 decimals + uint256 actualCollateralBacking = cTokenBalance.mulDivDown(exchangeRate, 1e18); // NOTE - this is the 1st key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(oraclePrice, 1e18); // NOTE - this is the 2nd key difference usage of a different scaling factor than in OptionB and CompoundV2. This means less precision but it is possibly negligible. + actualCollateralBacking = actualCollateralBacking.mulDivDown(collateralFactor, 1e18); // scaling factor for collateral factor is always 1e18. + // scale up actualCollateralBacking to 1e18 if it isn't already for health factor calculations. + uint256 additionalBorrowBalance = borrowBalance.mulDivDown(oraclePrice, 1e18); // converts cToken underlying borrow to USD + sumCollateral = sumCollateral + actualCollateralBacking; + sumBorrow = additionalBorrowBalance + sumBorrow; + } + // now we can calculate health factor with sumCollateral and sumBorrow + healthFactor = sumCollateral.mulDivDown(1e18, sumBorrow); + } + + function _getHFOptionB(address _account) internal view returns (uint256 healthFactor) { + // get the array of markets currently being used + CErc20[] memory marketsEntered; + marketsEntered = comptroller.getAssetsIn(address(_account)); + PriceOracle oracle = comptroller.oracle(); + uint256 sumCollateral; + uint256 sumBorrow; + // Loop to calculate total collateral & total borrow for HF calcs w/ assets we're in. + for (uint256 i = 0; i < marketsEntered.length; i++) { + // Obtain values from markets + CErc20 asset = marketsEntered[i]; + (, uint256 cTokenBalance, uint256 borrowBalance, uint256 exchangeRate) = asset.getAccountSnapshot(_account); + uint256 oraclePrice = oracle.getUnderlyingPrice(asset); + ERC20 underlyingAsset = ERC20(asset.underlying()); + uint256 underlyingDecimals = underlyingAsset.decimals(); + + // Actual calculation of collateral and borrows for respective markets. + // NOTE - below is scoped for stack too deep errors + { + (, uint256 collateralFactor, ) = comptroller.markets(address(asset)); // get collateral factor from markets + uint256 oraclePriceScalingFactor = 10 ** (36 - underlyingDecimals); + uint256 exchangeRateScalingFactor = 10 ** (18 - 8 + underlyingDecimals); //18 - 8 + underlyingDecimals + uint256 actualCollateralBacking = cTokenBalance.mulDivDown(exchangeRate, exchangeRateScalingFactor); + + // convert to USD values + + actualCollateralBacking = actualCollateralBacking.mulDivDown(oraclePrice, oraclePriceScalingFactor); // converts it to USD but it is in the decimals of the underlying --> it's still in decimals of 8 (so ctoken decimals) + + // Apply collateral factor to collateral backing + actualCollateralBacking = actualCollateralBacking.mulDivDown(collateralFactor, 1e18); // scaling factor for collateral factor is always 1e18. + + // refactor as needed for decimals + actualCollateralBacking = _refactorCollateralBalance(actualCollateralBacking, underlyingDecimals); // scale up additionalBorrowBalance to 1e18 if it isn't already. + + // borrow balances + // NOTE - below is scoped for stack too deep errors + { + uint256 additionalBorrowBalance = borrowBalance.mulDivDown(oraclePrice, oraclePriceScalingFactor); // converts cToken underlying borrow to USD but it's in decimals of underlyingAsset + + // refactor as needed for decimals + additionalBorrowBalance = _refactorBorrowBalance(additionalBorrowBalance, underlyingDecimals); + + sumBorrow = sumBorrow + additionalBorrowBalance; + } + + sumCollateral = sumCollateral + actualCollateralBacking; + } + } + // now we can calculate health factor with sumCollateral and sumBorrow + healthFactor = sumCollateral.mulDivDown(1e18, sumBorrow); + } + + /** + * @notice Option B - The ```_getHealthFactor``` function returns the current health factor + * @dev This has the same logic as CompoundV2HelperLogicVersionB + */ + function _generateAmountToBorrowOptionB( + uint256 _hfRequested, + address _account, + uint256 _borrowDecimals + ) internal view returns (uint256 borrowAmountNeeded) { + // get the array of markets currently being used + CErc20[] memory marketsEntered; + marketsEntered = comptroller.getAssetsIn(address(_account)); + PriceOracle oracle = comptroller.oracle(); + uint256 sumCollateral; + uint256 sumBorrow; + // Loop to calculate total collateral & total borrow for HF calcs w/ assets we're in. + for (uint256 i = 0; i < marketsEntered.length; i++) { + // Obtain values from markets + CErc20 asset = marketsEntered[i]; + (, uint256 cTokenBalance, uint256 borrowBalance, uint256 exchangeRate) = asset.getAccountSnapshot(_account); + uint256 oraclePrice = oracle.getUnderlyingPrice(asset); + ERC20 underlyingAsset = ERC20(asset.underlying()); + uint256 underlyingDecimals = underlyingAsset.decimals(); + + // Actual calculation of collateral and borrows for respective markets. + // NOTE - below is scoped for stack too deep errors + { + (, uint256 collateralFactor, ) = comptroller.markets(address(asset)); // get collateral factor from markets + uint256 oraclePriceScalingFactor = 10 ** (36 - underlyingDecimals); + uint256 exchangeRateScalingFactor = 10 ** (18 - 8 + underlyingDecimals); // 18 - 8 + underlyingDecimals + uint256 actualCollateralBacking = cTokenBalance.mulDivDown(exchangeRate, exchangeRateScalingFactor); // okay, for dai, you'd end up with: 8 + 28 - 28... yeah so it just stays as 8 + + // convert to USD values + actualCollateralBacking = actualCollateralBacking.mulDivDown(oraclePrice, oraclePriceScalingFactor); // converts it to USD but it is in the decimals of the underlying --> it's still in decimals of 8 (so ctoken decimals) + + // Apply collateral factor to collateral backing + actualCollateralBacking = actualCollateralBacking.mulDivDown(collateralFactor, 1e18); // scaling factor for collateral factor is always 1e18. + + // refactor as needed for decimals + actualCollateralBacking = _refactorCollateralBalance(actualCollateralBacking, underlyingDecimals); // scale up additionalBorrowBalance to 1e18 if it isn't already. + + // borrow balances + // NOTE - below is scoped for stack too deep errors + { + uint256 additionalBorrowBalance = borrowBalance.mulDivDown(oraclePrice, oraclePriceScalingFactor); // converts cToken underlying borrow to USD but it's in decimals of underlyingAsset + + // refactor as needed for decimals + additionalBorrowBalance = _refactorBorrowBalance(additionalBorrowBalance, underlyingDecimals); + + sumBorrow = sumBorrow + additionalBorrowBalance; + } + + sumCollateral = sumCollateral + actualCollateralBacking; + } + } + + borrowAmountNeeded = + (sumCollateral.mulDivDown(1e18, _hfRequested) - sumBorrow) / + (10 ** (18 - _borrowDecimals)); // recall: sumBorrow = sumCollateral / healthFactor --> because specific market collateral factors are already accounted for within calcs above + } + + // helper that scales passed in param _balance to 18 decimals. This is needed to make it easier for health factor calculations + function _refactorBalance(uint256 _balance, uint256 _decimals) public pure returns (uint256) { + if (_decimals != 18) { + _balance = _balance * (10 ** (18 - _decimals)); + } + return _balance; + } + + // helper that scales passed in param _balance to 18 decimals. _balance param is always passed in 8 decimals (cToken decimals). This is needed to make it easier for health factor calculations + function _refactorCollateralBalance(uint256 _balance, uint256 _decimals) public pure returns (uint256 balance) { + if (_decimals < 8) { + //convert to _decimals precision first) + balance = _balance / (10 ** (8 - _decimals)); + } else if (_decimals > 8) { + balance = _balance * (10 ** (_decimals - 8)); + } + if (_decimals != 18) { + balance = balance * (10 ** (18 - _decimals)); // if _balance is 8 decimals, it'll convert balance to 18 decimals. Ah. + } + return balance; + } + + function _refactorBorrowBalance(uint256 _balance, uint256 _decimals) public pure returns (uint256 balance) { + if (_decimals != 18) { + balance = _balance * (10 ** (18 - _decimals)); // if _balance is 8 decimals, it'll convert balance to 18 decimals. Ah. + } + return balance; + } +} + + diff --git a/test/testAdaptors/MorphoBlue/MorphoBlueCollateralAndDebt.t.sol b/test/testAdaptors/MorphoBlue/MorphoBlueCollateralAndDebt.t.sol new file mode 100644 index 000000000..8110b8174 --- /dev/null +++ b/test/testAdaptors/MorphoBlue/MorphoBlueCollateralAndDebt.t.sol @@ -0,0 +1,987 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; +import { MockDataFeedForMorphoBlue } from "src/mocks/MockDataFeedForMorphoBlue.sol"; +import "test/resources/MainnetStarter.t.sol"; +import { MorphoBlueDebtAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueDebtAdaptor.sol"; +import { MorphoBlueHelperLogic } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueHelperLogic.sol"; +import { MorphoBlueCollateralAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueCollateralAdaptor.sol"; +import { MorphoBlueSupplyAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol"; +import { IMorpho, MarketParams, Id, Market } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { SharesMathLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol"; +import { MarketParamsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol"; +import "forge-std/console.sol"; +import { MorphoLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol"; +import { IrmMock } from "src/mocks/IrmMock.sol"; + +/** + * @notice Test provision of collateral and borrowing on MorphoBlue Markets. + * @author 0xEinCodes, crispymangoes + */ +contract MorphoBlueCollateralAndDebtTest is MainnetStarterTest, AdaptorHelperFunctions { + using SafeTransferLib for ERC20; + using Math for uint256; + using SharesMathLib for uint256; + using MarketParamsLib for MarketParams; + using MorphoLib for IMorpho; + + MorphoBlueCollateralAdaptor public morphoBlueCollateralAdaptor; + MorphoBlueDebtAdaptor public morphoBlueDebtAdaptor; + MorphoBlueSupplyAdaptor public morphoBlueSupplyAdaptor; + + Cellar public cellar; + + uint32 public morphoBlueSupplyWETHPosition = 1_000_001; + uint32 public morphoBlueCollateralWETHPosition = 1_000_002; + uint32 public morphoBlueDebtWETHPosition = 1_000_003; + uint32 public morphoBlueSupplyUSDCPosition = 1_000_004; + uint32 public morphoBlueCollateralUSDCPosition = 1_000_005; + uint32 public morphoBlueDebtUSDCPosition = 1_000_006; + uint32 public morphoBlueSupplyWBTCPosition = 1_000_007; + uint32 public morphoBlueCollateralWBTCPosition = 1_000_008; + uint32 public morphoBlueDebtWBTCPosition = 1_000_009; + + IMorpho public morphoBlue = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + address public morphoBlueOwner = 0x6ABfd6139c7C3CC270ee2Ce132E309F59cAaF6a2; + address public DEFAULT_IRM = 0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC; + uint256 public DEFAULT_LLTV = 860000000000000000; // (86% LLTV) + + // Chainlink PriceFeeds + MockDataFeedForMorphoBlue private mockWethUsd; + MockDataFeedForMorphoBlue private mockUsdcUsd; + MockDataFeedForMorphoBlue private mockWbtcUsd; + MockDataFeedForMorphoBlue private mockDaiUsd; + + uint32 private wethPosition = 1; + uint32 private usdcPosition = 2; + uint32 private wbtcPosition = 3; + uint32 private daiPosition = 4; + + uint256 initialAssets; + uint256 minHealthFactor = 1.05e18; + + bool ACCOUNT_FOR_INTEREST = true; + + MarketParams private wethUsdcMarket; + MarketParams private wbtcUsdcMarket; + MarketParams private usdcDaiMarket; + Id private wethUsdcMarketId; + Id private wbtcUsdcMarketId; + Id private usdcDaiMarketId; + + address internal SUPPLIER; + IrmMock internal irm; + + function setUp() public { + // Setup forked environment. + string memory rpcKey = "MAINNET_RPC_URL"; + uint256 blockNumber = 18922158; + + _startFork(rpcKey, blockNumber); + + // Run Starter setUp code. + _setUp(); + + mockUsdcUsd = new MockDataFeedForMorphoBlue(USDC_USD_FEED); + mockWbtcUsd = new MockDataFeedForMorphoBlue(WBTC_USD_FEED); + mockWethUsd = new MockDataFeedForMorphoBlue(WETH_USD_FEED); + mockDaiUsd = new MockDataFeedForMorphoBlue(DAI_USD_FEED); + + bytes memory creationCode; + bytes memory constructorArgs; + + creationCode = type(MorphoBlueCollateralAdaptor).creationCode; + constructorArgs = abi.encode(address(morphoBlue), minHealthFactor); + morphoBlueCollateralAdaptor = MorphoBlueCollateralAdaptor( + deployer.deployContract("Morpho Blue Collateral Adaptor V 0.0", creationCode, constructorArgs, 0) + ); + + creationCode = type(MorphoBlueDebtAdaptor).creationCode; + constructorArgs = abi.encode(address(morphoBlue), minHealthFactor); + morphoBlueDebtAdaptor = MorphoBlueDebtAdaptor( + deployer.deployContract("Morpho Blue Debt Adaptor V 0.0", creationCode, constructorArgs, 0) + ); + + creationCode = type(MorphoBlueSupplyAdaptor).creationCode; + constructorArgs = abi.encode(address(morphoBlue)); + morphoBlueSupplyAdaptor = MorphoBlueSupplyAdaptor( + deployer.deployContract("Morpho Blue Supply Adaptor V 0.0", creationCode, constructorArgs, 0) + ); + + PriceRouter.ChainlinkDerivativeStorage memory stor; + + PriceRouter.AssetSettings memory settings; + + uint256 price = uint256(mockWethUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWethUsd)); + priceRouter.addAsset(WETH, settings, abi.encode(stor), price); + + price = uint256(mockUsdcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockUsdcUsd)); + priceRouter.addAsset(USDC, settings, abi.encode(stor), price); + + price = uint256(mockWbtcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWbtcUsd)); + priceRouter.addAsset(WBTC, settings, abi.encode(stor), price); + + price = uint256(mockDaiUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockDaiUsd)); + priceRouter.addAsset(DAI, settings, abi.encode(stor), price); + + // set mock prices for chainlink price feeds, but add in params to adjust the morphoBlue price format needed --> recall from IOracle.sol that the units will be 10 ** (36 - collateralUnits + borrowUnits) + + mockWethUsd.setMockAnswer(2200e8, WETH, USDC); + mockUsdcUsd.setMockAnswer(1e8, USDC, USDC); + mockWbtcUsd.setMockAnswer(42000e8, WBTC, USDC); + mockDaiUsd.setMockAnswer(1e8, DAI, USDC); + + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(morphoBlueCollateralAdaptor)); + registry.trustAdaptor(address(morphoBlueDebtAdaptor)); + registry.trustAdaptor(address(morphoBlueSupplyAdaptor)); + + registry.trustPosition(wethPosition, address(erc20Adaptor), abi.encode(WETH)); + registry.trustPosition(usdcPosition, address(erc20Adaptor), abi.encode(USDC)); + registry.trustPosition(wbtcPosition, address(erc20Adaptor), abi.encode(WBTC)); + registry.trustPosition(daiPosition, address(erc20Adaptor), abi.encode(DAI)); + + /// setup morphoBlue test markets; WETH:USDC, WBTC:USDC, USDC:DAI? + // note - oracle param w/ MarketParams struct is for collateral price + + wethUsdcMarket = MarketParams({ + loanToken: address(USDC), + collateralToken: address(WETH), + oracle: address(mockWethUsd), + irm: DEFAULT_IRM, + lltv: DEFAULT_LLTV + }); + + wbtcUsdcMarket = MarketParams({ + loanToken: address(USDC), + collateralToken: address(WBTC), + oracle: address(mockWbtcUsd), + irm: DEFAULT_IRM, + lltv: DEFAULT_LLTV + }); + + usdcDaiMarket = MarketParams({ + loanToken: address(DAI), + collateralToken: address(USDC), + oracle: address(mockUsdcUsd), + irm: DEFAULT_IRM, + lltv: DEFAULT_LLTV + }); + + morphoBlue.createMarket(wethUsdcMarket); + wethUsdcMarketId = wethUsdcMarket.id(); + + morphoBlue.createMarket(wbtcUsdcMarket); + wbtcUsdcMarketId = wbtcUsdcMarket.id(); + + morphoBlue.createMarket(usdcDaiMarket); + usdcDaiMarketId = usdcDaiMarket.id(); + + registry.trustPosition( + morphoBlueSupplyWETHPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(wethUsdcMarket) + ); + registry.trustPosition( + morphoBlueCollateralWETHPosition, + address(morphoBlueCollateralAdaptor), + abi.encode(wethUsdcMarket) + ); + registry.trustPosition(morphoBlueDebtWETHPosition, address(morphoBlueDebtAdaptor), abi.encode(wethUsdcMarket)); + registry.trustPosition( + morphoBlueSupplyUSDCPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(usdcDaiMarket) + ); + registry.trustPosition( + morphoBlueCollateralUSDCPosition, + address(morphoBlueCollateralAdaptor), + abi.encode(usdcDaiMarket) + ); + registry.trustPosition(morphoBlueDebtUSDCPosition, address(morphoBlueDebtAdaptor), abi.encode(usdcDaiMarket)); + registry.trustPosition( + morphoBlueSupplyWBTCPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(wbtcUsdcMarket) + ); + registry.trustPosition( + morphoBlueCollateralWBTCPosition, + address(morphoBlueCollateralAdaptor), + abi.encode(wbtcUsdcMarket) + ); + registry.trustPosition(morphoBlueDebtWBTCPosition, address(morphoBlueDebtAdaptor), abi.encode(wbtcUsdcMarket)); + + string memory cellarName = "Morpho Blue Collateral & Debt Cellar V0.0"; + uint256 initialDeposit = 1e18; + uint64 platformCut = 0.75e18; + + // Approve new cellar to spend assets. + address cellarAddress = deployer.getAddress(cellarName); + deal(address(WETH), address(this), initialDeposit); + WETH.approve(cellarAddress, initialDeposit); + + creationCode = type(Cellar).creationCode; + constructorArgs = abi.encode( + address(this), + registry, + WETH, + cellarName, + cellarName, + wethPosition, + abi.encode(true), + initialDeposit, + platformCut, + type(uint192).max + ); + + cellar = Cellar(deployer.deployContract(cellarName, creationCode, constructorArgs, 0)); + + cellar.addAdaptorToCatalogue(address(morphoBlueSupplyAdaptor)); + cellar.addAdaptorToCatalogue(address(morphoBlueCollateralAdaptor)); + cellar.addAdaptorToCatalogue(address(morphoBlueDebtAdaptor)); + + cellar.addPositionToCatalogue(wethPosition); + cellar.addPositionToCatalogue(usdcPosition); + cellar.addPositionToCatalogue(wbtcPosition); + cellar.addPositionToCatalogue(daiPosition); + + // only add weth adaptor positions for now. + cellar.addPositionToCatalogue(morphoBlueSupplyWETHPosition); + cellar.addPositionToCatalogue(morphoBlueCollateralWETHPosition); + cellar.addPositionToCatalogue(morphoBlueDebtWETHPosition); + + cellar.addPosition(1, usdcPosition, abi.encode(true), false); + cellar.addPosition(2, wbtcPosition, abi.encode(true), false); + cellar.addPosition(3, morphoBlueSupplyWETHPosition, abi.encode(true), false); + cellar.addPosition(4, morphoBlueCollateralWETHPosition, abi.encode(0), false); + + cellar.addPosition(0, morphoBlueDebtWETHPosition, abi.encode(0), true); + + WETH.safeApprove(address(cellar), type(uint256).max); + USDC.safeApprove(address(cellar), type(uint256).max); + WBTC.safeApprove(address(cellar), type(uint256).max); + + SUPPLIER = makeAddr("Supplier"); + } + + /// MorphoBlueCollateralAdaptor tests + + // test that holding position for adding collateral is being tracked properly and works upon user deposits + function testDeposit(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.setHoldingPosition(morphoBlueCollateralWETHPosition); + cellar.deposit(assets, address(this)); + assertApproxEqAbs( + WETH.balanceOf(address(cellar)), + initialAssets, + 1, + "Cellar should have only initial assets, and have supplied the new asset amount as collateral" + ); + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + + assertEq(newCellarCollateralBalance, assets, "Assets should be collateral provided to Morpho Blue Market."); + } + + // test adding collateral where holdingPosition is WETH erc20Position + function testAddCollateral(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + assertApproxEqAbs( + WETH.balanceOf(address(cellar)), + assets + initialAssets, + 1, + "Cellar should have all deposited WETH assets" + ); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + assertApproxEqAbs( + WETH.balanceOf(address(cellar)), + initialAssets, + 1, + "Only initialAssets should be within Cellar." + ); + + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + assertEq( + newCellarCollateralBalance, + assets, + "Assets (except initialAssets) should be collateral provided to Morpho Blue Market." + ); + + // test balanceOf() of collateralAdaptor + bytes memory adaptorData = abi.encode(wethUsdcMarket); + vm.prank(address(cellar)); + uint256 newBalance = morphoBlueCollateralAdaptor.balanceOf(adaptorData); + + assertEq(newBalance, newCellarCollateralBalance, "CollateralAdaptor - balanceOf() additional tests"); + } + + // carry out a total assets test checking that balanceOf works for adaptors. + function testTotalAssets(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + assertApproxEqAbs( + cellar.totalAssets(), + (assets + initialAssets), + 1, + "Adaptor totalAssets(): Total assets should equal initialAssets + assets." + ); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + assertApproxEqAbs( + cellar.totalAssets(), + (assets + initialAssets), + 1, + "Adaptor totalAssets(): Total assets should not have changed." + ); + } + + function testRemoveCollateral(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // no collateral interest or anything has accrued, should be able to withdraw everything and have nothing left in it. + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + + assertEq(WETH.balanceOf(address(cellar)), assets + initialAssets); + assertEq(newCellarCollateralBalance, 0); + } + + function testRemoveSomeCollateral(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // no collateral interest or anything has accrued, should be able to withdraw everything and have nothing left in it. + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + + assertEq(WETH.balanceOf(address(cellar)), (assets / 2) + initialAssets); + assertApproxEqAbs(newCellarCollateralBalance, assets / 2, 1); + } + + // test strategist input param for _collateralAmount to be type(uint256).max + function testRemoveAllCollateralWithTypeUINT256Max(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // no collateral interest or anything has accrued, should be able to withdraw everything and have nothing left in it. + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + + assertEq(WETH.balanceOf(address(cellar)), assets + initialAssets); + assertEq(newCellarCollateralBalance, 0); + } + + // externalReceiver triggers when doing Strategist Function calls via adaptorCall within collateral adaptor. + function testBlockExternalReceiver(uint256 assets) external { + assets = bound(assets, 0.1e18, 100e18); + deal(address(WETH), address(this), assets); + cellar.setHoldingPosition(morphoBlueCollateralWETHPosition); + cellar.deposit(assets, address(this)); // holding position == collateralPosition w/ WETH MorphoBlue weth:usdc market + // Strategist tries to withdraw USDC to their own wallet using Adaptor's `withdraw` function. + address maliciousStrategist = vm.addr(10); + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = abi.encodeWithSelector( + MorphoBlueCollateralAdaptor.withdraw.selector, + 100_000e18, + maliciousStrategist, + abi.encode(wethUsdcMarket), + abi.encode(0) + ); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + vm.expectRevert(bytes(abi.encodeWithSelector(BaseAdaptor.BaseAdaptor__UserWithdrawsNotAllowed.selector))); + cellar.callOnAdaptor(data); + } + + /// MorphoBlueDebtAdaptor tests + + // test taking loans w/ a morpho blue market + function testTakingOutLoans(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 1000, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // Take out a loan + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + bytes memory adaptorData = abi.encode(wethUsdcMarket); + + vm.prank(address(cellar)); + uint256 newBalance = morphoBlueDebtAdaptor.balanceOf(adaptorData); + assertApproxEqAbs( + newBalance, + borrowAmount, + 1, + "DebtAdaptor - balanceOf() additional tests: Cellar should have debt recorded within Morpho Blue market equal to assets / 2" + ); + assertApproxEqAbs( + USDC.balanceOf(address(cellar)), + borrowAmount, + 1, + "Cellar should have borrow amount equal to assets / 2" + ); + } + + // test taking loan w/ the wrong pair that we provided collateral to + function testTakingOutLoanInUntrackedPosition(uint256 assets) external { + assets = bound(assets, 0.1e18, 100_000e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // try borrowing from the wrong market that is untracked by cellar + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(usdcDaiMarket, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + vm.expectRevert( + bytes( + abi.encodeWithSelector( + MorphoBlueDebtAdaptor.MorphoBlueDebtAdaptor__MarketPositionsMustBeTracked.selector, + usdcDaiMarket + ) + ) + ); + cellar.callOnAdaptor(data); + } + + function testRepayingLoans(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 1000, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // Take out a loan + + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + bytes memory adaptorData = abi.encode(wethUsdcMarket); + + // start repayment sequence - NOTE that the repay function in Morpho Blue calls accrue interest within it. + deal(address(USDC), address(cellar), borrowAmount); + + // Repay the loan. + adaptorCalls[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + uint256 newBalance = morphoBlueDebtAdaptor.balanceOf(adaptorData); + + assertApproxEqAbs(newBalance, 0, 1, "Cellar should have zero debt recorded within Morpho Blue Market"); + assertEq(USDC.balanceOf(address(cellar)), 0, "Cellar should have zero debtAsset"); + } + + // ensuring that zero as an input param will revert in various scenarios (due to INCONSISTENT_INPUT error within MorphoBlue (it doesn't allow more than one zero input param) for respective function calls). + function testRepayingLoansWithZeroInput(uint256 assets) external { + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + vm.expectRevert(); + cellar.callOnAdaptor(data); + + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 1000, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // Take out a loan + + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // Repay the loan + adaptorCalls[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, 0); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + + vm.expectRevert(bytes("inconsistent input")); + cellar.callOnAdaptor(data); + } + + function testRepayPartialDebt(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 1000, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // Take out a loan + + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + bytes memory adaptorData = abi.encode(wethUsdcMarket); + vm.prank(address(cellar)); + uint256 debtBefore = morphoBlueDebtAdaptor.balanceOf(adaptorData); + + // Repay the loan + adaptorCalls[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, borrowAmount / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + vm.prank(address(cellar)); + uint256 debtNow = morphoBlueDebtAdaptor.balanceOf(adaptorData); + assertLt(debtNow, debtBefore); + assertApproxEqAbs( + USDC.balanceOf(address(cellar)), + borrowAmount / 2, + 1e18, + "Cellar should have approximately half debtAsset" + ); + } + + // This check stops strategists from taking on any debt in positions they do not set up properly. + // Try sending out adaptorCalls that has a call with an position that is unregistered within the cellar, should lead to a revert from the adaptor that is trusted. + function testNestedAdaptorCallLoanInUntrackedPosition(uint256 assets) external { + // purposely do not trust a debt position with WBTC with the cellar + cellar.addPositionToCatalogue(morphoBlueCollateralWBTCPosition); // decimals is 8 for wbtc + cellar.addPosition(5, morphoBlueCollateralWBTCPosition, abi.encode(0), false); + assets = bound(assets, 0.1e8, 100e8); + uint256 MBWbtcUsdcBorrowAmount = priceRouter.getValue(WBTC, assets / 2, USDC); // assume wbtcUsdcMarketId corresponds to a wbtc-usdc market on morpho blue + + deal(address(WBTC), address(cellar), assets); + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](2); + bytes[] memory adaptorCallsFirstAdaptor = new bytes[](1); // collateralAdaptor + bytes[] memory adaptorCallsSecondAdaptor = new bytes[](1); // debtAdaptor + adaptorCallsFirstAdaptor[0] = _createBytesDataToAddCollateralToMorphoBlue(wbtcUsdcMarket, assets); + adaptorCallsSecondAdaptor[0] = _createBytesDataToBorrowFromMorphoBlue(wbtcUsdcMarket, MBWbtcUsdcBorrowAmount); + data[0] = Cellar.AdaptorCall({ + adaptor: address(morphoBlueCollateralAdaptor), + callData: adaptorCallsFirstAdaptor + }); + data[1] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCallsSecondAdaptor }); + vm.expectRevert( + bytes( + abi.encodeWithSelector( + MorphoBlueDebtAdaptor.MorphoBlueDebtAdaptor__MarketPositionsMustBeTracked.selector, + wbtcUsdcMarket + ) + ) + ); + cellar.callOnAdaptor(data); + } + + // have strategist call repay function when no debt owed. Expect revert. + function testRepayingDebtThatIsNotOwed(uint256 assets) external { + assets = bound(assets, 0.1e18, 100e18); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + + adaptorCalls[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + vm.expectRevert(bytes("inconsistent input")); + cellar.callOnAdaptor(data); + } + + /// MorphoBlueDebtAdaptor AND MorphoBlueCollateralAdaptor tests + + // Check that multiple morpho blue positions are handled properly + function testMultipleMorphoBluePositions(uint256 assets) external { + assets = bound(assets, 0.1e18, 100e18); + + // Add new assets positions to cellar + cellar.addPositionToCatalogue(morphoBlueCollateralWBTCPosition); + cellar.addPositionToCatalogue(morphoBlueDebtWBTCPosition); + cellar.addPosition(5, morphoBlueCollateralWBTCPosition, abi.encode(0), false); + cellar.addPosition(1, morphoBlueDebtWBTCPosition, abi.encode(0), true); + + cellar.setHoldingPosition(morphoBlueCollateralWETHPosition); + + // multiple adaptor calls + // deposit WETH + // borrow USDC from weth:usdc morpho blue market + // deposit WBTC + // borrow USDC from wbtc:usdc morpho blue market + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); // holding position == collateralPosition w/ MB wethUsdcMarket + + uint256 wbtcAssets = assets.changeDecimals(18, 8); + deal(address(WBTC), address(cellar), wbtcAssets); + uint256 wethUSDCToBorrow = priceRouter.getValue(WETH, assets / 2, USDC); + uint256 wbtcUSDCToBorrow = priceRouter.getValue(WBTC, wbtcAssets / 2, USDC); + + // Supply markets so we can test borrowing from cellar with multiple positions + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WBTC, assets * 1000, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount * 4); + USDC.safeApprove(address(morphoBlue), supplyAmount * 4); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + morphoBlue.supply(wbtcUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + + vm.stopPrank(); + + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](2); + bytes[] memory adaptorCallsFirstAdaptor = new bytes[](1); // collateralAdaptor, MKR already deposited due to cellar holding position + bytes[] memory adaptorCallsSecondAdaptor = new bytes[](2); // debtAdaptor + adaptorCallsFirstAdaptor[0] = _createBytesDataToAddCollateralToMorphoBlue(wbtcUsdcMarket, wbtcAssets); + adaptorCallsSecondAdaptor[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, wethUSDCToBorrow); + adaptorCallsSecondAdaptor[1] = _createBytesDataToBorrowFromMorphoBlue(wbtcUsdcMarket, wbtcUSDCToBorrow); + data[0] = Cellar.AdaptorCall({ + adaptor: address(morphoBlueCollateralAdaptor), + callData: adaptorCallsFirstAdaptor + }); + data[1] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCallsSecondAdaptor }); + cellar.callOnAdaptor(data); + + // Check that we have the right amount of USDC borrowed + assertApproxEqAbs( + (getMorphoBlueDebtBalance(wethUsdcMarketId, address(cellar))) + + getMorphoBlueDebtBalance(wbtcUsdcMarketId, address(cellar)), + wethUSDCToBorrow + wbtcUSDCToBorrow, + 1 + ); + + assertApproxEqAbs(USDC.balanceOf(address(cellar)), wethUSDCToBorrow + wbtcUSDCToBorrow, 1); + + uint256 maxAmountToRepay = type(uint256).max; // set up repayment amount to be cellar's total USDC. + deal(address(USDC), address(cellar), (wethUSDCToBorrow + wbtcUSDCToBorrow) * 2); + + // Repay the loan in one of the morpho blue markets + Cellar.AdaptorCall[] memory newData2 = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls2 = new bytes[](1); + adaptorCalls2[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, maxAmountToRepay); + newData2[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls2 }); + cellar.callOnAdaptor(newData2); + + assertApproxEqAbs( + getMorphoBlueDebtBalance(wethUsdcMarketId, address(cellar)), + 0, + 1, + "Cellar should have zero debt recorded within Morpho Blue Market" + ); + + assertApproxEqAbs( + getMorphoBlueDebtBalance(wbtcUsdcMarketId, address(cellar)), + wbtcUSDCToBorrow, + 1, + "Cellar should still have debt for WBTC Morpho Blue Market" + ); + + assertApproxEqAbs( + USDC.balanceOf(address(cellar)), + wethUSDCToBorrow + (2 * wbtcUSDCToBorrow), + 1, + "Cellar should have paid off debt w/ type(uint256).max but not have paid more than needed." + ); + + deal(address(WETH), address(cellar), 0); + + adaptorCalls2[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, assets); + newData2[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls2 }); + cellar.callOnAdaptor(newData2); + + // Check that we no longer have any WETH in the collateralPosition + assertEq(WETH.balanceOf(address(cellar)), assets); + + // have user withdraw from cellar + cellar.withdraw(assets, address(this), address(this)); + assertEq(WETH.balanceOf(address(this)), assets); + } + + // Test removal of collateral but with taking a loan out and repaying it in full first. + function testRemoveCollateralWithTypeUINT256MaxAfterRepay(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 10, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // Take out a loan + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + bytes memory adaptorData = abi.encode(wethUsdcMarket); + + vm.prank(address(cellar)); + + // start repayment sequence - NOTE that the repay function in Morpho Blue calls accrue interest within it. + uint256 maxAmountToRepay = type(uint256).max; // set up repayment amount to be cellar's total loanToken + deal(address(USDC), address(cellar), borrowAmount); + + // Repay the loan. + adaptorCalls[0] = _createBytesDataToRepayDebtToMorphoBlue(wethUsdcMarket, maxAmountToRepay); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + uint256 newBalance = morphoBlueDebtAdaptor.balanceOf(adaptorData); + + assertApproxEqAbs(newBalance, 0, 1, "Cellar should have zero debt recorded within Morpho Blue Market"); + assertEq(USDC.balanceOf(address(cellar)), 0, "Cellar should have zero debtAsset"); + + // no collateral interest or anything has accrued, should be able to withdraw everything and have nothing left in it. + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + + assertEq(WETH.balanceOf(address(cellar)), assets + initialAssets); + assertEq(newCellarCollateralBalance, 0); + } + + // test attempting to removeCollateral() when the LTV would be too high as a result + function testFailRemoveCollateralBecauseLTV(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 10, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // Take out a loan + uint256 borrowAmount = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // try to removeCollateral but more than should be allowed + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + MorphoBlueCollateralAdaptor.MorphoBlueCollateralAdaptor__HealthFactorTooLow.selector, + wethUsdcMarket + ) + ) + ); + cellar.callOnAdaptor(data); + + adaptorCalls[0] = _createBytesDataToRemoveCollateralToMorphoBlue(wethUsdcMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + MorphoBlueCollateralAdaptor.MorphoBlueCollateralAdaptor__HealthFactorTooLow.selector, + wethUsdcMarket + ) + ) + ); + cellar.callOnAdaptor(data); + } + + function testLTV(uint256 assets) external { + assets = bound(assets, 1e18, 100e18); + initialAssets = cellar.totalAssets(); + deal(address(WETH), address(this), assets); + cellar.deposit(assets, address(this)); + + // carry out a proper addCollateral() call + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + vm.startPrank(SUPPLIER); // SUPPLIER + uint256 supplyAmount = priceRouter.getValue(WETH, assets * 10, USDC); // assumes that wethUsdcMarketId is a WETH:USDC market. Correct this if otherwise. + + deal(address(USDC), SUPPLIER, supplyAmount); + USDC.safeApprove(address(morphoBlue), supplyAmount); + morphoBlue.supply(wethUsdcMarket, supplyAmount, 0, SUPPLIER, hex""); + vm.stopPrank(); + + // Take out a loan + uint256 borrowAmount = priceRouter.getValue(WETH, assets.mulDivDown(1e4, 1.05e4), USDC); // calculate a borrow amount that would make the position unhealthy (health factor wise) + + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, borrowAmount); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + vm.expectRevert(bytes("insufficient collateral")); + cellar.callOnAdaptor(data); + + // add collateral to be able to borrow amount desired + deal(address(WETH), address(cellar), 3 * assets); + adaptorCalls[0] = _createBytesDataToAddCollateralToMorphoBlue(wethUsdcMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueCollateralAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + assertEq(WETH.balanceOf(address(cellar)), assets * 2); + + uint256 newCellarCollateralBalance = uint256(morphoBlue.position(wethUsdcMarketId, address(cellar)).collateral); + assertEq(newCellarCollateralBalance, 2 * assets); + + // Try taking out more USDC now + uint256 moreUSDCToBorrow = priceRouter.getValue(WETH, assets / 2, USDC); + adaptorCalls[0] = _createBytesDataToBorrowFromMorphoBlue(wethUsdcMarket, moreUSDCToBorrow); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueDebtAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); // should transact now + } + + /// MorphoBlue Collateral and Debt Specific Helpers + + // NOTE - make sure to call `accrueInterest()` beforehand to ensure we get proper debt balance returned + function getMorphoBlueDebtBalance(Id _id, address _user) internal view returns (uint256) { + Market memory market = morphoBlue.market(_id); + return (((morphoBlue.borrowShares(_id, _user))).toAssetsUp(market.totalBorrowAssets, market.totalBorrowShares)); + } + + // NOTE - make sure to call `accrueInterest()` beforehand to ensure we get proper supply balance returned + function getMorphoBlueSupplyBalance(Id _id, address _user) internal view returns (uint256) { + Market memory market = morphoBlue.market(_id); + return ( + uint256((morphoBlue.position(_id, _user).supplyShares)).toAssetsUp( + market.totalSupplyAssets, + market.totalSupplyShares + ) + ); + } +} diff --git a/test/testAdaptors/MorphoBlue/MorphoBlueSupplyAdaptor.t.sol b/test/testAdaptors/MorphoBlue/MorphoBlueSupplyAdaptor.t.sol new file mode 100644 index 000000000..8838f8039 --- /dev/null +++ b/test/testAdaptors/MorphoBlue/MorphoBlueSupplyAdaptor.t.sol @@ -0,0 +1,736 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { MockDataFeedForMorphoBlue } from "src/mocks/MockDataFeedForMorphoBlue.sol"; +import { MorphoBlueSupplyAdaptor } from "src/modules/adaptors/Morpho/MorphoBlue/MorphoBlueSupplyAdaptor.sol"; +import { IMorpho, MarketParams, Id, Market } from "src/interfaces/external/Morpho/MorphoBlue/interfaces/IMorpho.sol"; +import { SharesMathLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/SharesMathLib.sol"; +import { AdaptorHelperFunctions } from "test/resources/AdaptorHelperFunctions.sol"; +import { MarketParamsLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/MarketParamsLib.sol"; +import { MorphoLib } from "src/interfaces/external/Morpho/MorphoBlue/libraries/periphery/MorphoLib.sol"; +import { IrmMock } from "src/mocks/IrmMock.sol"; +import "test/resources/MainnetStarter.t.sol"; + +contract MorphoBlueSupplyAdaptorTest is MainnetStarterTest, AdaptorHelperFunctions { + using SafeTransferLib for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + using Address for address; + using MarketParamsLib for MarketParams; + using SharesMathLib for uint256; + using MorphoLib for IMorpho; + + MorphoBlueSupplyAdaptor public morphoBlueSupplyAdaptor; + + Cellar private cellar; + + // Chainlink PriceFeeds + MockDataFeedForMorphoBlue private mockWethUsd; + MockDataFeedForMorphoBlue private mockUsdcUsd; + MockDataFeedForMorphoBlue private mockWbtcUsd; + MockDataFeedForMorphoBlue private mockDaiUsd; + + uint32 private wethPosition = 1; + uint32 private usdcPosition = 2; + uint32 private wbtcPosition = 3; + uint32 private daiPosition = 4; + + uint32 public morphoBlueSupplyWETHPosition = 1_000_001; + uint32 public morphoBlueSupplyUSDCPosition = 1_000_002; + uint32 public morphoBlueSupplyWBTCPosition = 1_000_003; + + address private whaleBorrower = vm.addr(777); + + IMorpho public morphoBlue = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + address public morphoBlueOwner = 0x6ABfd6139c7C3CC270ee2Ce132E309F59cAaF6a2; + address public DEFAULT_IRM = 0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC; + uint256 public DEFAULT_LLTV = 860000000000000000; // (86% LLTV) + + MarketParams private wethUsdcMarket; + MarketParams private wbtcUsdcMarket; + MarketParams private usdcDaiMarket; + MarketParams private UNTRUSTED_mbFakeMarket; + Id private wethUsdcMarketId; + Id private wbtcUsdcMarketId; + Id private usdcDaiMarketId; + // Id private UNTRUSTED_mbFakeMarket = Id.wrap(bytes32(abi.encode(1_000_009))); + + uint256 initialAssets; + uint256 initialLend; + IrmMock internal irm; + address public FEE_RECIPIENT = address(9000); + + function setUp() external { + // Setup forked environment. + string memory rpcKey = "MAINNET_RPC_URL"; + uint256 blockNumber = 18922158; + _startFork(rpcKey, blockNumber); + + // Run Starter setUp code. + _setUp(); + + mockUsdcUsd = new MockDataFeedForMorphoBlue(USDC_USD_FEED); + mockWbtcUsd = new MockDataFeedForMorphoBlue(WBTC_USD_FEED); + mockWethUsd = new MockDataFeedForMorphoBlue(WETH_USD_FEED); + mockDaiUsd = new MockDataFeedForMorphoBlue(DAI_USD_FEED); + + bytes memory creationCode; + bytes memory constructorArgs; + + creationCode = type(MorphoBlueSupplyAdaptor).creationCode; + constructorArgs = abi.encode(address(morphoBlue)); + morphoBlueSupplyAdaptor = MorphoBlueSupplyAdaptor( + deployer.deployContract("Morpho Blue Supply Adaptor V 0.0", creationCode, constructorArgs, 0) + ); + PriceRouter.ChainlinkDerivativeStorage memory stor; + + PriceRouter.AssetSettings memory settings; + + uint256 price = uint256(mockWethUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWethUsd)); + priceRouter.addAsset(WETH, settings, abi.encode(stor), price); + + price = uint256(mockUsdcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockUsdcUsd)); + priceRouter.addAsset(USDC, settings, abi.encode(stor), price); + + price = uint256(mockWbtcUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockWbtcUsd)); + priceRouter.addAsset(WBTC, settings, abi.encode(stor), price); + + price = uint256(mockDaiUsd.latestAnswer()); + settings = PriceRouter.AssetSettings(CHAINLINK_DERIVATIVE, address(mockDaiUsd)); + priceRouter.addAsset(DAI, settings, abi.encode(stor), price); + + // set mock prices for chainlink price feeds, but add in params to adjust the morphoBlue price format needed --> recall from IOracle.sol (from Morpho Blue repo) that the units will be 10 ** (36 - collateralUnits + borrowUnits). + + mockWethUsd.setMockAnswer(2200e8, WETH, USDC); + mockUsdcUsd.setMockAnswer(1e8, USDC, USDC); + mockWbtcUsd.setMockAnswer(42000e8, WBTC, USDC); + mockDaiUsd.setMockAnswer(1e8, DAI, USDC); + + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(morphoBlueSupplyAdaptor)); + + registry.trustPosition(wethPosition, address(erc20Adaptor), abi.encode(WETH)); + registry.trustPosition(usdcPosition, address(erc20Adaptor), abi.encode(USDC)); + registry.trustPosition(wbtcPosition, address(erc20Adaptor), abi.encode(WBTC)); + registry.trustPosition(daiPosition, address(erc20Adaptor), abi.encode(DAI)); + + // We will work with a mock IRM similar to tests within Morpho Blue repo. + + irm = new IrmMock(); + + vm.startPrank(morphoBlueOwner); + morphoBlue.enableIrm(address(irm)); + morphoBlue.setFeeRecipient(FEE_RECIPIENT); + vm.stopPrank(); + + wethUsdcMarket = MarketParams({ + loanToken: address(USDC), + collateralToken: address(WETH), + oracle: address(mockWethUsd), + irm: address(irm), + lltv: DEFAULT_LLTV + }); + + // setup morphoBlue WBTC:USDC market + wbtcUsdcMarket = MarketParams({ + loanToken: address(USDC), + collateralToken: address(WBTC), + oracle: address(mockWbtcUsd), + irm: address(irm), + lltv: DEFAULT_LLTV + }); + + // setup morphoBlue USDC:DAI market + usdcDaiMarket = MarketParams({ + loanToken: address(USDC), + collateralToken: address(DAI), + oracle: address(mockUsdcUsd), + irm: address(irm), + lltv: DEFAULT_LLTV + }); + + morphoBlue.createMarket(wethUsdcMarket); + wethUsdcMarketId = wethUsdcMarket.id(); + + morphoBlue.createMarket(wbtcUsdcMarket); + wbtcUsdcMarketId = wbtcUsdcMarket.id(); + + morphoBlue.createMarket(usdcDaiMarket); + usdcDaiMarketId = usdcDaiMarket.id(); + + registry.trustPosition( + morphoBlueSupplyWETHPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(wethUsdcMarket) + ); + registry.trustPosition( + morphoBlueSupplyUSDCPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(usdcDaiMarket) + ); + registry.trustPosition( + morphoBlueSupplyWBTCPosition, + address(morphoBlueSupplyAdaptor), + abi.encode(wbtcUsdcMarket) + ); + + string memory cellarName = "Morpho Blue Supply Cellar V0.0"; + uint256 initialDeposit = 1e6; + uint64 platformCut = 0.75e18; + + // Approve new cellar to spend assets. + address cellarAddress = deployer.getAddress(cellarName); + deal(address(USDC), address(this), initialDeposit); + USDC.approve(cellarAddress, initialDeposit); + + creationCode = type(Cellar).creationCode; + constructorArgs = abi.encode( + address(this), + registry, + USDC, + cellarName, + cellarName, + usdcPosition, + abi.encode(true), + initialDeposit, + platformCut, + type(uint192).max + ); + + cellar = Cellar(deployer.deployContract(cellarName, creationCode, constructorArgs, 0)); + + cellar.addAdaptorToCatalogue(address(morphoBlueSupplyAdaptor)); + + cellar.addPositionToCatalogue(wethPosition); + cellar.addPositionToCatalogue(wbtcPosition); + + // only add USDC supply position for now. + cellar.addPositionToCatalogue(morphoBlueSupplyUSDCPosition); + + cellar.addPosition(1, wethPosition, abi.encode(true), false); + cellar.addPosition(2, wbtcPosition, abi.encode(true), false); + cellar.addPosition(3, morphoBlueSupplyUSDCPosition, abi.encode(true), false); + + cellar.setHoldingPosition(morphoBlueSupplyUSDCPosition); + + WETH.safeApprove(address(cellar), type(uint256).max); + USDC.safeApprove(address(cellar), type(uint256).max); + WBTC.safeApprove(address(cellar), type(uint256).max); + + initialAssets = cellar.totalAssets(); + + // tests that adaptor call for lending works when holding position is a position with morphoBlueSupplyAdaptor + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + // Lend USDC on Morpho Blue. Use the initial deposit that is in the cellar to begin with. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(usdcDaiMarket, initialDeposit); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + + initialLend = _userSupplyBalance(usdcDaiMarketId, address(cellar)); + assertEq( + initialLend, + initialAssets, + "Should be equal as the test setup includes lending initialDeposit of USDC into Morpho Blue" + ); + } + + // Throughout all tests, setup() has supply usdc position fully trusted (cellar and registry), weth and wbtc supply positions trusted w/ registry. mbsupplyusdc position is holding position. + + function testDeposit(uint256 assets) external { + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + assertEq(USDC.balanceOf(address(cellar)), 0, "testDeposit: all assets should have been supplied to MB market."); + assertApproxEqAbs( + _userSupplyBalance(usdcDaiMarketId, address(cellar)), + assets + initialAssets, + 1, + "testDeposit: all assets should have been supplied to MB market." + ); + } + + function testWithdraw(uint256 assets) external { + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + USDC.safeApprove(address(cellar), type(uint256).max); + cellar.withdraw(assets / 2, address(this), address(this)); + + assertEq( + USDC.balanceOf(address(this)), + assets / 2, + "testWithdraw: half of assets should have been withdrawn to cellar." + ); + assertApproxEqAbs( + _userSupplyBalance(usdcDaiMarketId, address(cellar)), + (assets / 2) + initialAssets, + 1, + "testDeposit: half of assets from cellar should remain in MB market." + ); + cellar.withdraw((assets / 2), address(this), address(this)); // NOTE - initialAssets is actually originally from the deployer. + } + + function testTotalAssets(uint256 assets) external { + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + assertEq( + cellar.totalAssets(), + assets + initialAssets, + "testTotalAssets: Total assets MUST equal assets deposited + initialAssets." + ); + } + + function testStrategistLendingUSDC(uint256 assets) external { + cellar.setHoldingPosition(usdcPosition); // set holding position back to erc20Position + + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist rebalances to lend USDC. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + // Lend USDC on Morpho Blue. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(usdcDaiMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + + uint256 newSupplyBalance = _userSupplyBalance(usdcDaiMarketId, address(cellar)); + // check supply share balance for cellar has increased. + assertGt(newSupplyBalance, initialLend, "Cellar should have supplied more USDC to MB market"); + assertEq(newSupplyBalance, assets + initialAssets, "Rebalance should have lent all USDC on Morpho Blue."); + } + + function testBalanceOfCalculationMethods(uint256 assets) external { + cellar.setHoldingPosition(usdcPosition); // set holding position back to erc20Position + + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist rebalances to lend USDC. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + // Lend USDC on Morpho Blue. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(usdcDaiMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + + uint256 newSupplyBalanceAccToMBLib = _userSupplyBalance(usdcDaiMarketId, address(cellar)); + uint256 supplyBalanceDirectFromMorphoBlue = uint256( + (morphoBlue.position(usdcDaiMarketId, address(cellar)).supplyShares).toAssetsDown( + uint256(morphoBlue.market(usdcDaiMarketId).totalSupplyAssets), + uint256(morphoBlue.market(usdcDaiMarketId).totalSupplyShares) + ) + ); + vm.startPrank(address(cellar)); + bytes memory adaptorData = abi.encode(usdcDaiMarket); + + uint256 balanceOfAccToSupplyAdaptor = morphoBlueSupplyAdaptor.balanceOf(adaptorData); + + assertEq( + balanceOfAccToSupplyAdaptor, + supplyBalanceDirectFromMorphoBlue, + "balanceOf() should report same amount as morpho blue as long interest has been accrued beforehand." + ); + assertEq( + newSupplyBalanceAccToMBLib, + supplyBalanceDirectFromMorphoBlue, + "Checking that helper _userSupplyBalance() reports proper supply balances as long as interest has been accrued beforehand." + ); + vm.stopPrank(); + } + + // w/ holdingPosition as morphoBlueSupplyUSDC, we make sure that strategists can lend to the holding position outright. ie.) some airdropped assets were swapped to USDC to use in morpho blue. + function testStrategistLendWithHoldingPosition(uint256 assets) external { + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(cellar), assets); + + // Strategist rebalances to lend USDC. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + // Lend USDC on Morpho Blue. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(usdcDaiMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + + uint256 newSupplyBalance = _userSupplyBalance(usdcDaiMarketId, address(cellar)); + assertEq(newSupplyBalance, assets + initialAssets, "Rebalance should have lent all USDC on Morpho Blue."); + } + + function testStrategistWithdrawing(uint256 assets) external { + // Have user deposit into cellar. + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist rebalances to withdraw. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromMorphoBlue(usdcDaiMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + assertEq( + USDC.balanceOf(address(cellar)), + assets + initialAssets, + "Cellar USDC should have been withdrawn from Morpho Blue Market." + ); + } + + // lend assets into holdingPosition (morphoSupplyUSDCPosition, and then withdraw the USDC from it and lend it into a new market, wethUsdcMarketId (a different morpho blue usdc market)) + function testRebalancingBetweenPairs(uint256 assets) external { + // Add another Morpho Blue Market to cellar + cellar.addPositionToCatalogue(morphoBlueSupplyWETHPosition); + cellar.addPosition(4, morphoBlueSupplyWETHPosition, abi.encode(true), false); + + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist rebalances to withdraw, and lend in a different pair. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](2); + // Withdraw USDC from MB market + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromMorphoBlue(usdcDaiMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(wethUsdcMarket, type(uint256).max); + data[1] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + uint256 newSupplyBalance = _userSupplyBalance(wethUsdcMarketId, address(cellar)); + + assertApproxEqAbs( + newSupplyBalance, + assets + initialAssets, + 2, + "Rebalance should have lent all USDC on new Morpho Blue WETH:USDC market." + ); + + // Withdraw half the assets + data = new Cellar.AdaptorCall[](1); + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromMorphoBlue(wethUsdcMarket, assets / 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + assertEq( + USDC.balanceOf(address(cellar)), + assets / 2, + "Should have withdrawn half the assets from MB Market wethUsdcMarketId." + ); + + newSupplyBalance = _userSupplyBalance(wethUsdcMarketId, address(cellar)); + assertApproxEqAbs( + newSupplyBalance, + (assets / 2) + initialAssets, + 2, + "Rebalance should have led to some assets withdrawn from MB Market wethUsdcMarketId." + ); + } + + function testUsingMarketNotSetupAsPosition(uint256 assets) external { + cellar.setHoldingPosition(usdcPosition); // set holding position back to erc20Position + + // Have user deposit into cellar. + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Strategist rebalances to lend USDC but with an untrusted market. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(UNTRUSTED_mbFakeMarket, assets); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + vm.expectRevert( + bytes( + abi.encodeWithSelector( + MorphoBlueSupplyAdaptor.MorphoBlueSupplyAdaptor__MarketPositionsMustBeTracked.selector, + (UNTRUSTED_mbFakeMarket) + ) + ) + ); + cellar.callOnAdaptor(data); + } + + // Check that loanToken in multiple different pairs is correctly accounted for in totalAssets(). + function testMultiplePositionsTotalAssets(uint256 assets) external { + // Have user deposit into cellar + assets = bound(assets, 0.01e6, 100_000_000e6); + uint256 dividedAssetPerMultiPair = assets / 3; // amount of loanToken (where we've made it the same one for these tests) to distribute between three different morpho blue markets + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Test that users can withdraw from multiple pairs at once. + _setupMultiplePositions(dividedAssetPerMultiPair); + + assertApproxEqAbs( + assets + initialAssets, + cellar.totalAssets(), + 2, + "Total assets should have been lent out and are accounted for via MorphoBlueSupplyAdaptor positions." + ); + + assertApproxEqAbs( + _userSupplyBalance(usdcDaiMarketId, address(cellar)), + dividedAssetPerMultiPair + initialAssets, + 2, + "testMultiplePositionsTotalAssets: cellar should have assets supplied to usdcDaiMarketId." + ); + assertApproxEqAbs( + _userSupplyBalance(wethUsdcMarketId, address(cellar)), + dividedAssetPerMultiPair, + 2, + "testMultiplePositionsTotalAssets: cellar should have assets supplied to wethUsdcMarket." + ); + assertApproxEqAbs( + _userSupplyBalance(wbtcUsdcMarketId, address(cellar)), + dividedAssetPerMultiPair, + 2, + "testMultiplePositionsTotalAssets: cellar should have assets supplied to wbtcUsdcMarketId." + ); + } + + // Check that user able to withdraw from multiple lending positions outright + function testMultiplePositionsUserWithdraw(uint256 assets) external { + // Have user deposit into cellar + assets = bound(assets, 0.01e6, 100_000_000e6); + uint256 dividedAssetPerMultiPair = assets / 3; // amount of loanToken to distribute between different markets + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Test that users can withdraw from multiple pairs at once. + _setupMultiplePositions(dividedAssetPerMultiPair); + + deal(address(USDC), address(this), 0); + uint256 withdrawAmount = cellar.maxWithdraw(address(this)); + cellar.withdraw(withdrawAmount, address(this), address(this)); + + assertApproxEqAbs( + USDC.balanceOf(address(this)), + withdrawAmount, + 1, + "User should have gotten all their USDC (minus some dust)" + ); + assertEq( + USDC.balanceOf(address(this)), + withdrawAmount, + "User should have gotten all their USDC (minus some dust)" + ); + } + + function testWithdrawableFrom() external { + cellar.addPositionToCatalogue(morphoBlueSupplyWETHPosition); + cellar.addPosition(4, morphoBlueSupplyWETHPosition, abi.encode(true), false); + // Strategist rebalances to withdraw USDC, and lend in a different pair. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](2); + // Withdraw USDC from Morpho Blue. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromMorphoBlue(usdcDaiMarket, type(uint256).max); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(wethUsdcMarket, type(uint256).max); + data[1] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + // Make cellar deposits lend USDC into WETH Pair by default + cellar.setHoldingPosition(morphoBlueSupplyWETHPosition); + uint256 assets = 10_000e6; + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + // Figure out how much the whale must borrow to borrow all the loanToken. + uint256 totalLoanTokenSupplied = uint256(morphoBlue.market(wethUsdcMarketId).totalSupplyAssets); + uint256 totalLoanTokenBorrowed = uint256(morphoBlue.market(wethUsdcMarketId).totalBorrowAssets); + uint256 assetsToBorrow = totalLoanTokenSupplied > totalLoanTokenBorrowed + ? totalLoanTokenSupplied - totalLoanTokenBorrowed + : 0; + // Supply 2x the value we are trying to borrow in weth market collateral (WETH) + uint256 collateralToProvide = priceRouter.getValue(USDC, 2 * assetsToBorrow, WETH); + deal(address(WETH), whaleBorrower, collateralToProvide); + vm.startPrank(whaleBorrower); + WETH.approve(address(morphoBlue), collateralToProvide); + MarketParams memory market = morphoBlue.idToMarketParams(wethUsdcMarketId); + morphoBlue.supplyCollateral(market, collateralToProvide, whaleBorrower, hex""); + // now borrow + morphoBlue.borrow(market, assetsToBorrow, 0, whaleBorrower, whaleBorrower); + vm.stopPrank(); + uint256 assetsWithdrawable = cellar.totalAssetsWithdrawable(); + assertEq(assetsWithdrawable, 0, "There should be no assets withdrawable."); + // Whale repays half of their debt. + uint256 sharesToRepay = (morphoBlue.position(wethUsdcMarketId, whaleBorrower).borrowShares) / 2; + vm.startPrank(whaleBorrower); + USDC.approve(address(morphoBlue), assetsToBorrow); + morphoBlue.repay(market, 0, sharesToRepay, whaleBorrower, hex""); + vm.stopPrank(); + uint256 totalLoanTokenSupplied2 = uint256(morphoBlue.market(wethUsdcMarketId).totalSupplyAssets); + uint256 totalLoanTokenBorrowed2 = uint256(morphoBlue.market(wethUsdcMarketId).totalBorrowAssets); + uint256 liquidLoanToken2 = totalLoanTokenSupplied2 - totalLoanTokenBorrowed2; + assetsWithdrawable = cellar.totalAssetsWithdrawable(); + assertEq(assetsWithdrawable, liquidLoanToken2, "Should be able to withdraw liquid loanToken."); + // Have user withdraw the loanToken. + deal(address(USDC), address(this), 0); + cellar.withdraw(liquidLoanToken2, address(this), address(this)); + assertEq(USDC.balanceOf(address(this)), liquidLoanToken2, "User should have received liquid loanToken."); + } + + // NOTE - This fuzz test has larger bounds compared to the other fuzz tests because the IRM used within these tests paired w/ the test market conditions means we either have to skip large amounts of time or work with large amounts of fuzz bounds. When the fuzz bounds are the other ones we used before, this test reverts w/ Cellar__TotalAssetDeviatedOutsideRange when we skip 1 day or more, and it doesn't seem to show accrued interest when skipping less than that. The irm shows borrowRate changes though based on utilization as per the mockIrm setup. + function testAccrueInterest(uint256 assets) external { + assets = bound(assets, 1_000e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + uint256 balance1 = (_userSupplyBalance(usdcDaiMarketId, address(cellar))); + + skip(1 days); + mockUsdcUsd.setMockUpdatedAt(block.timestamp); + mockDaiUsd.setMockUpdatedAt(block.timestamp); + + // Strategist rebalances to accrue interest in markets + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAccrueInterestToMorphoBlueSupplyAdaptor(usdcDaiMarket); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + uint256 balance2 = (_userSupplyBalance(usdcDaiMarketId, address(cellar))); + + assertEq(balance2, balance1, "No interest accrued since no loans were taken out."); + + // provide collateral + uint256 collateralToProvide = priceRouter.getValue(USDC, 2 * assets, DAI); + deal(address(DAI), whaleBorrower, collateralToProvide); + vm.startPrank(whaleBorrower); + DAI.approve(address(morphoBlue), collateralToProvide); + MarketParams memory market = morphoBlue.idToMarketParams(usdcDaiMarketId); + morphoBlue.supplyCollateral(market, collateralToProvide, whaleBorrower, hex""); + + // now borrow + morphoBlue.borrow(market, assets / 5, 0, whaleBorrower, whaleBorrower); + vm.stopPrank(); + + skip(1 days); + + mockUsdcUsd.setMockUpdatedAt(block.timestamp); + mockDaiUsd.setMockUpdatedAt(block.timestamp); + + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAccrueInterestToMorphoBlueSupplyAdaptor(usdcDaiMarket); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + cellar.callOnAdaptor(data); + + uint256 balance3 = (_userSupplyBalance(usdcDaiMarketId, address(cellar))); + + assertGt(balance3, balance2, "Supplied loanAsset into MorphoBlue should have accrued interest."); + } + + function testWithdrawWhenIlliquid(uint256 assets) external { + assets = bound(assets, 0.01e6, 100_000_000e6); + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + // Check logic in the withdraw function by having strategist call withdraw, passing in isLiquid = false. + bool isLiquid = false; + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = abi.encodeWithSelector( + MorphoBlueSupplyAdaptor.withdraw.selector, + assets, + address(this), + abi.encode(usdcDaiMarket), + abi.encode(isLiquid) + ); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + vm.expectRevert(bytes(abi.encodeWithSelector(BaseAdaptor.BaseAdaptor__UserWithdrawsNotAllowed.selector))); + cellar.callOnAdaptor(data); + + vm.startPrank(address(cellar)); + uint256 withdrawableFrom = morphoBlueSupplyAdaptor.withdrawableFrom(abi.encode(0), abi.encode(isLiquid)); + vm.stopPrank(); + + assertEq(withdrawableFrom, 0, "Since it is illiquid it should be zero."); + } + + // ========================================= HELPER FUNCTIONS ========================================= + + // setup multiple lending positions + function _setupMultiplePositions(uint256 dividedAssetPerMultiPair) internal { + // add numerous USDC markets atop of holdingPosition + + cellar.addPositionToCatalogue(morphoBlueSupplyWETHPosition); + cellar.addPositionToCatalogue(morphoBlueSupplyWBTCPosition); + + cellar.addPosition(4, morphoBlueSupplyWETHPosition, abi.encode(true), false); + cellar.addPosition(5, morphoBlueSupplyWBTCPosition, abi.encode(true), false); + + // Strategist rebalances to withdraw set amount of USDC, and lend in a different pair. + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](3); + // Withdraw 2/3 of cellar USDC from one MB market, then redistribute to other MB markets. + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToWithdrawFromMorphoBlue(usdcDaiMarket, dividedAssetPerMultiPair * 2); + data[0] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(wethUsdcMarket, dividedAssetPerMultiPair); + data[1] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + { + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToLendOnMorphoBlue(wbtcUsdcMarket, type(uint256).max); + data[2] = Cellar.AdaptorCall({ adaptor: address(morphoBlueSupplyAdaptor), callData: adaptorCalls }); + } + + cellar.callOnAdaptor(data); + } + + /** + * NOTE: make sure to call `accrueInterest()` on respective market before calling these helpers + */ + function _userSupplyBalance(Id _id, address _user) internal view returns (uint256) { + Market memory market = morphoBlue.market(_id); + // this currently doesn't account for interest, that needs to be done before calling this helper. + return (morphoBlue.supplyShares(_id, _user).toAssetsDown(market.totalSupplyAssets, market.totalSupplyShares)); + } +}