From 2514eecb19e9b7b202b96aa849e3a779a9b3ab9b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 18 Jul 2024 13:31:24 +0200 Subject: [PATCH] Added a Morpho Blue integration (#1094) * Implemented a basic version of the `addLiquidity` circuit breaker * Fixed `test_lp_withdrawal_long_and_short_maturity` * Fixed the remaining tests * add priceDiscoveryCheck to LPMath and also check it in initialize * use initial price * add tests and fix placement of check * remove lib from LPMath * remove comment * remove console import * remove console import * fix price discovery tests * fixed tests * commit test to investigate * add test_solvency_at_0_apr * add test_solvency_cross_checkpoint_long_short * address review feedback * Update test/integrations/hyperdrive/PriceDiscovery.t.sol * Added some testing examples * Minor updates * Updated `verifyPriceDiscovery` to `calculateSolvencyAfterMaxLong` * Cleaned up the tests * Increased the efficiency of the solvency check * Fixed the code size issue * Addressed Saw Mon's comment * Improved one of the price discovery tests * Updated the price discovery tests * Fixed the remaining tests * Addressed review feedback from @mcclurejt * Fixed the deployment scripts * Generated the code for the Aave integration * forge install: aave-v3-core v1.19.1 * Fixed codegen and compiler errors * Implemented `_convertToBase` and `_convertToShares` for AaveHyperdrive * Implemented the deposit functions for the AaveHyperdrive instance * Implemented the withdrawal logic for ATokens * Addressed most of the remaining FIXMEs * Reduced the code-size of the Aave integration * Copied over the instance test * Added a fourth target to fix the codesize issues * Started work on the AaveHyperdrive tests * Fixed some of the tests * Fixed more tests * Fixed the remaining Aave instance tests * Fixed the other instance tests * Addressed remaining FIXMEs and fixed the code generator * Fixed the publish scripts * Lowered the minimum share reserves * Addressed review feedback from @jrhea * Added conversion functions to the public interfaces for instances and deployer coordinators * Added the skeleton for `MorphoBlueHyperdrive` * Sketched out the implementation plan * forge install: morpho-blue v1.0.0 * Made some progress on the Morpho integration * Updated the Morpho Blue integration to use a struct in the constructors * Addressed the remaining FIXMEs in the Morpho Blue integration * Wrote the morpho tests * Updated some tests * "Fixed" the tests * Improved the Morpho Blue tests to add confidence in the integration * Fixed tests * Fixed the stETH tests * Addressed review feedback from @jrhea and @mcclurejt --------- Co-authored-by: jonny rhea Co-authored-by: Jonny Rhea <5555162+jrhea@users.noreply.github.com> --- .gitmodules | 3 + codegen/example/config.yaml | 11 +- .../HyperdriveCoreDeployer.sol.jinja | 12 +- .../templates/instances/Hyperdrive.sol.jinja | 10 +- .../AaveHyperdriveDeployerCoordinator.sol | 2 +- .../LsETHHyperdriveDeployerCoordinator.sol | 5 +- .../MorphoBlueHyperdriveCoreDeployer.sol | 62 ++ ...orphoBlueHyperdriveDeployerCoordinator.sol | 231 ++++++ .../morpho-blue/MorphoBlueTarget0Deployer.sol | 40 + .../morpho-blue/MorphoBlueTarget1Deployer.sol | 40 + .../morpho-blue/MorphoBlueTarget2Deployer.sol | 40 + .../morpho-blue/MorphoBlueTarget3Deployer.sol | 40 + .../morpho-blue/MorphoBlueTarget4Deployer.sol | 40 + contracts/src/instances/lseth/LsETHBase.sol | 4 +- .../instances/morpho-blue/MorphoBlueBase.sol | 223 ++++++ .../morpho-blue/MorphoBlueConversions.sol | 122 +++ .../morpho-blue/MorphoBlueHyperdrive.sol | 91 +++ .../morpho-blue/MorphoBlueTarget0.sol | 63 ++ .../morpho-blue/MorphoBlueTarget1.sol | 26 + .../morpho-blue/MorphoBlueTarget2.sol | 26 + .../morpho-blue/MorphoBlueTarget3.sol | 26 + .../morpho-blue/MorphoBlueTarget4.sol | 26 + .../src/interfaces/IMorphoBlueHyperdrive.sol | 36 + contracts/src/internal/HyperdriveBase.sol | 17 +- contracts/src/libraries/Constants.sol | 6 + lib/morpho-blue | 1 + test/instances/aave/AaveHyperdrive.t.sol | 6 + test/instances/ezETH/EzETHHyperdrive.t.sol | 85 +- test/instances/lseth/LsETHHyperdrive.t.sol | 6 + .../morpho-blue/MorphoBlueHyperdrive.t.sol | 739 ++++++++++++++++++ test/instances/reth/RETHHyperdrive.t.sol | 6 + test/instances/steth/StETHHyperdrive.t.sol | 6 + test/utils/InstanceTest.sol | 180 +++-- 33 files changed, 2098 insertions(+), 133 deletions(-) create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveCoreDeployer.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveDeployerCoordinator.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueTarget0Deployer.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueTarget1Deployer.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueTarget2Deployer.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueTarget3Deployer.sol create mode 100644 contracts/src/deployers/morpho-blue/MorphoBlueTarget4Deployer.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueBase.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueConversions.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueHyperdrive.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueTarget0.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueTarget1.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueTarget2.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueTarget3.sol create mode 100644 contracts/src/instances/morpho-blue/MorphoBlueTarget4.sol create mode 100644 contracts/src/interfaces/IMorphoBlueHyperdrive.sol create mode 160000 lib/morpho-blue create mode 100644 test/instances/morpho-blue/MorphoBlueHyperdrive.t.sol diff --git a/.gitmodules b/.gitmodules index 37cf694b6..e6b310716 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "lib/aave-v3-core"] path = lib/aave-v3-core url = https://github.com/aave/aave-v3-core +[submodule "lib/morpho-blue"] + path = lib/morpho-blue + url = https://github.com/morpho-org/morpho-blue diff --git a/codegen/example/config.yaml b/codegen/example/config.yaml index 1fecb7d0c..29b01bb3a 100644 --- a/codegen/example/config.yaml +++ b/codegen/example/config.yaml @@ -4,22 +4,25 @@ name: # Capitalized version of the name to be used i.e. for contract names, file # names, and comments. - capitalized: "TestETH" + capitalized: "MorphoBlue" + + # All upper case name to be used for constants. + uppercase: "MORPHO_BLUE" # All upper case name to be used for constants. uppercase: "TEST_ETH" # All lower case name to be used for directories. - lowercase: "testeth" + lowercase: "morpho-blue" # Camel case name to be used i.e. for variables. - camelcase: "testEth" + camelcase: "morphoBlue" # Configuration parameters for the hyperdrive instance. contract: # If the contract is payable in Ether. If it is, then logic should be added # to accept Ether, and deposit into the vault to obtain vault shares. - payable: true + payable: false # If the contract can accept base to convert to valut shares on behalf of the # user. diff --git a/codegen/templates/deployers/HyperdriveCoreDeployer.sol.jinja b/codegen/templates/deployers/HyperdriveCoreDeployer.sol.jinja index 28f7856bd..01ccc8c1a 100644 --- a/codegen/templates/deployers/HyperdriveCoreDeployer.sol.jinja +++ b/codegen/templates/deployers/HyperdriveCoreDeployer.sol.jinja @@ -19,6 +19,7 @@ contract {{ name.capitalized }}HyperdriveCoreDeployer is IHyperdriveCoreDeployer /// @param _target1 The target1 address. /// @param _target2 The target2 address. /// @param _target3 The target3 address. + /// @param _target4 The target4 address. /// @param _salt The create2 salt used in the deployment. /// @return The address of the newly deployed {{ name.capitalized }}Hyperdrive instance. function deployHyperdrive( @@ -29,6 +30,7 @@ contract {{ name.capitalized }}HyperdriveCoreDeployer is IHyperdriveCoreDeployer address _target1, address _target2, address _target3, + address _target4, bytes32 _salt ) external returns (address) { return ( @@ -37,7 +39,15 @@ contract {{ name.capitalized }}HyperdriveCoreDeployer is IHyperdriveCoreDeployer // front-running of deployments. new {{ name.capitalized }}Hyperdrive{ salt: keccak256(abi.encode(msg.sender, _salt)) - }(_config, _target0, _target1, _target2, _target3) + }( + __name, + _config, + _target0, + _target1, + _target2, + _target3, + _target4 + ) ) ); } diff --git a/codegen/templates/instances/Hyperdrive.sol.jinja b/codegen/templates/instances/Hyperdrive.sol.jinja index 15498c161..38300fd9a 100644 --- a/codegen/templates/instances/Hyperdrive.sol.jinja +++ b/codegen/templates/instances/Hyperdrive.sol.jinja @@ -74,7 +74,15 @@ contract {{ name.capitalized }}Hyperdrive is Hyperdrive, {{ name.capitalized }}B address _target3, address _target4 ) - Hyperdrive(_config, _target0, _target1, _target2, _target3) + Hyperdrive( + __name, + _config, + _target0, + _target1, + _target2, + _target3, + _target4 + ) { // **************************************************************** // FIXME: Implement this for new instances. ERC4626 example provided. diff --git a/contracts/src/deployers/aave/AaveHyperdriveDeployerCoordinator.sol b/contracts/src/deployers/aave/AaveHyperdriveDeployerCoordinator.sol index ef167a7fc..b94d3e940 100644 --- a/contracts/src/deployers/aave/AaveHyperdriveDeployerCoordinator.sol +++ b/contracts/src/deployers/aave/AaveHyperdriveDeployerCoordinator.sol @@ -201,7 +201,7 @@ contract AaveHyperdriveDeployerCoordinator is /// pool. /// @return The initial vault share price of the Hyperdrive pool. function _getInitialVaultSharePrice( - IHyperdrive.PoolDeployConfig memory _deployConfig, // unused _deployConfig + IHyperdrive.PoolDeployConfig memory _deployConfig, bytes memory // unused _extraData ) internal view override returns (uint256) { // We calculate the vault share price by converting 1e18 vault shares to diff --git a/contracts/src/deployers/lseth/LsETHHyperdriveDeployerCoordinator.sol b/contracts/src/deployers/lseth/LsETHHyperdriveDeployerCoordinator.sol index 8ea49d2d6..92ceecba2 100644 --- a/contracts/src/deployers/lseth/LsETHHyperdriveDeployerCoordinator.sol +++ b/contracts/src/deployers/lseth/LsETHHyperdriveDeployerCoordinator.sol @@ -88,13 +88,12 @@ contract LsETHHyperdriveDeployerCoordinator is uint256 _contribution, IHyperdrive.Options memory _options ) internal override returns (uint256) { - // Depositing as base is disallowed. + // Depositing with base is not supported. if (_options.asBase) { revert IHyperdrive.UnsupportedToken(); } - // Transfer vault shares from the LP and approve the - // Hyperdrive pool. + // Transfer vault shares from the LP and approve the Hyperdrive pool. ERC20(address(river)).safeTransferFrom( _lp, address(this), diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveCoreDeployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveCoreDeployer.sol new file mode 100644 index 000000000..21f481430 --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveCoreDeployer.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveCoreDeployer } from "../../interfaces/IHyperdriveCoreDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueHyperdrive } from "../../instances/morpho-blue/MorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueHyperdriveCoreDeployer +/// @notice The core deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueHyperdriveCoreDeployer is IHyperdriveCoreDeployer { + /// @notice Deploys a Hyperdrive instance with the given parameters. + /// @param __name The name of the Hyperdrive pool. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _target0 The target0 address. + /// @param _target1 The target1 address. + /// @param _target2 The target2 address. + /// @param _target3 The target3 address. + /// @param _target4 The target4 address. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueHyperdrive instance. + function deployHyperdrive( + string memory __name, + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + address _target0, + address _target1, + address _target2, + address _target3, + address _target4, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return ( + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueHyperdrive{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }( + __name, + _config, + _target0, + _target1, + _target2, + _target3, + _target4, + params + ) + ) + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveDeployerCoordinator.sol b/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveDeployerCoordinator.sol new file mode 100644 index 000000000..b0c46334e --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveDeployerCoordinator.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { MorphoBlueConversions } from "../../instances/morpho-blue/MorphoBlueConversions.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { IHyperdriveDeployerCoordinator } from "../../interfaces/IHyperdriveDeployerCoordinator.sol"; +import { MORPHO_BLUE_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND } from "../../libraries/Constants.sol"; +import { ONE } from "../../libraries/FixedPointMath.sol"; +import { HyperdriveDeployerCoordinator } from "../HyperdriveDeployerCoordinator.sol"; + +/// @author DELV +/// @title MorphoBlueHyperdriveDeployerCoordinator +/// @notice The deployer coordinator for the MorphoBlueHyperdrive +/// implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueHyperdriveDeployerCoordinator is + HyperdriveDeployerCoordinator +{ + using SafeERC20 for ERC20; + + /// @notice The deployer coordinator's kind. + string public constant override kind = + MORPHO_BLUE_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND; + + /// @notice Instantiates the deployer coordinator. + /// @param _name The deployer coordinator's name. + /// @param _factory The factory that this deployer will be registered with. + /// @param _coreDeployer The core deployer. + /// @param _target0Deployer The target0 deployer. + /// @param _target1Deployer The target1 deployer. + /// @param _target2Deployer The target2 deployer. + /// @param _target3Deployer The target3 deployer. + /// @param _target4Deployer The target4 deployer. + constructor( + string memory _name, + address _factory, + address _coreDeployer, + address _target0Deployer, + address _target1Deployer, + address _target2Deployer, + address _target3Deployer, + address _target4Deployer + ) + HyperdriveDeployerCoordinator( + _name, + _factory, + _coreDeployer, + _target0Deployer, + _target1Deployer, + _target2Deployer, + _target3Deployer, + _target4Deployer + ) + {} + + /// @dev Prepares the coordinator for initialization by drawing funds from + /// the LP, if necessary. + /// @param _hyperdrive The Hyperdrive instance that is being initialized. + /// @param _lp The LP that is initializing the pool. + /// @param _contribution The amount of capital to supply. The units of this + /// quantity are either base or vault shares, depending on the value + /// of `_options.asBase`. + /// @param _options The options that configure how the initialization is + /// settled. + /// @return value The value that should be sent in the initialize transaction. + function _prepareInitialize( + IHyperdrive _hyperdrive, + address _lp, + uint256 _contribution, + IHyperdrive.Options memory _options + ) internal override returns (uint256 value) { + // Depositing with shares is not supported. + if (!_options.asBase) { + revert IHyperdrive.UnsupportedToken(); + } + + // Transfer base from the LP and approve the Hyperdrive pool. + ERC20 baseToken = ERC20(_hyperdrive.baseToken()); + baseToken.safeTransferFrom(_lp, address(this), _contribution); + baseToken.forceApprove(address(_hyperdrive), _contribution); + + // This yield source isn't payable, so we should always send 0 value. + return 0; + } + + /// @notice Convert an amount of vault shares to an amount of base. + /// @param _baseToken The base token underlying the Aave vault. + /// @param _vault The Morpho Blue contract. + /// @param _baseToken The collateral token for this Morpho Blue market. + /// @param _oracle The oracle for this Morpho Blue market. + /// @param _irm The IRM for this Morpho Blue market. + /// @param _lltv The LLTV for this Morpho Blue market. + /// @param _shareAmount The vault shares amount. + /// @return The base amount. + function convertToBase( + IMorpho _vault, + IERC20 _baseToken, + address _collateralToken, + address _oracle, + address _irm, + uint256 _lltv, + uint256 _shareAmount + ) public view returns (uint256) { + return + MorphoBlueConversions.convertToBase( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv, + _shareAmount + ); + } + + /// @notice Convert an amount of base to an amount of vault shares. + /// @param _baseToken The base token underlying the Aave vault. + /// @param _vault The Morpho Blue contract. + /// @param _baseToken The collateral token for this Morpho Blue market. + /// @param _oracle The oracle for this Morpho Blue market. + /// @param _irm The IRM for this Morpho Blue market. + /// @param _lltv The LLTV for this Morpho Blue market. + /// @param _baseAmount The base amount. + /// @return The base amount. + function convertToShares( + IMorpho _vault, + IERC20 _baseToken, + address _collateralToken, + address _oracle, + address _irm, + uint256 _lltv, + uint256 _baseAmount + ) public view returns (uint256) { + return + MorphoBlueConversions.convertToShares( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv, + _baseAmount + ); + } + + /// @dev We override the message value check since this integration is + /// not payable. + function _checkMessageValue() internal view override { + if (msg.value != 0) { + revert IHyperdriveDeployerCoordinator.NotPayable(); + } + } + + /// @notice Checks the pool configuration to ensure that it is valid. + /// @param _deployConfig The deploy configuration of the Hyperdrive pool. + function _checkPoolConfig( + IHyperdrive.PoolDeployConfig memory _deployConfig + ) internal view override { + // Perform the default checks. + super._checkPoolConfig(_deployConfig); + + // Ensure that the vault shares token address is zero. This makes it + // clear that there isn't a vault shares token backing the Morpho Blue + // integration. + if (address(_deployConfig.vaultSharesToken) != address(0)) { + revert IHyperdriveDeployerCoordinator.InvalidVaultSharesToken(); + } + + // Ensure that the base token address is properly configured. + if (address(_deployConfig.baseToken) == address(0)) { + revert IHyperdriveDeployerCoordinator.InvalidBaseToken(); + } + + // Ensure that the minimum share reserves are large enough to meet the + // minimum requirements for safety. + // + // NOTE: Some pools may require larger minimum share reserves to be + // considered safe. This is just a sanity check. + if ( + _deployConfig.minimumShareReserves < + 10 ** (_deployConfig.baseToken.decimals() - 3) + ) { + revert IHyperdriveDeployerCoordinator.InvalidMinimumShareReserves(); + } + + // Ensure that the minimum transaction amount is large enough to meet + // the minimum requirements for safety. + // + // NOTE: Some pools may require larger minimum transaction amounts to be + // considered safe. This is just a sanity check. + if ( + _deployConfig.minimumTransactionAmount < + 10 ** (_deployConfig.baseToken.decimals() - 3) + ) { + revert IHyperdriveDeployerCoordinator + .InvalidMinimumTransactionAmount(); + } + } + + /// @dev Gets the initial vault share price of the Hyperdrive pool. + /// @param _deployConfig The deploy configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @return The initial vault share price of the Hyperdrive pool. + function _getInitialVaultSharePrice( + IHyperdrive.PoolDeployConfig memory _deployConfig, + bytes memory _extraData + ) internal view override returns (uint256) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + convertToBase( + params.morpho, + _deployConfig.baseToken, + params.collateralToken, + params.oracle, + params.irm, + params.lltv, + ONE + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueTarget0Deployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueTarget0Deployer.sol new file mode 100644 index 000000000..e39cd943e --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueTarget0Deployer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { MorphoBlueTarget0 } from "../../instances/morpho-blue/MorphoBlueTarget0.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueTarget0Deployer +/// @notice The target0 deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget0Deployer is IHyperdriveTargetDeployer { + /// @notice Deploys a target0 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueTarget0 instance. + function deployTarget( + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueTarget0{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }(_config, params) + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueTarget1Deployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueTarget1Deployer.sol new file mode 100644 index 000000000..32fed5280 --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueTarget1Deployer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { MorphoBlueTarget1 } from "../../instances/morpho-blue/MorphoBlueTarget1.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueTarget1Deployer +/// @notice The target1 deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget1Deployer is IHyperdriveTargetDeployer { + /// @notice Deploys a target1 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueTarget1 instance. + function deployTarget( + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueTarget1{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }(_config, params) + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueTarget2Deployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueTarget2Deployer.sol new file mode 100644 index 000000000..a159c20b6 --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueTarget2Deployer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { MorphoBlueTarget2 } from "../../instances/morpho-blue/MorphoBlueTarget2.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueTarget2Deployer +/// @notice The target2 deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget2Deployer is IHyperdriveTargetDeployer { + /// @notice Deploys a target2 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueTarget2 instance. + function deployTarget( + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueTarget2{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }(_config, params) + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueTarget3Deployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueTarget3Deployer.sol new file mode 100644 index 000000000..02e875848 --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueTarget3Deployer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { MorphoBlueTarget3 } from "../../instances/morpho-blue/MorphoBlueTarget3.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueTarget3Deployer +/// @notice The target3 deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget3Deployer is IHyperdriveTargetDeployer { + /// @notice Deploys a target3 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueTarget3 instance. + function deployTarget( + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueTarget3{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }(_config, params) + ); + } +} diff --git a/contracts/src/deployers/morpho-blue/MorphoBlueTarget4Deployer.sol b/contracts/src/deployers/morpho-blue/MorphoBlueTarget4Deployer.sol new file mode 100644 index 000000000..de10bd080 --- /dev/null +++ b/contracts/src/deployers/morpho-blue/MorphoBlueTarget4Deployer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { MorphoBlueTarget4 } from "../../instances/morpho-blue/MorphoBlueTarget4.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; + +/// @author DELV +/// @title MorphoBlueTarget4Deployer +/// @notice The target4 deployer for the MorphoBlueHyperdrive implementation. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget4Deployer is IHyperdriveTargetDeployer { + /// @notice Deploys a target4 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _extraData The extra data for the Morpho instance. This contains + /// the market parameters that weren't specified in the config. + /// @param _salt The create2 salt used in the deployment. + /// @return The address of the newly deployed MorphoBlueTarget4 instance. + function deployTarget( + IHyperdrive.PoolConfig memory _config, + bytes memory _extraData, + bytes32 _salt + ) external returns (address) { + IMorphoBlueHyperdrive.MorphoBlueParams memory params = abi.decode( + _extraData, + (IMorphoBlueHyperdrive.MorphoBlueParams) + ); + return + address( + // NOTE: We hash the sender with the salt to prevent the + // front-running of deployments. + new MorphoBlueTarget4{ + salt: keccak256(abi.encode(msg.sender, _salt)) + }(_config, params) + ); + } +} diff --git a/contracts/src/instances/lseth/LsETHBase.sol b/contracts/src/instances/lseth/LsETHBase.sol index f2b623b8f..bcac4a7d5 100644 --- a/contracts/src/instances/lseth/LsETHBase.sol +++ b/contracts/src/instances/lseth/LsETHBase.sol @@ -23,7 +23,7 @@ abstract contract LsETHBase is HyperdriveBase { /// Yield Source /// - /// @dev Deposits as base asset not supported for this integration. + /// @dev Deposits with base are not supported for this integration. function _depositWithBase( uint256, // unused bytes calldata // unused @@ -45,7 +45,7 @@ abstract contract LsETHBase is HyperdriveBase { ); } - /// @dev Withdrawals as base asset not supported for this integration. + /// @dev Withdrawals with base are not supported for this integration. function _withdrawWithBase( uint256, // unused address, // unused diff --git a/contracts/src/instances/morpho-blue/MorphoBlueBase.sol b/contracts/src/instances/morpho-blue/MorphoBlueBase.sol new file mode 100644 index 000000000..ef047fda6 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueBase.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho, MarketParams } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { MarketParamsLib } from "morpho-blue/src/libraries/MarketParamsLib.sol"; +import { SharesMathLib } from "morpho-blue/src/libraries/SharesMathLib.sol"; +import { MorphoBalancesLib } from "morpho-blue/src/libraries/periphery/MorphoBalancesLib.sol"; +import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { HyperdriveBase } from "../../internal/HyperdriveBase.sol"; +import { MorphoBlueConversions } from "./MorphoBlueConversions.sol"; + +/// @author DELV +/// @title MorphoBlueBase +/// @notice The base contract for the MorphoBlue Hyperdrive implementation. +/// @dev This Hyperdrive implementation is designed to work with standard +/// MorphoBlue vaults. Non-standard implementations may not work correctly +/// and should be carefully checked. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +abstract contract MorphoBlueBase is HyperdriveBase { + using SafeERC20 for ERC20; + using MarketParamsLib for MarketParams; + using MorphoBalancesLib for IMorpho; + using SharesMathLib for uint256; + + /// @dev The Morpho Blue contract. + IMorpho internal immutable _vault; + + /// @dev The collateral token for this Morpho Blue market. + address internal immutable _collateralToken; + + /// @dev The oracle for this Morpho Blue market. + address internal immutable _oracle; + + /// @dev The IRM for this Morpho Blue market. + address internal immutable _irm; + + /// @dev The LLTV for this Morpho Blue market. + uint256 internal immutable _lltv; + + /// @notice Instantiates the MorphoBlueHyperdrive base contract. + /// @param _params The Morpho Blue params. + constructor(IMorphoBlueHyperdrive.MorphoBlueParams memory _params) { + // Initialize the Morpho vault immutable. + _vault = _params.morpho; + + // Initialize the market parameters immutables. We don't need an + // immutable for the loan token because we set the base token to the + // loan token. + _collateralToken = _params.collateralToken; + _oracle = _params.oracle; + _irm = _params.irm; + _lltv = _params.lltv; + + // Approve the Morpho vault with 1 wei. This ensures that all of the + // subsequent approvals will be writing to a dirty storage slot. + ERC20(address(_baseToken)).forceApprove(address(_vault), 1); + } + + /// Yield Source /// + + /// @dev Accepts a deposit from the user in base. + /// @param _baseAmount The base amount to deposit. + /// @param _extraData Additional data to pass to the Morpho vault. This + /// should be zero if it is unused. + /// @return sharesMinted The shares that were minted in the deposit. + /// @return value The amount of ETH to refund. Since this yield source isn't + /// payable, this is always zero. + function _depositWithBase( + uint256 _baseAmount, + bytes calldata _extraData + ) internal override returns (uint256 sharesMinted, uint256 value) { + // Take custody of the deposit in base. + ERC20(address(_baseToken)).safeTransferFrom( + msg.sender, + address(this), + _baseAmount + ); + + // Deposit the base into the yield source. + // + // NOTE: We increase the required approval amount by 1 wei so that + // the vault ends with an approval of 1 wei. This makes future + // approvals cheaper by keeping the storage slot warm. + ERC20(address(_baseToken)).forceApprove( + address(_vault), + _baseAmount + 1 + ); + (, sharesMinted) = _vault.supply( + MarketParams({ + loanToken: address(_baseToken), + collateralToken: _collateralToken, + oracle: _oracle, + irm: _irm, + lltv: _lltv + }), + _baseAmount, + 0, + address(this), + _extraData + ); + + // NOTE: Since this yield source isn't payable, the value must be zero. + value = 0; + return (sharesMinted, value); + } + + /// @dev Deposits with shares are not supported for this integration. + function _depositWithShares( + uint256, // unused _shareAmount + bytes calldata // unused _extraData + ) internal pure override { + revert IHyperdrive.UnsupportedToken(); + } + + /// @dev Process a withdrawal in base and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + /// @return amountWithdrawn The amount of base withdrawn. + function _withdrawWithBase( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override returns (uint256 amountWithdrawn) { + (amountWithdrawn, ) = _vault.withdraw( + MarketParams({ + loanToken: address(_baseToken), + collateralToken: _collateralToken, + oracle: _oracle, + irm: _irm, + lltv: _lltv + }), + 0, + _shareAmount, + address(this), + _destination + ); + + return amountWithdrawn; + } + + /// @dev Withdrawals with shares are not supported for this integration. + function _withdrawWithShares( + uint256, // unused _shareAmount + address, // unused _destination + bytes calldata // unused + ) internal pure override { + revert IHyperdrive.UnsupportedToken(); + } + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _shareAmount The vault shares amount. + /// @return The base amount. + function _convertToBase( + uint256 _shareAmount + ) internal view override returns (uint256) { + return + MorphoBlueConversions.convertToBase( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv, + _shareAmount + ); + } + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _baseAmount The base amount. + /// @return The vault shares amount. + function _convertToShares( + uint256 _baseAmount + ) internal view override returns (uint256) { + return + MorphoBlueConversions.convertToShares( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv, + _baseAmount + ); + } + + /// @dev Gets the total amount of shares held by the pool in the yield + /// source. + /// @return shareAmount The total amount of shares. + function _totalShares() + internal + view + override + returns (uint256 shareAmount) + { + return + _vault + .position( + MarketParams({ + loanToken: address(_baseToken), + collateralToken: _collateralToken, + oracle: _oracle, + irm: _irm, + lltv: _lltv + }).id(), + address(this) + ) + .supplyShares; + } + + /// @dev We override the message value check since this integration is + /// not payable. + function _checkMessageValue() internal view override { + if (msg.value != 0) { + revert IHyperdrive.NotPayable(); + } + } +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueConversions.sol b/contracts/src/instances/morpho-blue/MorphoBlueConversions.sol new file mode 100644 index 000000000..a1fe7c418 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueConversions.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho, MarketParams } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { SharesMathLib } from "morpho-blue/src/libraries/SharesMathLib.sol"; +import { MorphoBalancesLib } from "morpho-blue/src/libraries/periphery/MorphoBalancesLib.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { FixedPointMath } from "../../libraries/FixedPointMath.sol"; + +/// @author DELV +/// @title MorphoBlueConversions +/// @notice The conversion logic for the Morpho Blue integration. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +library MorphoBlueConversions { + using FixedPointMath for uint256; + using MorphoBalancesLib for IMorpho; + using SharesMathLib for uint256; + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _vault The Morpho Blue contract. + /// @param _baseToken The base token underlying the Morpho Blue vault. + /// @param _collateralToken The collateral token for this Morpho Blue market. + /// @param _oracle The oracle for this Morpho Blue market. + /// @param _irm The IRM for this Morpho Blue market. + /// @param _lltv The LLTV for this Morpho Blue market. + /// @param _shareAmount The vault shares amount. + /// @return The base amount. + function convertToBase( + IMorpho _vault, + IERC20 _baseToken, + address _collateralToken, + address _oracle, + address _irm, + uint256 _lltv, + uint256 _shareAmount + ) external view returns (uint256) { + // Get the total supply assets and shares after interest accrues. + ( + uint256 totalSupplyAssets, + uint256 totalSupplyShares + ) = getExpectedSupplyBalances( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv + ); + + return _shareAmount.toAssetsDown(totalSupplyAssets, totalSupplyShares); + } + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _vault The Morpho Blue vault. + /// @param _baseToken The base token underlying the Morpho Blue vault. + /// @param _collateralToken The collateral token for this Morpho Blue market. + /// @param _oracle The oracle for this Morpho Blue market. + /// @param _irm The IRM for this Morpho Blue market. + /// @param _lltv The LLTV for this Morpho Blue market. + /// @param _baseAmount The base amount. + /// @return The vault shares amount. + function convertToShares( + IMorpho _vault, + IERC20 _baseToken, + address _collateralToken, + address _oracle, + address _irm, + uint256 _lltv, + uint256 _baseAmount + ) external view returns (uint256) { + // Get the total supply assets and shares after interest accrues. + ( + uint256 totalSupplyAssets, + uint256 totalSupplyShares + ) = getExpectedSupplyBalances( + _vault, + _baseToken, + _collateralToken, + _oracle, + _irm, + _lltv + ); + + return _baseAmount.toSharesDown(totalSupplyAssets, totalSupplyShares); + } + + /// @dev Gets the Morpho Blue supply balances after accruing interest. + /// @param _vault The Morpho Blue vault. + /// @param _baseToken The base token underlying the Morpho Blue vault. + /// @param _collateralToken The collateral token for this Morpho Blue market. + /// @param _oracle The oracle for this Morpho Blue market. + /// @param _irm The IRM for this Morpho Blue market. + /// @param _lltv The LLTV for this Morpho Blue market. + /// @return totalSupplyAssets The total amount of assets after interest. + /// @return totalSupplyShares The total amount of shares after interest. + function getExpectedSupplyBalances( + IMorpho _vault, + IERC20 _baseToken, + address _collateralToken, + address _oracle, + address _irm, + uint256 _lltv + ) + internal + view + returns (uint256 totalSupplyAssets, uint256 totalSupplyShares) + { + (totalSupplyAssets, totalSupplyShares, , ) = _vault + .expectedMarketBalances( + MarketParams({ + loanToken: address(_baseToken), + collateralToken: _collateralToken, + oracle: _oracle, + irm: _irm, + lltv: _lltv + }) + ); + return (totalSupplyAssets, totalSupplyShares); + } +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueHyperdrive.sol b/contracts/src/instances/morpho-blue/MorphoBlueHyperdrive.sol new file mode 100644 index 000000000..33cadaa51 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueHyperdrive.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { Hyperdrive } from "../../external/Hyperdrive.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// ______ __ _________ _____ +/// ___ / / /____ ___________________________ /_________(_)__ ______ +/// __ /_/ /__ / / /__ __ \ _ \_ ___/ __ /__ ___/_ /__ | / / _ \ +/// _ __ / _ /_/ /__ /_/ / __/ / / /_/ / _ / _ / __ |/ // __/ +/// /_/ /_/ _\__, / _ ___/\___//_/ \__,_/ /_/ /_/ _____/ \___/ +/// /____/ /_/ +/// XXX ++ ++ XXX +/// ############ XXXXX ++0+ +0++ XXXXX ########### +/// ##////////////######## ++00++ ++00++ ########///////////## +/// ##////////////########## ++000++ ++000++ ##########///////////## +/// ##%%%%%%///// ###### ++0000+ +0000++ ###### /////%%%%%%## +/// %%%%%%%%&& ## ++0000+ +0000++ ## &&%%%%%%%%% +/// %&&& ## +o000+ +000o+ ## &&&% +/// ## ++00+- -+00++ ## +/// #% ++0+ +0++ %# +/// ###-:Oo.++++.oO:-### +/// ##: 00++++++00 :## +/// #S###########* 0++00+++00++0 *##########S# +/// #S % $ 0+++0 $ % S# +/// #S ---------- %+++++:#:+++++%----------- S# +/// #S ------------- %++++: ### :++++%------------ S# +/// S ---------------%++++*\ | /*++++%------------- S +/// #S --------------- %++++ ~W~ ++++%666--o UUUU o- S# +/// #S? --------------- %+++++~+++++%&&&8 o \ / o ?S# +/// ?*????**+++;::,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,::;+++**????*? +/// #?+////////////////////////////////////////////////////////////////+?# +/// #;;;;;//////////////////////////////////////////////////////////////;;;;;# +/// S;;;;;;;;;//////////////////////////////////////////////////////////;;;;;;;;;S +/// /;;;;;;;;;;;///////////////////////////////////////////////////////;;;;;;;;;;;;\ +/// |||OOOOOOOO||OOOOOOOO=========== __ ___ ===========OOOOOOOO||OOOOOOOO||| +/// |||OOOOOOOO||OOOOOOOO===========| \[__ | \ /===========OOOOOOOO||OOOOOOOO||| +/// |||OOOOOOOO||OOOOOOOO===========|__/[___|___ \/ ===========OOOOOOOO||OOOOOOOO||| +/// |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +/// |||////////000000000000\\\\\\\\|:::::::::::::::|////////00000000000\\\\\\\\\\||| +/// SSS\\\\\\\\000000000000////////|:::::0x666:::::|\\\\\\\\00000000000//////////SSS +/// SSS|||||||||||||||||||||||||||||:::::::::::::::||||||||||||||||||||||||||||||SSS +/// SSSSSSSS|_______________|______________||_______________|______________|SSSSSSSS +/// SSSSSSSS SSSSSSSS +/// SSSSSSSS SSSSSSSS +/// +/// @author DELV +/// @title MorphoBlueHyperdrive +/// @notice A Hyperdrive instance that uses a MorphoBlue vault as the yield source. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueHyperdrive is Hyperdrive, MorphoBlueBase { + using SafeERC20 for ERC20; + + /// @notice Instantiates Hyperdrive with a MorphoBlue vault as the yield source. + /// @param __name The pool's name. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _target0 The target0 address. + /// @param _target1 The target1 address. + /// @param _target2 The target2 address. + /// @param _target3 The target3 address. + /// @param _target4 The target4 address. + /// @param _params The Morpho Blue params. + constructor( + string memory __name, + IHyperdrive.PoolConfig memory _config, + address _target0, + address _target1, + address _target2, + address _target3, + address _target4, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) + Hyperdrive( + __name, + _config, + _target0, + _target1, + _target2, + _target3, + _target4 + ) + MorphoBlueBase(_params) + {} +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueTarget0.sol b/contracts/src/instances/morpho-blue/MorphoBlueTarget0.sol new file mode 100644 index 000000000..7df812ef0 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueTarget0.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { HyperdriveTarget0 } from "../../external/HyperdriveTarget0.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MORPHO_BLUE_HYPERDRIVE_KIND } from "../../libraries/Constants.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// @author DELV +/// @title MorphoBlueTarget0 +/// @notice MorphoBlueHyperdrive's target0 logic contract. This contract contains +/// all of the getters for Hyperdrive as well as some stateful +/// functions. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget0 is HyperdriveTarget0, MorphoBlueBase { + /// @notice Initializes the target0 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _params The Morpho Blue params. + constructor( + IHyperdrive.PoolConfig memory _config, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) HyperdriveTarget0(_config) MorphoBlueBase(_params) {} + + /// @notice Returns the instance's kind. + /// @return The instance's kind. + function kind() external pure override returns (string memory) { + _revert(abi.encode(MORPHO_BLUE_HYPERDRIVE_KIND)); + } + + /// @notice Returns the Morpho Blue contract. + /// @return The Morpho Blue contract. + function vault() external view returns (IMorpho) { + _revert(abi.encode(_vault)); + } + + /// @notice Returns the collateral token for this Morpho Blue market. + /// @return The collateral token for this Morpho Blue market. + function collateralToken() external view returns (address) { + _revert(abi.encode(_collateralToken)); + } + + /// @notice Returns the oracle for this Morpho Blue market. + /// @return The oracle for this Morpho Blue market. + function oracle() external view returns (address) { + _revert(abi.encode(_oracle)); + } + + /// @notice Returns the IRM for this Morpho Blue market. + /// @return The IRM for this Morpho Blue market. + function irm() external view returns (address) { + _revert(abi.encode(_irm)); + } + + /// @notice Returns the LLTV for this Morpho Blue market. + /// @return The LLTV for this Morpho Blue market. + function lltv() external view returns (uint256) { + _revert(abi.encode(_lltv)); + } +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueTarget1.sol b/contracts/src/instances/morpho-blue/MorphoBlueTarget1.sol new file mode 100644 index 000000000..37ba09d0b --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueTarget1.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { HyperdriveTarget1 } from "../../external/HyperdriveTarget1.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// @author DELV +/// @title MorphoBlueTarget1 +/// @notice MorphoBlueHyperdrive's target1 logic contract. This contract contains +/// several stateful functions that couldn't fit into the Hyperdrive +/// contract. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget1 is HyperdriveTarget1, MorphoBlueBase { + /// @notice Initializes the target1 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _params The Morpho Blue params. + constructor( + IHyperdrive.PoolConfig memory _config, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) HyperdriveTarget1(_config) MorphoBlueBase(_params) {} +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueTarget2.sol b/contracts/src/instances/morpho-blue/MorphoBlueTarget2.sol new file mode 100644 index 000000000..15c69222f --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueTarget2.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { HyperdriveTarget2 } from "../../external/HyperdriveTarget2.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// @author DELV +/// @title MorphoBlueTarget2 +/// @notice MorphoBlueHyperdrive's target2 logic contract. This contract contains +/// several stateful functions that couldn't fit into the Hyperdrive +/// contract. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget2 is HyperdriveTarget2, MorphoBlueBase { + /// @notice Initializes the target2 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _params The Morpho Blue params. + constructor( + IHyperdrive.PoolConfig memory _config, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) HyperdriveTarget2(_config) MorphoBlueBase(_params) {} +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueTarget3.sol b/contracts/src/instances/morpho-blue/MorphoBlueTarget3.sol new file mode 100644 index 000000000..5cc252106 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueTarget3.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { HyperdriveTarget3 } from "../../external/HyperdriveTarget3.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// @author DELV +/// @title MorphoBlueTarget3 +/// @notice MorphoBlueHyperdrive's target3 logic contract. This contract contains +/// several stateful functions that couldn't fit into the Hyperdrive +/// contract. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget3 is HyperdriveTarget3, MorphoBlueBase { + /// @notice Initializes the target3 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _params The Morpho Blue params. + constructor( + IHyperdrive.PoolConfig memory _config, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) HyperdriveTarget3(_config) MorphoBlueBase(_params) {} +} diff --git a/contracts/src/instances/morpho-blue/MorphoBlueTarget4.sol b/contracts/src/instances/morpho-blue/MorphoBlueTarget4.sol new file mode 100644 index 000000000..cbfe61299 --- /dev/null +++ b/contracts/src/instances/morpho-blue/MorphoBlueTarget4.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { HyperdriveTarget4 } from "../../external/HyperdriveTarget4.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "../../interfaces/IMorphoBlueHyperdrive.sol"; +import { MorphoBlueBase } from "./MorphoBlueBase.sol"; + +/// @author DELV +/// @title MorphoBlueTarget4 +/// @notice MorphoBlueHyperdrive's target4 logic contract. This contract contains +/// several stateful functions that couldn't fit into the Hyperdrive +/// contract. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract MorphoBlueTarget4 is HyperdriveTarget4, MorphoBlueBase { + /// @notice Initializes the target4 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _params The Morpho Blue params. + constructor( + IHyperdrive.PoolConfig memory _config, + IMorphoBlueHyperdrive.MorphoBlueParams memory _params + ) HyperdriveTarget4(_config) MorphoBlueBase(_params) {} +} diff --git a/contracts/src/interfaces/IMorphoBlueHyperdrive.sol b/contracts/src/interfaces/IMorphoBlueHyperdrive.sol new file mode 100644 index 000000000..f8db78006 --- /dev/null +++ b/contracts/src/interfaces/IMorphoBlueHyperdrive.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { IHyperdrive } from "./IHyperdrive.sol"; + +interface IMorphoBlueHyperdrive is IHyperdrive { + struct MorphoBlueParams { + IMorpho morpho; + address collateralToken; + address oracle; + address irm; + uint256 lltv; + } + + /// @notice Gets the vault used as this pool's yield source. + /// @return The compatible yield source. + function vault() external view returns (address); + + /// @notice Returns the collateral token for this Morpho Blue market. + /// @return The collateral token for this Morpho Blue market. + function collateralToken() external view returns (address); + + /// @notice Returns the oracle for this Morpho Blue market. + /// @return The oracle for this Morpho Blue market. + function oracle() external view returns (address); + + /// @notice Returns the interest rate model for this Morpho Blue market. + /// @return The interest rate model for this Morpho Blue market. + function irm() external view returns (address); + + /// @notice Returns the liquidation loan to value ratio for this Morpho Blue + /// market. + /// @return The liquiditation loan to value ratio for this Morpho Blue market. + function lltv() external view returns (uint256); +} diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index ae860555b..70bcd34f5 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -40,6 +40,14 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { uint256 _amount, IHyperdrive.Options calldata _options ) internal returns (uint256 sharesMinted, uint256 vaultSharePrice) { + // WARN: This logic doesn't account for slippage in the conversion + // from base to shares. If deposits to the yield source incur + // slippage, this logic will be incorrect. + // + // The amount of shares minted is equal to the input amount if the + // deposit asset is in shares. + sharesMinted = _amount; + // Deposit with either base or shares depending on the provided options. uint256 refund; if (_options.asBase) { @@ -55,13 +63,6 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { // Process the deposit in shares. _depositWithShares(_amount, _options.extraData); - - // WARN: This logic doesn't account for slippage in the conversion - // from base to shares. If deposits to the yield source incur - // slippage, this logic will be incorrect. - // - // The amount of shares minted is equal to the input amount. - sharesMinted = _amount; } // Calculate the vault share price. @@ -107,6 +108,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { } // Withdraw in either base or shares depending on the provided options. + amountWithdrawn = _shares; if (_options.asBase) { // Process the withdrawal in base. amountWithdrawn = _withdrawWithBase( @@ -121,7 +123,6 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { _options.destination, _options.extraData ); - amountWithdrawn = _shares; } return amountWithdrawn; diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index 41743ab46..91e7721d3 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -37,6 +37,9 @@ string constant ERC4626_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND = "ERC4626Hyperdriv /// @dev The kind of the EzETHHyperdrive deployer coordinator factory. string constant EZETH_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND = "EzETHHyperdriveDeployerCoordinator"; +/// @dev The kind of the MorphoBlueHyperdrive deployer coordinator factory. +string constant MORPHO_BLUE_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND = "MorphoBlueHyperdriveDeployerCoordinator"; + /// @dev The kind of the LsETHHyperdrive deployer coordinator factory. string constant LSETH_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND = "LsETHHyperdriveDeployerCoordinator"; @@ -58,6 +61,9 @@ string constant EZETH_HYPERDRIVE_KIND = "EzETHHyperdrive"; /// @dev The kind of LsETHHyperdrive. string constant LSETH_HYPERDRIVE_KIND = "LsETHHyperdrive"; +/// @dev The kind of MorphoBlueHyperdrive. +string constant MORPHO_BLUE_HYPERDRIVE_KIND = "MorphoBlueHyperdrive"; + /// @dev The kind of RETHHyperdrive. string constant RETH_HYPERDRIVE_KIND = "RETHHyperdrive"; diff --git a/lib/morpho-blue b/lib/morpho-blue new file mode 160000 index 000000000..55d2d9930 --- /dev/null +++ b/lib/morpho-blue @@ -0,0 +1 @@ +Subproject commit 55d2d99304fb3fb930c688462ae2ccabb1d533ad diff --git a/test/instances/aave/AaveHyperdrive.t.sol b/test/instances/aave/AaveHyperdrive.t.sol index d773b1dcb..dfd08b779 100644 --- a/test/instances/aave/AaveHyperdrive.t.sol +++ b/test/instances/aave/AaveHyperdrive.t.sol @@ -77,6 +77,12 @@ contract AaveHyperdriveTest is InstanceTest { /// Overrides /// + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + /// @dev Converts base amount to the equivalent about in shares. function convertToShares( uint256 baseAmount diff --git a/test/instances/ezETH/EzETHHyperdrive.t.sol b/test/instances/ezETH/EzETHHyperdrive.t.sol index 854872082..e4a1ff431 100644 --- a/test/instances/ezETH/EzETHHyperdrive.t.sol +++ b/test/instances/ezETH/EzETHHyperdrive.t.sol @@ -93,6 +93,12 @@ contract EzETHHyperdriveTest is InstanceTest { /// Overrides /// + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + /// @dev Converts base amount to the equivalent about in EzETH. function convertToShares( uint256 baseAmount @@ -166,62 +172,35 @@ contract EzETHHyperdriveTest is InstanceTest { AccountBalances memory traderBalancesBefore, AccountBalances memory hyperdriveBalancesBefore ) internal view override { + // Base deposits are not supported for this instance. if (asBase) { - // Ensure that the amount of pooled ether increased by the base paid. - (, uint256 totalPooledEther, ) = getSharePrice(); - assertEq(totalPooledEther, totalBaseSupplyBefore + basePaid); - - // Ensure that the ETH balances were updated correctly. - assertEq( - address(hyperdrive).balance, - hyperdriveBalancesBefore.ETHBalance - ); - assertEq(bob.balance, traderBalancesBefore.ETHBalance - basePaid); + revert IHyperdrive.UnsupportedToken(); + } - // Ensure ezETH shares were updated correctly. - assertEq( - EZETH.balanceOf(trader), - traderBalancesBefore.sharesBalance - ); + // Ensure that the amount of pooled ether stays the same. + (, uint256 totalPooledEther, ) = getSharePrice(); + assertEq(totalPooledEther, totalBaseSupplyBefore); - // Ensure that the ezETH shares were updated correctly. - uint256 expectedShares = RENZO_ORACLE.calculateMintAmount( - totalBaseSupplyBefore, - basePaid, - totalSharesBefore - ); - assertEq(EZETH.totalSupply(), totalSharesBefore + expectedShares); - assertEq( - EZETH.balanceOf(address(hyperdrive)), - hyperdriveBalancesBefore.sharesBalance + expectedShares - ); - assertEq(EZETH.balanceOf(bob), traderBalancesBefore.sharesBalance); - } else { - // Ensure that the amount of pooled ether stays the same. - (, uint256 totalPooledEther, ) = getSharePrice(); - assertEq(totalPooledEther, totalBaseSupplyBefore); - - // Ensure that the ETH balances were updated correctly. - assertEq( - address(hyperdrive).balance, - hyperdriveBalancesBefore.ETHBalance - ); - assertEq(trader.balance, traderBalancesBefore.ETHBalance); - - // Ensure that the ezETH shares were updated correctly. - uint256 expectedShares = convertToShares(basePaid); - assertEq(EZETH.totalSupply(), totalSharesBefore); - assertApproxEqAbs( - EZETH.balanceOf(address(hyperdrive)), - hyperdriveBalancesBefore.sharesBalance + expectedShares, - 2 // Higher tolerance due to rounding when converting back into shares. - ); - assertApproxEqAbs( - EZETH.balanceOf(trader), - traderBalancesBefore.sharesBalance - expectedShares, - 2 // Higher tolerance due to rounding when converting back into shares. - ); - } + // Ensure that the ETH balances were updated correctly. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(trader.balance, traderBalancesBefore.ETHBalance); + + // Ensure that the ezETH shares were updated correctly. + uint256 expectedShares = convertToShares(basePaid); + assertEq(EZETH.totalSupply(), totalSharesBefore); + assertApproxEqAbs( + EZETH.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.sharesBalance + expectedShares, + 2 // Higher tolerance due to rounding when converting back into shares. + ); + assertApproxEqAbs( + EZETH.balanceOf(trader), + traderBalancesBefore.sharesBalance - expectedShares, + 2 // Higher tolerance due to rounding when converting back into shares. + ); } /// @dev Verifies that withdrawal accounting is correct when closing positions. diff --git a/test/instances/lseth/LsETHHyperdrive.t.sol b/test/instances/lseth/LsETHHyperdrive.t.sol index 34e7a34f4..d2a3ea6ba 100644 --- a/test/instances/lseth/LsETHHyperdrive.t.sol +++ b/test/instances/lseth/LsETHHyperdrive.t.sol @@ -75,6 +75,12 @@ contract LsETHHyperdriveTest is InstanceTest { /// Overrides /// + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + /// @dev Deploys the LsETH deployer coordinator contract. /// @param _factory The address of the Hyperdrive factory contract. function deployCoordinator( diff --git a/test/instances/morpho-blue/MorphoBlueHyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlueHyperdrive.t.sol new file mode 100644 index 000000000..0e256f437 --- /dev/null +++ b/test/instances/morpho-blue/MorphoBlueHyperdrive.t.sol @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { Id, IMorpho, Market, MarketParams } from "morpho-blue/src/interfaces/IMorpho.sol"; +import { MarketParamsLib } from "morpho-blue/src/libraries/MarketParamsLib.sol"; +import { MorphoBalancesLib } from "morpho-blue/src/libraries/periphery/MorphoBalancesLib.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { MorphoBlueHyperdriveCoreDeployer } from "contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveCoreDeployer.sol"; +import { MorphoBlueHyperdriveDeployerCoordinator } from "contracts/src/deployers/morpho-blue/MorphoBlueHyperdriveDeployerCoordinator.sol"; +import { MorphoBlueTarget0Deployer } from "contracts/src/deployers/morpho-blue/MorphoBlueTarget0Deployer.sol"; +import { MorphoBlueTarget1Deployer } from "contracts/src/deployers/morpho-blue/MorphoBlueTarget1Deployer.sol"; +import { MorphoBlueTarget2Deployer } from "contracts/src/deployers/morpho-blue/MorphoBlueTarget2Deployer.sol"; +import { MorphoBlueTarget3Deployer } from "contracts/src/deployers/morpho-blue/MorphoBlueTarget3Deployer.sol"; +import { MorphoBlueTarget4Deployer } from "contracts/src/deployers/morpho-blue/MorphoBlueTarget4Deployer.sol"; +import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; +import { MorphoBlueConversions } from "contracts/src/instances/morpho-blue/MorphoBlueConversions.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { IMorphoBlueHyperdrive } from "contracts/src/interfaces/IMorphoBlueHyperdrive.sol"; +import { AssetId } from "contracts/src/libraries/AssetId.sol"; +import { ETH } from "contracts/src/libraries/Constants.sol"; +import { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; +import { ERC20ForwarderFactory } from "contracts/src/token/ERC20ForwarderFactory.sol"; +import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; +import { InstanceTest } from "test/utils/InstanceTest.sol"; +import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; + +contract MorphoBlueHyperdriveTest is InstanceTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using MarketParamsLib for MarketParams; + using MorphoBalancesLib for IMorpho; + using Lib for *; + using stdStorage for StdStorage; + + // The mainnet Morpho Blue pool. + IMorpho internal constant MORPHO = + IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + + // The ID of the SUSDe market. + bytes32 internal constant MARKET_ID = + bytes32( + 0x39d11026eae1c6ec02aa4c0910778664089cdd97c3fd23f68f7cd05e2e95af48 + ); + + // The address of the loan token. This is just the DAI token. + address internal constant LOAN_TOKEN = + address(0x6B175474E89094C44Da98b954EedeAC495271d0F); + + // The address of the collateral token. This is just the SUSDe token. + address internal constant COLLATERAL_TOKEN = + address(0x9D39A5DE30e57443BfF2A8307A4256c8797A3497); + + // The address of the oracle. + address internal constant ORACLE = + address(0x5D916980D5Ae1737a8330Bf24dF812b2911Aae25); + + // The address of the interest rate model. + address internal constant IRM = + address(0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC); + + // The liquidation loan to value ratio of the SUSDe market. + uint256 internal constant LLTV = 860000000000000000; + + // Whale accounts. + address internal LOAN_TOKEN_WHALE = + address(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + address[] internal baseTokenWhaleAccounts = [LOAN_TOKEN_WHALE]; + + // The configuration for the Instance testing suite. + InstanceTestConfig internal __testConfig = + InstanceTestConfig({ + name: "Hyperdrive", + kind: "MorphoBlueHyperdrive", + baseTokenWhaleAccounts: baseTokenWhaleAccounts, + vaultSharesTokenWhaleAccounts: new address[](0), + baseToken: IERC20(LOAN_TOKEN), + vaultSharesToken: IERC20(address(0)), + // NOTE: The share tolerance is quite high for this integration + // because the vault share price is ~1e12, which means that just + // multiplying or dividing by the vault is an imprecise way of + // converting between base and vault shares. We included more + // assertions than normal to the round trip tests to verify that + // the calculations satisfy our expectations of accuracy. + shareTolerance: 1e15, + minTransactionAmount: 1e15, + positionDuration: POSITION_DURATION, + enableBaseDeposits: true, + enableShareDeposits: false, + enableBaseWithdraws: true, + enableShareWithdraws: false + }); + + /// @dev Instantiates the Instance testing suite with the configuration. + constructor() InstanceTest(__testConfig) {} + + /// @dev Forge function that is invoked to setup the testing environment. + function setUp() public override __mainnet_fork(20_276_503) { + // Invoke the Instance testing suite setup. + super.setUp(); + } + + /// Overrides /// + + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return + abi.encode( + IMorphoBlueHyperdrive.MorphoBlueParams({ + morpho: MORPHO, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }) + ); + } + + /// @dev Converts base amount to the equivalent about in shares. + function convertToShares( + uint256 baseAmount + ) internal view override returns (uint256) { + return + MorphoBlueConversions.convertToShares( + MORPHO, + IERC20(LOAN_TOKEN), + COLLATERAL_TOKEN, + ORACLE, + IRM, + LLTV, + baseAmount + ); + } + + /// @dev Converts share amount to the equivalent amount in base. + function convertToBase( + uint256 shareAmount + ) internal view override returns (uint256) { + return + MorphoBlueConversions.convertToBase( + MORPHO, + IERC20(LOAN_TOKEN), + COLLATERAL_TOKEN, + ORACLE, + IRM, + LLTV, + shareAmount + ); + } + + /// @dev Deploys the Morpho Blue deployer coordinator contract. + /// @param _factory The address of the Hyperdrive factory. + function deployCoordinator( + address _factory + ) internal override returns (address) { + vm.startPrank(alice); + return + address( + new MorphoBlueHyperdriveDeployerCoordinator( + string.concat(__testConfig.name, "DeployerCoordinator"), + _factory, + address(new MorphoBlueHyperdriveCoreDeployer()), + address(new MorphoBlueTarget0Deployer()), + address(new MorphoBlueTarget1Deployer()), + address(new MorphoBlueTarget2Deployer()), + address(new MorphoBlueTarget3Deployer()), + address(new MorphoBlueTarget4Deployer()) + ) + ); + } + + /// @dev Fetches the total supply of the base and share tokens. + function getSupply() internal view override returns (uint256, uint256) { + (uint256 totalSupplyAssets, uint256 totalSupplyShares, , ) = MORPHO + .expectedMarketBalances( + MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }) + ); + return (totalSupplyAssets, totalSupplyShares); + } + + /// @dev Fetches the token balance information of an account. + function getTokenBalances( + address account + ) internal view override returns (uint256, uint256) { + return ( + IERC20(LOAN_TOKEN).balanceOf(account), + MORPHO + .position( + MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }).id(), + account + ) + .supplyShares + ); + } + + /// @dev Verifies that deposit accounting is correct when opening positions. + function verifyDeposit( + address trader, + uint256 amountPaid, + bool asBase, + uint256 totalBaseBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal view override { + // Vault shares deposits are not supported for this instance. + if (!asBase) { + revert IHyperdrive.UnsupportedToken(); + } + + // Ensure that the total supply increased by the base paid. + (uint256 totalSupplyAssets, uint256 totalSupplyShares, , ) = MORPHO + .expectedMarketBalances( + MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }) + ); + assertApproxEqAbs(totalSupplyAssets, totalBaseBefore + amountPaid, 1); + assertApproxEqAbs( + totalSupplyShares, + totalSharesBefore + hyperdrive.convertToShares(amountPaid), + 1 + ); + + // Ensure that the ETH balances didn't change. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(bob.balance, traderBalancesBefore.ETHBalance); + + // Ensure that the Hyperdrive instance's base balance doesn't change + // and that the trader's base balance decreased by the amount paid. + assertEq( + IERC20(LOAN_TOKEN).balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.baseBalance + ); + assertEq( + IERC20(LOAN_TOKEN).balanceOf(trader), + traderBalancesBefore.baseBalance - amountPaid + ); + + // Ensure that the shares balances were updated correctly. + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + (, uint256 traderSharesAfter) = getTokenBalances(address(trader)); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveBalancesBefore.sharesBalance + + hyperdrive.convertToShares(amountPaid), + 2 + ); + assertEq(traderSharesAfter, traderBalancesBefore.sharesBalance); + } + + /// @dev Verifies that withdrawal accounting is correct when closing positions. + function verifyWithdrawal( + address trader, + uint256 baseProceeds, + bool asBase, + uint256 totalBaseBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal view override { + // Vault shares withdrawals are not supported for this instance. + if (!asBase) { + revert IHyperdrive.UnsupportedToken(); + } + + // Ensure that the total supply decreased by the base proceeds. + (uint256 totalSupplyAssets, uint256 totalSupplyShares, , ) = MORPHO + .expectedMarketBalances( + MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }) + ); + assertApproxEqAbs(totalSupplyAssets, totalBaseBefore - baseProceeds, 1); + assertApproxEqAbs( + totalSupplyShares, + totalSharesBefore - hyperdrive.convertToShares(baseProceeds), + 1e6 + ); + + // Ensure that the ETH balances didn't change. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(bob.balance, traderBalancesBefore.ETHBalance); + + // Ensure that the base balances Hyperdrive base balance doesn't + // change and that the trader's base balance decreased by the amount + // paid. + assertApproxEqAbs( + IERC20(LOAN_TOKEN).balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.baseBalance, + 1 + ); + assertEq( + IERC20(LOAN_TOKEN).balanceOf(trader), + traderBalancesBefore.baseBalance + baseProceeds + ); + + // Ensure that the shares balances were updated correctly. + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + (, uint256 traderSharesAfter) = getTokenBalances(address(trader)); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveBalancesBefore.sharesBalance - + hyperdrive.convertToShares(baseProceeds), + 1e6 + ); + assertApproxEqAbs( + traderSharesAfter, + traderBalancesBefore.sharesBalance, + 1 + ); + } + + /// Price Per Share /// + + function test__pricePerVaultShare(uint256 basePaid) external { + // Ensure that the share price is the expected value. + (uint256 totalSupplyAssets, uint256 totalSupplyShares, , ) = MORPHO + .expectedMarketBalances( + MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }) + ); + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + assertEq(vaultSharePrice, totalSupplyAssets.divDown(totalSupplyShares)); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of shares. This will + // be an approximation. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + hyperdrive.calculateMaxLong() + ); + (, uint256 hyperdriveSharesBefore) = getTokenBalances( + address(hyperdrive) + ); + openLong(bob, basePaid); + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveSharesBefore + basePaid.divDown(vaultSharePrice), + __testConfig.shareTolerance + ); + } + + /// LP /// + + function test_round_trip_lp_instantaneous() external { + // Bob adds liquidity with base. + uint256 contribution = 2_500e18; + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + contribution + ); + uint256 lpShares = addLiquidity(bob, contribution); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob removes his liquidity with base as the target asset. + (uint256 baseProceeds, uint256 withdrawalShares) = removeLiquidity( + bob, + lpShares + ); + assertEq(withdrawalShares, 0); + + // Bob should receive approximately as much base as he contributed since + // no time as passed and the fees are zero. + assertApproxEqAbs(baseProceeds, contribution, 1e10); + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_round_trip_lp_withdrawal_shares() external { + // Bob adds liquidity with base. + uint256 contribution = 2_500e18; + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + contribution + ); + uint256 lpShares = addLiquidity(bob, contribution); + + // Alice opens a large short. + vm.stopPrank(); + vm.startPrank(alice); + uint256 shortAmount = hyperdrive.calculateMaxShort(); + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + shortAmount + ); + openShort(alice, shortAmount); + + // Bob removes his liquidity with base as the target asset. + (uint256 baseProceeds, uint256 withdrawalShares) = removeLiquidity( + bob, + lpShares + ); + assertEq(baseProceeds, 0); + assertGt(withdrawalShares, 0); + + // The term passes and interest accrues. + advanceTime(POSITION_DURATION, 1.421e18); + + // Bob should be able to redeem all of his withdrawal shares for + // approximately the LP share price. + uint256 lpSharePrice = hyperdrive.getPoolInfo().lpSharePrice; + uint256 withdrawalSharesRedeemed; + (baseProceeds, withdrawalSharesRedeemed) = redeemWithdrawalShares( + bob, + withdrawalShares + ); + assertEq(withdrawalSharesRedeemed, withdrawalShares); + + // Bob should receive base approximately equal in value to his present + // value. + assertApproxEqAbs( + baseProceeds, + withdrawalShares.mulDown(lpSharePrice), + 1e9 + ); + } + + /// Long /// + + function test_open_long_nonpayable() external { + vm.startPrank(bob); + + // Ensure that sending ETH to `openLong` fails. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.openLong{ value: 2e18 }( + 1e18, + 0, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Ensure that sending ETH to `openShort` fails. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.openLong{ value: 0.5e18 }( + 1e18, + 0, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + } + + function test_round_trip_long_instantaneous() external { + // Bob opens a long with base. + uint256 basePaid = hyperdrive.calculateMaxLong(); + IERC20(hyperdrive.baseToken()).approve(address(hyperdrive), basePaid); + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with base as the target asset. + uint256 baseProceeds = closeLong(bob, maturityTime, longAmount); + + // Bob should receive approximately as much base as he paid since no + // time as passed and the fees are zero. + assertApproxEqAbs(baseProceeds, basePaid, 1e9); + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_round_trip_long_maturity() external { + // Bob opens a long with base. + uint256 basePaid = hyperdrive.calculateMaxLong(); + IERC20(hyperdrive.baseToken()).approve(address(hyperdrive), basePaid); + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + + // Advance the time and accrue a large amount of interest. + advanceTime(POSITION_DURATION, 137.123423e18); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with base as the target asset. + uint256 baseProceeds = closeLong(bob, maturityTime, longAmount); + + // Bob should receive almost exactly his bond amount. + assertApproxEqAbs(baseProceeds, longAmount, 2); + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + /// Short /// + + function test_open_short_nonpayable() external { + vm.startPrank(bob); + + // Ensure that sending ETH to `openLong` fails. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.openShort{ value: 2e18 }( + 1e18, + 1e18, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Ensure that Bob receives a refund when he opens a short with "asBase" + // set to false and sends ether to the contract. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.openShort{ value: 0.5e18 }( + 1e18, + 1e18, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + } + + function test_round_trip_short_instantaneous() external { + // Bob opens a short with base. + uint256 shortAmount = hyperdrive.calculateMaxShort(); + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + shortAmount + ); + (uint256 maturityTime, uint256 basePaid) = openShort(bob, shortAmount); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with base as the target asset. + uint256 baseProceeds = closeShort(bob, maturityTime, shortAmount); + + // Bob should receive approximately as much base as he paid since no + // time as passed and the fees are zero. + assertApproxEqAbs(baseProceeds, basePaid, 1e9); + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_round_trip_short_maturity() external { + // Bob opens a short with base. + uint256 shortAmount = hyperdrive.calculateMaxShort(); + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + shortAmount + ); + (uint256 maturityTime, ) = openShort(bob, shortAmount); + + // The term passes and some interest accrues. + int256 variableAPR = 0.57e18; + advanceTime(POSITION_DURATION, variableAPR); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with base as the target asset. + uint256 baseProceeds = closeShort(bob, maturityTime, shortAmount); + + // Bob should receive almost exactly the interest that accrued on the + // bonds that were shorted. + assertApproxEqAbs( + baseProceeds, + shortAmount.mulDown(uint256(variableAPR)), + 1e9 + ); + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + /// Helpers /// + + function advanceTime( + uint256 timeDelta, + int256 variableRate + ) internal override { + // Advance the time. + vm.warp(block.timestamp + timeDelta); + + // Accrue interest in the Morpho market. This amounts to manually + // updating the total supply assets and the last update time. + Id marketId = MarketParams({ + loanToken: LOAN_TOKEN, + collateralToken: COLLATERAL_TOKEN, + oracle: ORACLE, + irm: IRM, + lltv: LLTV + }).id(); + Market memory market = MORPHO.market(marketId); + uint256 totalSupplyAssets = variableRate >= 0 + ? market.totalSupplyAssets + + uint256(market.totalSupplyAssets).mulDown(uint256(variableRate)) + : market.totalSupplyAssets - + uint256(market.totalSupplyAssets).mulDown( + uint256(-variableRate) + ); + bytes32 marketLocation = keccak256(abi.encode(marketId, 3)); + vm.store( + address(MORPHO), + marketLocation, + bytes32( + (uint256(market.totalSupplyShares) << 128) | totalSupplyAssets + ) + ); + vm.store( + address(MORPHO), + bytes32(uint256(marketLocation) + 2), + bytes32((uint256(market.fee) << 128) | uint256(block.timestamp)) + ); + } +} diff --git a/test/instances/reth/RETHHyperdrive.t.sol b/test/instances/reth/RETHHyperdrive.t.sol index f7a697620..5353fadba 100644 --- a/test/instances/reth/RETHHyperdrive.t.sol +++ b/test/instances/reth/RETHHyperdrive.t.sol @@ -81,6 +81,12 @@ contract RETHHyperdriveTest is InstanceTest { /// Overrides /// + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + /// @dev Converts base amount to the equivalent amount in rETH. function convertToShares( uint256 baseAmount diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol index 55dd86c40..2d20feae0 100644 --- a/test/instances/steth/StETHHyperdrive.t.sol +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -69,6 +69,12 @@ contract StETHHyperdriveTest is InstanceTest { /// Overrides /// + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + /// @dev Converts base amount to the equivalent about in stETH. function convertToShares( uint256 baseAmount diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index d03b8e527..c2d5a1514 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -125,22 +125,46 @@ abstract contract InstanceTest is HyperdriveTest { DEFAULT_DEPLOYMENT_ID, // Deployment Id DEFAULT_DEPLOYMENT_SALT, // Deployment Salt contribution, // Contribution - false // asBase + !config.enableShareDeposits // asBase ); - config.vaultSharesToken.approve(address(hyperdrive), 100_000e18); - vm.startPrank(bob); - config.vaultSharesToken.approve(address(hyperdrive), 100_000e18); + // If base deposits are supported, approve a large amount of shares for + // Alice and Bob. + if (config.enableBaseDeposits && !isBaseETH) { + config.baseToken.approve(address(hyperdrive), 100_000e18); + vm.startPrank(bob); + config.baseToken.approve(address(hyperdrive), 100_000e18); + } + + // If share deposits are supported, approve a large amount of shares for + // Alice and Bob. + if (config.enableShareDeposits) { + config.vaultSharesToken.approve(address(hyperdrive), 100_000e18); + vm.startPrank(bob); + config.vaultSharesToken.approve(address(hyperdrive), 100_000e18); + } // Ensure that Alice received the correct amount of LP tokens. She should // receive LP shares totaling the amount of shares that she contributed // minus the shares set aside for the minimum share reserves and the // zero address's initial LP contribution. - assertApproxEqAbs( - hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), - contribution - 2 * hyperdrive.getPoolConfig().minimumShareReserves, - config.shareTolerance - ); + if (config.enableShareDeposits) { + assertApproxEqAbs( + hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), + contribution - + 2 * + hyperdrive.getPoolConfig().minimumShareReserves, + config.shareTolerance + ); + } else { + assertApproxEqAbs( + hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), + convertToShares(contribution) - + 2 * + hyperdrive.getPoolConfig().minimumShareReserves, + config.shareTolerance + ); + } // Start recording event logs. vm.recordLogs(); @@ -173,7 +197,7 @@ abstract contract InstanceTest is HyperdriveTest { deploymentId, deployerCoordinator, poolConfig, - new bytes(0), + getExtraData(), FIXED_RATE, FIXED_RATE, i, @@ -182,10 +206,11 @@ abstract contract InstanceTest is HyperdriveTest { } // Alice gives approval to the deployer coordinator to fund the market. - if (config.enableBaseDeposits && !isBaseETH) { + if (asBase && !isBaseETH) { config.baseToken.approve(deployerCoordinator, 100_000e18); + } else if (!asBase) { + config.vaultSharesToken.approve(deployerCoordinator, 100_000e18); } - config.vaultSharesToken.approve(deployerCoordinator, 100_000e18); // We expect the deployAndInitialize to fail with an // UnsupportedToken error if depositing with base are not supported. @@ -216,7 +241,7 @@ abstract contract InstanceTest is HyperdriveTest { deployerCoordinator, config.name, poolConfig, - new bytes(0), + getExtraData(), contribution, FIXED_RATE, FIXED_RATE, @@ -319,6 +344,10 @@ abstract contract InstanceTest is HyperdriveTest { address _factory ) internal virtual returns (address); + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal view virtual returns (bytes memory); + /// @dev A virtual function that converts an amount in terms of the base token /// to equivalent amount in shares. /// @param baseAmount Amount in terms of base. @@ -426,6 +455,11 @@ abstract contract InstanceTest is HyperdriveTest { function test__deployAndInitialize__asBase() external virtual { uint256 aliceBalanceBefore = address(alice).balance; + // Early termination if base deposits are not supported. + if (!config.enableBaseDeposits) { + return; + } + // Contribution in terms of base. uint256 contribution = 1_000e18; @@ -441,11 +475,6 @@ abstract contract InstanceTest is HyperdriveTest { true // asBase ); - // Early termination if base deposits are not supported. - if (!config.enableBaseDeposits) { - return; - } - // If base deposits are enabled we verify some assertions. if (isBaseETH) { // If the base token is ETH we assert the ETH balance is correct. @@ -461,9 +490,9 @@ abstract contract InstanceTest is HyperdriveTest { // zero address's initial LP contribution. assertApproxEqAbs( hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), - contribution.divDown( - hyperdrive.getPoolConfig().initialVaultSharePrice - ) - 2 * hyperdrive.getPoolConfig().minimumShareReserves, + hyperdrive.convertToShares(contribution) - + 2 * + hyperdrive.getPoolConfig().minimumShareReserves, config.shareTolerance // Custom share tolerance per instance. ); @@ -488,7 +517,7 @@ abstract contract InstanceTest is HyperdriveTest { FIXED_RATE, true, hyperdrive.getPoolConfig().minimumShareReserves, - new bytes(0), + getExtraData(), config.shareTolerance ); } @@ -498,6 +527,11 @@ abstract contract InstanceTest is HyperdriveTest { function test__deployAndInitialize__asShares() external { uint256 aliceBalanceBefore = address(alice).balance; + // Early termination if share deposits are not supported. + if (!config.enableShareDeposits) { + return; + } + // Contribution in terms of base. uint256 contribution = 1_000e18; @@ -512,11 +546,6 @@ abstract contract InstanceTest is HyperdriveTest { false // asBase ); - // Early termination if share deposits are not supported. - if (!config.enableShareDeposits) { - return; - } - // Ensure Alice's ETH balance remains the same. assertEq(address(alice).balance, aliceBalanceBefore); @@ -562,6 +591,11 @@ abstract contract InstanceTest is HyperdriveTest { /// share deposits are not supported. /// @param basePaid Amount in terms of base to open a long. function test_open_long_with_shares(uint256 basePaid) external { + // Early termination if share deposits are not supported. + if (!config.enableShareDeposits) { + return; + } + // Get balance information before opening a long. ( uint256 totalBaseSupplyBefore, @@ -604,11 +638,6 @@ abstract contract InstanceTest is HyperdriveTest { }) ); - // Early termination if share deposits are not supported. - if (!config.enableShareDeposits) { - return; - } - // Ensure that Bob received the correct amount of bonds. assertEq( hyperdrive.balanceOf( @@ -734,6 +763,11 @@ abstract contract InstanceTest is HyperdriveTest { uint256 basePaid, int256 variableRate ) external virtual { + // Early termination if share withdrawals are not supported. + if (!config.enableShareWithdraws) { + return; + } + // Get Bob's account balances before opening the long. AccountBalances memory bobBalancesBefore = getAccountBalances(bob); @@ -819,22 +853,40 @@ abstract contract InstanceTest is HyperdriveTest { // Calculate the maximum amount of basePaid we can test. The limit is // either the maximum long that Hyperdrive can open or the amount of the - // share token the trader has. + // base token the trader has. uint256 maxLongAmount = HyperdriveUtils.calculateMaxLong(hyperdrive); - uint256 maxShareAmount = bobBalancesBefore.sharesBalance; - // We normalize the basePaid variable within a valid range the market can support. - basePaid = basePaid.normalizeToRange( - 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - maxLongAmount > maxShareAmount ? maxShareAmount : maxLongAmount - ); + // Open a long in either base or shares, depending on which asset is + // supported. + uint256 maturityTime; + uint256 longAmount; + if (config.enableBaseDeposits) { + // We normalize the basePaid variable within a valid range the market + // can support. + uint256 maxBaseAmount = bobBalancesBefore.baseBalance; + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxLongAmount > maxBaseAmount ? maxBaseAmount : maxLongAmount + ); - // Bob opens a long with the share token. - (uint256 maturityTime, uint256 longAmount) = openLong( - bob, - convertToShares(basePaid), - false - ); + // Bob opens a long with the base token. + (maturityTime, longAmount) = openLong(bob, basePaid); + } else { + // We normalize the sharesPaid variable within a valid range the market + // can support. + uint256 maxSharesAmount = bobBalancesBefore.sharesBalance; + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxLongAmount + ); + + // Bob opens a long with the share token. + (maturityTime, longAmount) = openLong( + bob, + convertToShares(basePaid).min(maxSharesAmount), + false + ); + } // The term passes and some interest accrues. variableRate = variableRate.normalizeToRange(0, 2.5e18); @@ -987,6 +1039,11 @@ abstract contract InstanceTest is HyperdriveTest { /// base deposits are not supported. /// @param shortAmount Amount of bonds to short. function test_open_short_with_shares(uint256 shortAmount) external { + // Early termination if base deposits are not supported. + if (!config.enableShareDeposits) { + return; + } + // Get some balance information before opening a short. ( uint256 totalBaseSupplyBefore, @@ -1021,11 +1078,6 @@ abstract contract InstanceTest is HyperdriveTest { }) ); - // Early termination if base deposits are not supported. - if (!config.enableShareDeposits) { - return; - } - // Ensure that Bob received the correct amount of shorted bonds. assertEq( hyperdrive.balanceOf( @@ -1080,12 +1132,17 @@ abstract contract InstanceTest is HyperdriveTest { int256(FIXED_RATE) ); - // Bob opens a short with the share token. + // Bob opens a short with the base token if base deposits are supported + // and the shares token if they aren't. shortAmount = shortAmount.normalizeToRange( 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, HyperdriveUtils.calculateMaxShort(hyperdrive) ); - (uint256 maturityTime, ) = openShort(bob, shortAmount, false); + (uint256 maturityTime, ) = openShort( + bob, + shortAmount, + config.enableBaseDeposits + ); // The term passes and interest accrues. uint256 startingVaultSharePrice = hyperdrive @@ -1131,7 +1188,6 @@ abstract contract InstanceTest is HyperdriveTest { // Convert proceeds to the base token and ensure the proper about of // interest was credited to Bob. - // uint256 baseProceeds = convertToBase(shareProceeds); assertLe(baseProceeds, expectedBaseProceeds + 10); assertApproxEqAbs(baseProceeds, expectedBaseProceeds, 100); @@ -1154,6 +1210,11 @@ abstract contract InstanceTest is HyperdriveTest { uint256 shortAmount, int256 variableRate ) external virtual { + // Early termination if share withdrawals are not supported. + if (!config.enableShareWithdraws) { + return; + } + // Accrue interest for a term to ensure that the share price is greater // than one. advanceTime( @@ -1200,11 +1261,6 @@ abstract contract InstanceTest is HyperdriveTest { false ); - // Early termination if share withdraws are not supported. - if (!config.enableShareWithdraws) { - return; - } - // Convert proceeds to the base token and ensure the proper about of // interest was credited to Bob. uint256 baseProceeds = convertToBase(shareProceeds); @@ -1226,10 +1282,14 @@ abstract contract InstanceTest is HyperdriveTest { /// Sweep /// function test_sweep_failure_directSweep() external { - vm.startPrank(factory.sweepCollector()); + // Return early if the vault shares token is zero. + address vaultSharesToken = hyperdrive.vaultSharesToken(); + if (vaultSharesToken == address(0)) { + return; + } // Fails to sweep the vault shares token. - address vaultSharesToken = hyperdrive.vaultSharesToken(); + vm.startPrank(factory.sweepCollector()); vm.expectRevert(IHyperdrive.SweepFailed.selector); hyperdrive.sweep(IERC20(vaultSharesToken)); }