diff --git a/.github/workflows/testnet_docker.yml b/.github/workflows/testnet_docker.yml index 2142e5241..233d13312 100644 --- a/.github/workflows/testnet_docker.yml +++ b/.github/workflows/testnet_docker.yml @@ -54,18 +54,13 @@ jobs: build-args: | ADMIN=${{ vars.TESTNET_ADMIN }} IS_COMPETITION_MODE=true - BASE_TOKEN_NAME=Wrapped Ether - BASE_TOKEN_SYMBOL=WETH - VAULT_NAME=stETH - VAULT_SYMBOL=STETH - VAULT_STARTING_RATE=35000000000000000 # 3.5% APR - HYPERDRIVE_CONTRIBUTION=50000000000000000000000 # 50,000 ETH - HYPERDRIVE_FIXED_RATE=35000000000000000 # 3.5% APR - HYPERDRIVE_INITIAL_SHARE_PRICE=1000000000000000000 # 1 base/share - HYPERDRIVE_MINIMUM_SHARE_RESERVES=1000000000000000 # 1e15 - HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT=1000000000000 # 1e12 - HYPERDRIVE_POSITION_DURATION=31536000 # 1 year in seconds - HYPERDRIVE_CHECKPOINT_DURATION=86400 # 1 day in seconds - HYPERDRIVE_TIME_STRETCH_APR=35000000000000000 # 3.5% APR - HYPERDRIVE_ORACLE_SIZE=10 # 10 recorded spot prices - HYPERDRIVE_UPDATE_GAP=3600 # 1 hour in seconds + BASE_TOKEN_NAME=Multi Collateral DAI + BASE_TOKEN_SYMBOL=DAI + VAULT_NAME=sDai + VAULT_SYMBOL=SDAI + VAULT_STARTING_RATE=50000000000000000 # 5% APR + LIDO_STARTING_RATE=35000000000000000 # 3.5% APR + ERC4626_HYPERDRIVE_POSITION_DURATION=31536000 # 1 year in seconds + ERC4626_HYPERDRIVE_CHECKPOINT_DURATION=86400 # 1 day in seconds + STETH_HYPERDRIVE_POSITION_DURATION=31536000 # 1 year in seconds + STETH_HYPERDRIVE_CHECKPOINT_DURATION=86400 # 1 day in seconds diff --git a/Dockerfile b/Dockerfile index 36e7008b4..503e819a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,23 +30,32 @@ ARG ADMIN ARG IS_COMPETITION_MODE ARG BASE_TOKEN_NAME ARG BASE_TOKEN_SYMBOL +ARG VAULT_NAME +ARG VAULT_SYMBOL ARG VAULT_STARTING_RATE -ARG HYPERDRIVE_CONTRIBUTION -ARG HYPERDRIVE_FIXED_RATE -ARG HYPERDRIVE_INITIAL_SHARE_PRICE -ARG HYPERDRIVE_MINIMUM_SHARE_RESERVES -ARG HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT -ARG HYPERDRIVE_POSITION_DURATION -ARG HYPERDRIVE_CHECKPOINT_DURATION -ARG HYPERDRIVE_TIME_STRETCH_APR -ARG HYPERDRIVE_ORACLE_SIZE -ARG HYPERDRIVE_UPDATE_GAP +ARG LIDO_STARTING_RATE +ARG ERC4626_HYPERDRIVE_CONTRIBUTION +ARG ERC4626_HYPERDRIVE_FIXED_RATE +ARG ERC4626_HYPERDRIVE_INITIAL_SHARE_PRICE +ARG ERC4626_HYPERDRIVE_MINIMUM_SHARE_RESERVES +ARG ERC4626_HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT +ARG ERC4626_HYPERDRIVE_POSITION_DURATION +ARG ERC4626_HYPERDRIVE_CHECKPOINT_DURATION +ARG ERC4626_HYPERDRIVE_TIME_STRETCH_APR +ARG STETH_HYPERDRIVE_CONTRIBUTION +ARG STETH_HYPERDRIVE_FIXED_RATE +ARG STETH_HYPERDRIVE_INITIAL_SHARE_PRICE +ARG STETH_HYPERDRIVE_MINIMUM_SHARE_RESERVES +ARG STETH_HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT +ARG STETH_HYPERDRIVE_POSITION_DURATION +ARG STETH_HYPERDRIVE_CHECKPOINT_DURATION +ARG STETH_HYPERDRIVE_TIME_STRETCH_APR # Run anvil as a background process. We run the migrations against this anvil # node and dump the state into the "./data" directory. At runtime, the consumer # can start anvil with the "--load-state ./data" flag to start up anvil with # the post-migrations state. -RUN anvil --dump-state ./data --code-size-limit 9999999999 & \ +RUN anvil --dump-state ./data --balance 100000 & \ ANVIL="$!" && \ sleep 2 && \ ./migrate.sh && \ diff --git a/Makefile b/Makefile index 5dd397d68..742d75fa5 100644 --- a/Makefile +++ b/Makefile @@ -30,9 +30,8 @@ test-rust: lint: make lint-sol && make lint-rust -# FIXME: Add the code-size-check back once we've created a target2 contract. lint-sol: - make solhint && make style-check && make spell-check && make warnings-check + make solhint && make style-check && make spell-check && make warnings-check && make code-size-check code-size-check: FOUNDRY_PROFILE=production forge build && python3 python/contract_size.py out diff --git a/contracts/src/instances/ERC4626HyperdriveDeployer.sol b/contracts/src/deployers/HyperdriveDeployerCoordinator.sol similarity index 74% rename from contracts/src/instances/ERC4626HyperdriveDeployer.sol rename to contracts/src/deployers/HyperdriveDeployerCoordinator.sol index 13a0b7b13..d307c31dc 100644 --- a/contracts/src/instances/ERC4626HyperdriveDeployer.sol +++ b/contracts/src/deployers/HyperdriveDeployerCoordinator.sol @@ -1,54 +1,49 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { ERC4626Hyperdrive } from "../instances/ERC4626Hyperdrive.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IERC4626HyperdriveDeployer } from "../interfaces/IERC4626HyperdriveDeployer.sol"; +import { IHyperdriveCoreDeployer } from "../interfaces/IHyperdriveCoreDeployer.sol"; import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; import { IHyperdriveTargetDeployer } from "../interfaces/IHyperdriveTargetDeployer.sol"; -import { ONE } from "../libraries/FixedPointMath.sol"; /// @author DELV -/// @title ERC4626HyperdriveDeployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// Hyperdrive and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. -/// @dev We use two contracts to avoid any code size limit issues with Hyperdrive. +/// @title HyperdriveDeployerCoordinator +/// @notice This Hyperdrive deployer coordinates the process of deploying the +/// Hyperdrive system utilizing several child deployers. +/// @dev We use multiple deployers to avoid the maximum code size. /// @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 ERC4626HyperdriveDeployer is IHyperdriveDeployer { +abstract contract HyperdriveDeployerCoordinator is IHyperdriveDeployer { /// @notice The contract used to deploy new instances of Hyperdrive. - address public immutable hyperdriveCoreDeployer; + address public immutable coreDeployer; - /// @notice The contract used to deploy new instances of Hyperdrive target0. + /// @notice The contract used to deploy new instances of HyperdriveTarget0. address public immutable target0Deployer; - /// @notice The contract used to deploy new instances of Hyperdrive target1. + /// @notice The contract used to deploy new instances of HyperdriveTarget1. address public immutable target1Deployer; - /// @notice The contract used to deploy new instances of Hyperdrive target2. + /// @notice The contract used to deploy new instances of HyperdriveTarget2. address public immutable target2Deployer; - /// @notice The contract used to deploy new instances of Hyperdrive target3. + /// @notice The contract used to deploy new instances of HyperdriveTarget3. address public immutable target3Deployer; - /// @notice Initializes the contract with the given parameters. - /// @param _hyperdriveCoreDeployer The contract used to deploy new instances - /// of Hyperdrive. + /// @notice Instantiates the deployer coordinator. + /// @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. constructor( - address _hyperdriveCoreDeployer, + address _coreDeployer, address _target0Deployer, address _target1Deployer, address _target2Deployer, address _target3Deployer ) { - hyperdriveCoreDeployer = _hyperdriveCoreDeployer; + coreDeployer = _coreDeployer; target0Deployer = _target0Deployer; target1Deployer = _target1Deployer; target2Deployer = _target2Deployer; @@ -63,13 +58,10 @@ contract ERC4626HyperdriveDeployer is IHyperdriveDeployer { IHyperdrive.PoolDeployConfig memory _deployConfig, bytes memory _extraData ) external override returns (address) { - // Decode the extra data to extract the pool address. - (address pool, ) = abi.decode(_extraData, (address, address[])); - // Convert the deploy config into the pool config and set the initial // share price. IHyperdrive.PoolConfig memory _config = _copyPoolConfig(_deployConfig); - _config.initialSharePrice = IERC4626(pool).convertToAssets(ONE); + _config.initialSharePrice = _getInitialSharePrice(_extraData); // Deploy the target0 contract. address target0 = IHyperdriveTargetDeployer(target0Deployer).deploy( @@ -89,9 +81,9 @@ contract ERC4626HyperdriveDeployer is IHyperdriveDeployer { _extraData ); - // Deploy the ERC4626Hyperdrive instance. + // Deploy the Hyperdrive instance. return - IERC4626HyperdriveDeployer(hyperdriveCoreDeployer).deploy( + IHyperdriveCoreDeployer(coreDeployer).deploy( _config, _extraData, target0, @@ -101,6 +93,13 @@ contract ERC4626HyperdriveDeployer is IHyperdriveDeployer { ); } + /// @dev Gets the initial share price of the Hyperdrive pool. + /// @param _extraData The extra data passed to the child deployers. + /// @return The initial share price of the Hyperdrive pool. + function _getInitialSharePrice( + bytes memory _extraData + ) internal view virtual returns (uint256); + /// @notice Copies the deploy config into a pool config. /// @param _deployConfig The deploy configuration of the Hyperdrive pool. /// @return _config The pool configuration of the Hyperdrive pool. diff --git a/contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol b/contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol similarity index 71% rename from contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol rename to contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol index 11fffc6de..09b2ff15b 100644 --- a/contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol +++ b/contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol @@ -1,20 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IERC4626HyperdriveDeployer } from "../interfaces/IERC4626HyperdriveDeployer.sol"; -import { ERC4626Hyperdrive } from "../instances/ERC4626Hyperdrive.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveCoreDeployer } from "../../interfaces/IHyperdriveCoreDeployer.sol"; +import { ERC4626Hyperdrive } from "../../instances/erc4626/ERC4626Hyperdrive.sol"; /// @author DELV /// @title ERC4626HyperdriveCoreDeployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// the hyperdrive contract and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. +/// @notice The core deployer for the ERC4626Hyperdrive 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 ERC4626HyperdriveCoreDeployer is IERC4626HyperdriveDeployer { +contract ERC4626HyperdriveCoreDeployer is IHyperdriveCoreDeployer { /// @notice Deploys a Hyperdrive instance with the given parameters. /// @param _config The configuration of the Hyperdrive pool. /// @param _extraData The extra data that contains the ERC4626 vault. @@ -31,9 +29,8 @@ contract ERC4626HyperdriveCoreDeployer is IERC4626HyperdriveDeployer { address target2, address target3 ) external override returns (address) { - address pool = abi.decode(_extraData, (address)); - // Deploy the ERC4626Hyperdrive instance. + address pool = abi.decode(_extraData, (address)); return ( address( new ERC4626Hyperdrive( diff --git a/contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol b/contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol new file mode 100644 index 000000000..5bbbd912f --- /dev/null +++ b/contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { ONE } from "../../libraries/FixedPointMath.sol"; +import { HyperdriveDeployerCoordinator } from "../HyperdriveDeployerCoordinator.sol"; + +/// @author DELV +/// @title ERC4626HyperdriveDeployerCoordinator +/// @notice The deployer coordinator for the ERC4626Hyperdrive 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 ERC4626HyperdriveDeployerCoordinator is HyperdriveDeployerCoordinator { + /// @notice Instantiates the deployer coordinator. + /// @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. + constructor( + address _coreDeployer, + address _target0Deployer, + address _target1Deployer, + address _target2Deployer, + address _target3Deployer + ) + HyperdriveDeployerCoordinator( + _coreDeployer, + _target0Deployer, + _target1Deployer, + _target2Deployer, + _target3Deployer + ) + {} + + /// @dev Gets the initial share price of the Hyperdrive pool. + /// @param _extraData The extra data passed to the child deployers. + /// @return The initial share price of the Hyperdrive pool. + function _getInitialSharePrice( + bytes memory _extraData + ) internal view override returns (uint256) { + // Return the vault's current share price. + IERC4626 pool = IERC4626(abi.decode(_extraData, (address))); + return pool.convertToAssets(ONE); + } +} diff --git a/contracts/src/instances/ERC4626Target0Deployer.sol b/contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol similarity index 55% rename from contracts/src/instances/ERC4626Target0Deployer.sol rename to contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol index 18bcdd2a8..879010357 100644 --- a/contracts/src/instances/ERC4626Target0Deployer.sol +++ b/contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveTargetDeployer } from "../interfaces/IHyperdriveTargetDeployer.sol"; -import { ERC4626Target0 } from "../instances/ERC4626Target0.sol"; +import { ERC4626Target0 } from "../../instances/erc4626/ERC4626Target0.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; /// @author DELV /// @title ERC4626Target0Deployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// the target0 contract and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. -/// @dev We use two contracts to avoid any code size limit issues with Hyperdrive. +/// @notice The target0 deployer for the ERC4626Hyperdrive 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. @@ -24,8 +21,8 @@ contract ERC4626Target0Deployer is IHyperdriveTargetDeployer { IHyperdrive.PoolConfig memory _config, bytes memory _extraData ) external override returns (address) { - (address pool, ) = abi.decode(_extraData, (address, address[])); // Deploy the ERC4626Target0 instance. - return (address(new ERC4626Target0(_config, IERC4626(pool)))); + IERC4626 pool = IERC4626(abi.decode(_extraData, (address))); + return address(new ERC4626Target0(_config, pool)); } } diff --git a/contracts/src/instances/ERC4626Target1Deployer.sol b/contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol similarity index 55% rename from contracts/src/instances/ERC4626Target1Deployer.sol rename to contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol index 87ca3414f..a05c5a9bb 100644 --- a/contracts/src/instances/ERC4626Target1Deployer.sol +++ b/contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveTargetDeployer } from "../interfaces/IHyperdriveTargetDeployer.sol"; -import { ERC4626Target1 } from "../instances/ERC4626Target1.sol"; +import { ERC4626Target1 } from "../../instances/erc4626/ERC4626Target1.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; /// @author DELV /// @title ERC4626Target1Deployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// the target1 contract and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. -/// @dev We use two contracts to avoid any code size limit issues with Hyperdrive. +/// @notice The target1 deployer for the ERC4626Hyperdrive 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. @@ -24,8 +21,8 @@ contract ERC4626Target1Deployer is IHyperdriveTargetDeployer { IHyperdrive.PoolConfig memory _config, bytes memory _extraData ) external override returns (address) { - (address pool, ) = abi.decode(_extraData, (address, address[])); // Deploy the ERC4626Target1 instance. - return address(new ERC4626Target1(_config, IERC4626(pool))); + IERC4626 pool = IERC4626(abi.decode(_extraData, (address))); + return address(new ERC4626Target1(_config, pool)); } } diff --git a/contracts/src/instances/ERC4626Target2Deployer.sol b/contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol similarity index 55% rename from contracts/src/instances/ERC4626Target2Deployer.sol rename to contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol index d11029d4e..b3fb02aa4 100644 --- a/contracts/src/instances/ERC4626Target2Deployer.sol +++ b/contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveTargetDeployer } from "../interfaces/IHyperdriveTargetDeployer.sol"; -import { ERC4626Target2 } from "../instances/ERC4626Target2.sol"; +import { ERC4626Target2 } from "../../instances/erc4626/ERC4626Target2.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; /// @author DELV /// @title ERC4626Target2Deployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// the target2 contract and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. -/// @dev We use two contracts to avoid any code size limit issues with Hyperdrive. +/// @notice The target2 deployer for the ERC4626Hyperdrive 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. @@ -24,8 +21,8 @@ contract ERC4626Target2Deployer is IHyperdriveTargetDeployer { IHyperdrive.PoolConfig memory _config, bytes memory _extraData ) external override returns (address) { - (address pool, ) = abi.decode(_extraData, (address, address[])); // Deploy the ERC4626Target2 instance. - return address(new ERC4626Target2(_config, IERC4626(pool))); + IERC4626 pool = IERC4626(abi.decode(_extraData, (address))); + return address(new ERC4626Target2(_config, pool)); } } diff --git a/contracts/src/instances/ERC4626Target3Deployer.sol b/contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol similarity index 55% rename from contracts/src/instances/ERC4626Target3Deployer.sol rename to contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol index cede8903b..eee744983 100644 --- a/contracts/src/instances/ERC4626Target3Deployer.sol +++ b/contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveTargetDeployer } from "../interfaces/IHyperdriveTargetDeployer.sol"; -import { ERC4626Target3 } from "../instances/ERC4626Target3.sol"; +import { ERC4626Target3 } from "../../instances/erc4626/ERC4626Target3.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; /// @author DELV /// @title ERC4626Target3Deployer -/// @notice This is a minimal factory which contains only the logic to deploy -/// the target3 contract and is called by a more complex factory which -/// initializes the Hyperdrive instances and acts as a registry. -/// @dev We use two contracts to avoid any code size limit issues with Hyperdrive. +/// @notice The target3 deployer for the ERC4626Hyperdrive 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. @@ -24,8 +21,8 @@ contract ERC4626Target3Deployer is IHyperdriveTargetDeployer { IHyperdrive.PoolConfig memory _config, bytes memory _extraData ) external override returns (address) { - (address pool, ) = abi.decode(_extraData, (address, address[])); // Deploy the ERC4626Target3 instance. - return address(new ERC4626Target3(_config, IERC4626(pool))); + IERC4626 pool = IERC4626(abi.decode(_extraData, (address))); + return address(new ERC4626Target3(_config, pool)); } } diff --git a/contracts/src/deployers/steth/StETHHyperdriveCoreDeployer.sol b/contracts/src/deployers/steth/StETHHyperdriveCoreDeployer.sol new file mode 100644 index 000000000..c6b6ebd22 --- /dev/null +++ b/contracts/src/deployers/steth/StETHHyperdriveCoreDeployer.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveCoreDeployer } from "../../interfaces/IHyperdriveCoreDeployer.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHHyperdrive } from "../../instances/steth/StETHHyperdrive.sol"; + +/// @author DELV +/// @title StETHHyperdriveCoreDeployer +/// @notice The core deployer for the StETHHyperdrive 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 StETHHyperdriveCoreDeployer is IHyperdriveCoreDeployer { + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instanstiates the core deployer. + /// @param _lido The Lido contract. + constructor(ILido _lido) { + lido = _lido; + } + + /// @notice Deploys a Hyperdrive instance with the given parameters. + /// @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. + /// @return The address of the newly deployed StETHHyperdrive Instance. + function deploy( + IHyperdrive.PoolConfig memory _config, + bytes memory, // unused extra data + address target0, + address target1, + address target2, + address target3 + ) external override returns (address) { + return ( + address( + new StETHHyperdrive( + _config, + target0, + target1, + target2, + target3, + lido + ) + ) + ); + } +} diff --git a/contracts/src/deployers/steth/StETHHyperdriveDeployerCoordinator.sol b/contracts/src/deployers/steth/StETHHyperdriveDeployerCoordinator.sol new file mode 100644 index 000000000..824bc8b16 --- /dev/null +++ b/contracts/src/deployers/steth/StETHHyperdriveDeployerCoordinator.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { ILido } from "../../interfaces/ILido.sol"; +import { FixedPointMath, ONE } from "../../libraries/FixedPointMath.sol"; +import { HyperdriveDeployerCoordinator } from "../HyperdriveDeployerCoordinator.sol"; + +/// @author DELV +/// @title StETHHyperdriveDeployerCoordinator +/// @notice The deployer coordinator for the StETHHyperdrive 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 StETHHyperdriveDeployerCoordinator is HyperdriveDeployerCoordinator { + using FixedPointMath for uint256; + + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instantiates the deployer coordinator. + /// @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 _lido The Lido contract. + constructor( + address _coreDeployer, + address _target0Deployer, + address _target1Deployer, + address _target2Deployer, + address _target3Deployer, + ILido _lido + ) + HyperdriveDeployerCoordinator( + _coreDeployer, + _target0Deployer, + _target1Deployer, + _target2Deployer, + _target3Deployer + ) + { + lido = _lido; + } + + /// @dev Gets the initial share price of the Hyperdrive pool. + /// @return The initial share price of the Hyperdrive pool. + function _getInitialSharePrice( + bytes memory // unused extra data + ) internal view override returns (uint256) { + // Return the stETH's current share price. + return lido.getTotalPooledEther().divDown(lido.getTotalShares()); + } +} diff --git a/contracts/src/deployers/steth/StETHTarget0Deployer.sol b/contracts/src/deployers/steth/StETHTarget0Deployer.sol new file mode 100644 index 000000000..7af308c09 --- /dev/null +++ b/contracts/src/deployers/steth/StETHTarget0Deployer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { StETHTarget0 } from "../../instances/steth/StETHTarget0.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { ILido } from "../../interfaces/ILido.sol"; + +/// @author DELV +/// @title StETHTarget0Deployer +/// @notice The target0 deployer for the StETHHyperdrive 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 StETHTarget0Deployer is IHyperdriveTargetDeployer { + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instanstiates the target0 deployer. + /// @param _lido The Lido contract. + constructor(ILido _lido) { + lido = _lido; + } + + /// @notice Deploys a target0 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @return The address of the newly deployed StETHTarget0 Instance. + function deploy( + IHyperdrive.PoolConfig memory _config, + bytes memory // unused extra data + ) external override returns (address) { + // Deploy the StETHTarget0 instance. + return address(new StETHTarget0(_config, lido)); + } +} diff --git a/contracts/src/deployers/steth/StETHTarget1Deployer.sol b/contracts/src/deployers/steth/StETHTarget1Deployer.sol new file mode 100644 index 000000000..0cadc1b5d --- /dev/null +++ b/contracts/src/deployers/steth/StETHTarget1Deployer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { StETHTarget1 } from "../../instances/steth/StETHTarget1.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { ILido } from "../../interfaces/ILido.sol"; + +/// @author DELV +/// @title StETHTarget1Deployer +/// @notice The target1 deployer for the StETHHyperdrive 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 StETHTarget1Deployer is IHyperdriveTargetDeployer { + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instanstiates the target1 deployer. + /// @param _lido The Lido contract. + constructor(ILido _lido) { + lido = _lido; + } + + /// @notice Deploys a target1 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @return The address of the newly deployed StETHTarget1 Instance. + function deploy( + IHyperdrive.PoolConfig memory _config, + bytes memory // unused extra data + ) external override returns (address) { + // Deploy the StETHTarget1 instance. + return address(new StETHTarget1(_config, lido)); + } +} diff --git a/contracts/src/deployers/steth/StETHTarget2Deployer.sol b/contracts/src/deployers/steth/StETHTarget2Deployer.sol new file mode 100644 index 000000000..fd320cfed --- /dev/null +++ b/contracts/src/deployers/steth/StETHTarget2Deployer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { StETHTarget2 } from "../../instances/steth/StETHTarget2.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { ILido } from "../../interfaces/ILido.sol"; + +/// @author DELV +/// @title StETHTarget2Deployer +/// @notice The target2 deployer for the StETHHyperdrive 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 StETHTarget2Deployer is IHyperdriveTargetDeployer { + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instanstiates the target2 deployer. + /// @param _lido The Lido contract. + constructor(ILido _lido) { + lido = _lido; + } + + /// @notice Deploys a target2 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @return The address of the newly deployed StETHTarget2 Instance. + function deploy( + IHyperdrive.PoolConfig memory _config, + bytes memory // unused extra data + ) external override returns (address) { + // Deploy the StETHTarget2 instance. + return address(new StETHTarget2(_config, lido)); + } +} diff --git a/contracts/src/deployers/steth/StETHTarget3Deployer.sol b/contracts/src/deployers/steth/StETHTarget3Deployer.sol new file mode 100644 index 000000000..b95b42725 --- /dev/null +++ b/contracts/src/deployers/steth/StETHTarget3Deployer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { StETHTarget3 } from "../../instances/steth/StETHTarget3.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IHyperdriveTargetDeployer } from "../../interfaces/IHyperdriveTargetDeployer.sol"; +import { ILido } from "../../interfaces/ILido.sol"; + +/// @author DELV +/// @title StETHTarget3Deployer +/// @notice The target3 deployer for the StETHHyperdrive 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 StETHTarget3Deployer is IHyperdriveTargetDeployer { + /// @notice The Lido contract. + ILido public immutable lido; + + /// @notice Instanstiates the target3 deployer. + /// @param _lido The Lido contract. + constructor(ILido _lido) { + lido = _lido; + } + + /// @notice Deploys a target3 instance with the given parameters. + /// @param _config The configuration of the Hyperdrive pool. + /// @return The address of the newly deployed StETHTarget3 Instance. + function deploy( + IHyperdrive.PoolConfig memory _config, + bytes memory // unused extra data + ) external override returns (address) { + // Deploy the StETHTarget3 instance. + return address(new StETHTarget3(_config, lido)); + } +} diff --git a/contracts/src/factory/HyperdriveFactory.sol b/contracts/src/factory/HyperdriveFactory.sol index 68655b8be..1896cbd82 100644 --- a/contracts/src/factory/HyperdriveFactory.sol +++ b/contracts/src/factory/HyperdriveFactory.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; +import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; @@ -14,6 +16,7 @@ import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; /// particular legal or regulatory significance. contract HyperdriveFactory { using FixedPointMath for uint256; + using SafeTransferLib for ERC20; /// @notice Emitted when governance is transferred. event GovernanceUpdated(address indexed governance); @@ -263,7 +266,8 @@ contract HyperdriveFactory { /// by default. /// @param _hyperdriveDeployer Address of the hyperdrive deployer. /// @param _deployConfig The deploy configuration of the Hyperdrive pool. - /// @param _extraData The extra data that contains data necessary for the specific deployer. + /// @param _extraData The extra data that contains data necessary for the + /// specific deployer. /// @param _contribution Base token to call init with /// @param _apr The apr to call init with /// @param _initializeExtraData The extra data for the `initialize` call. @@ -276,15 +280,7 @@ contract HyperdriveFactory { uint256 _apr, bytes memory _initializeExtraData ) public payable virtual returns (IHyperdrive) { - if (msg.value > 0) { - revert IHyperdrive.NonPayableInitialization(); - } - - // TODO: Should we do some input validation on the config like making - // sure that the linker factory and linker code hash are set to zero? - // This kind of check makes it clear that the deployer knows the values - // will be overridden. - + // Ensure that the target deployer has been registered. if (!isHyperdriveDeployer[_hyperdriveDeployer]) { revert IHyperdrive.InvalidDeployer(); } @@ -320,28 +316,57 @@ contract HyperdriveFactory { isInstance[address(hyperdrive)] = true; // Initialize the Hyperdrive instance. - _deployConfig.baseToken.transferFrom( - msg.sender, - address(this), - _contribution - ); - if ( - !_deployConfig.baseToken.approve( + uint256 refund; + if (msg.value >= _contribution) { + // Only the contribution amount of ether will be passed to + // Hyperdrive. + refund = msg.value - _contribution; + + // Initialize the Hyperdrive instance. + hyperdrive.initialize{ value: _contribution }( + _contribution, + _apr, + IHyperdrive.Options({ + destination: msg.sender, + asBase: true, + extraData: _initializeExtraData + }) + ); + } else { + // None of the provided ether is used for the contribution. + refund = msg.value; + + // Transfer the contribution to this contract and set an approval + // on Hyperdrive to prepare for initialization. + ERC20(address(_deployConfig.baseToken)).safeTransferFrom( + msg.sender, + address(this), + _contribution + ); + ERC20(address(_deployConfig.baseToken)).safeApprove( address(hyperdrive), - type(uint256).max - ) - ) { - revert IHyperdrive.ApprovalFailed(); + _contribution + ); + + // Initialize the Hyperdrive instance. + hyperdrive.initialize( + _contribution, + _apr, + IHyperdrive.Options({ + destination: msg.sender, + asBase: true, + extraData: _initializeExtraData + }) + ); + } + + // Refund any excess ether that was sent to this contract. + if (refund > 0) { + (bool success, ) = payable(msg.sender).call{ value: refund }(""); + if (!success) { + revert IHyperdrive.TransferFailed(); + } } - hyperdrive.initialize( - _contribution, - _apr, - IHyperdrive.Options({ - destination: msg.sender, - asBase: true, - extraData: _initializeExtraData - }) - ); // Set the default pausers and transfer the governance status to the // hyperdrive governance address. diff --git a/contracts/src/instances/ERC4626Base.sol b/contracts/src/instances/erc4626/ERC4626Base.sol similarity index 93% rename from contracts/src/instances/ERC4626Base.sol rename to contracts/src/instances/erc4626/ERC4626Base.sol index b8657f394..e14c1e82d 100644 --- a/contracts/src/instances/ERC4626Base.sol +++ b/contracts/src/instances/erc4626/ERC4626Base.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.19; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IERC4626Hyperdrive } from "../interfaces/IERC4626Hyperdrive.sol"; -import { HyperdriveBase } from "../internal/HyperdriveBase.sol"; -import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IERC4626Hyperdrive } from "../../interfaces/IERC4626Hyperdrive.sol"; +import { HyperdriveBase } from "../../internal/HyperdriveBase.sol"; +import { FixedPointMath, ONE } from "../../libraries/FixedPointMath.sol"; /// @author DELV /// @title ERC4626Base diff --git a/contracts/src/instances/ERC4626Hyperdrive.sol b/contracts/src/instances/erc4626/ERC4626Hyperdrive.sol similarity index 81% rename from contracts/src/instances/ERC4626Hyperdrive.sol rename to contracts/src/instances/erc4626/ERC4626Hyperdrive.sol index 346f35ab4..d0605708e 100644 --- a/contracts/src/instances/ERC4626Hyperdrive.sol +++ b/contracts/src/instances/erc4626/ERC4626Hyperdrive.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.19; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; -import { Hyperdrive } from "../external/Hyperdrive.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IERC4626Hyperdrive } from "../interfaces/IERC4626Hyperdrive.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; +import { Hyperdrive } from "../../external/Hyperdrive.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IERC4626Hyperdrive } from "../../interfaces/IERC4626Hyperdrive.sol"; +import { FixedPointMath } from "../../libraries/FixedPointMath.sol"; import { ERC4626Base } from "./ERC4626Base.sol"; /// @author DELV @@ -39,8 +39,9 @@ contract ERC4626Hyperdrive is Hyperdrive, ERC4626Base { Hyperdrive(_config, _target0, _target1, _target2, _target3) ERC4626Base(__pool) { - // Ensure that the Hyperdrive pool was configured properly. - // WARN: 4626 implementations should be checked that if they use an + // Ensure that the initial share price is properly configured. + // + // WARN: ERC4626 implementations should be checked that if they use an // asset with decimals less than 18 that the preview deposit is scale // invariant. EG - because this line uses a very large query to load // price for USDC if the price per share changes based on size of @@ -49,6 +50,9 @@ contract ERC4626Hyperdrive is Hyperdrive, ERC4626Base { if (_config.initialSharePrice != _pricePerShare()) { revert IHyperdrive.InvalidInitialSharePrice(); } + + // Ensure that the base token is the same as the vault's underlying + // asset. if (address(_config.baseToken) != IERC4626(_pool).asset()) { revert IHyperdrive.InvalidBaseToken(); } diff --git a/contracts/src/instances/ERC4626Target0.sol b/contracts/src/instances/erc4626/ERC4626Target0.sol similarity index 89% rename from contracts/src/instances/ERC4626Target0.sol rename to contracts/src/instances/erc4626/ERC4626Target0.sol index ae09baf63..edabf8d00 100644 --- a/contracts/src/instances/ERC4626Target0.sol +++ b/contracts/src/instances/erc4626/ERC4626Target0.sol @@ -3,15 +3,15 @@ pragma solidity 0.8.19; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; -import { HyperdriveTarget0 } from "../external/HyperdriveTarget0.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { HyperdriveTarget0 } from "../../external/HyperdriveTarget0.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; import { ERC4626Base } from "./ERC4626Base.sol"; /// @author DELV /// @title ERC4626Target0 -/// @notice ERC4626Hyperdrive's target 0 logic contract. This contract contains +/// @notice ERC4626Hyperdrive'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 diff --git a/contracts/src/instances/ERC4626Target1.sol b/contracts/src/instances/erc4626/ERC4626Target1.sol similarity index 66% rename from contracts/src/instances/ERC4626Target1.sol rename to contracts/src/instances/erc4626/ERC4626Target1.sol index 2f90967b5..3a6cc704c 100644 --- a/contracts/src/instances/ERC4626Target1.sol +++ b/contracts/src/instances/erc4626/ERC4626Target1.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { HyperdriveTarget1 } from "../external/HyperdriveTarget1.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { HyperdriveTarget1 } from "../../external/HyperdriveTarget1.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; import { ERC4626Base } from "./ERC4626Base.sol"; /// @author DELV /// @title ERC4626Target1 -/// @notice ERC4626Hyperdrive's target 1 logic contract. This contract contains -/// several stateful functions that couldn't fit into the Hyperdrive -/// contract. +/// @notice ERC4626Hyperdrive's target1 logic 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. diff --git a/contracts/src/instances/ERC4626Target2.sol b/contracts/src/instances/erc4626/ERC4626Target2.sol similarity index 75% rename from contracts/src/instances/ERC4626Target2.sol rename to contracts/src/instances/erc4626/ERC4626Target2.sol index fd36f7871..e9fadf9dd 100644 --- a/contracts/src/instances/ERC4626Target2.sol +++ b/contracts/src/instances/erc4626/ERC4626Target2.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { HyperdriveTarget2 } from "../external/HyperdriveTarget2.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { HyperdriveTarget2 } from "../../external/HyperdriveTarget2.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; import { ERC4626Base } from "./ERC4626Base.sol"; /// @author DELV /// @title ERC4626Target2 -/// @notice ERC4626Hyperdrive's target 2 logic contract. This contract contains +/// @notice ERC4626Hyperdrive'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 diff --git a/contracts/src/instances/ERC4626Target3.sol b/contracts/src/instances/erc4626/ERC4626Target3.sol similarity index 75% rename from contracts/src/instances/ERC4626Target3.sol rename to contracts/src/instances/erc4626/ERC4626Target3.sol index 1f0569757..e018866c6 100644 --- a/contracts/src/instances/ERC4626Target3.sol +++ b/contracts/src/instances/erc4626/ERC4626Target3.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { HyperdriveTarget3 } from "../external/HyperdriveTarget3.sol"; -import { IERC4626 } from "../interfaces/IERC4626.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { HyperdriveTarget3 } from "../../external/HyperdriveTarget3.sol"; +import { IERC4626 } from "../../interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; import { ERC4626Base } from "./ERC4626Base.sol"; /// @author DELV /// @title ERC4626Target3 -/// @notice ERC4626Hyperdrive's target 3 logic contract. This contract contains +/// @notice ERC4626Hyperdrive'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 diff --git a/contracts/src/instances/steth/StETHBase.sol b/contracts/src/instances/steth/StETHBase.sol new file mode 100644 index 000000000..a06fa1c1f --- /dev/null +++ b/contracts/src/instances/steth/StETHBase.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { HyperdriveBase } from "../../internal/HyperdriveBase.sol"; +import { FixedPointMath } from "../../libraries/FixedPointMath.sol"; + +/// @author DELV +/// @title StethHyperdrive +/// @notice The base contract for the stETH Hyperdrive implementation. +/// @dev Lido has it's own notion of shares to account for the accrual of +/// interest on the ether pooled in the Lido protocol. Instead of +/// maintaining a balance of shares, this integration can simply use Lido +/// shares directly. +/// @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 StETHBase is HyperdriveBase { + using FixedPointMath for uint256; + + /// @dev The Lido contract. + ILido internal immutable _lido; + + /// @notice Instantiates the stETH Hyperdrive base contract. + /// @param __lido The Lido contract. + constructor(ILido __lido) { + _lido = __lido; + + // Ensure that the minimum share reserves are equal to 1e15. This value + // has been tested to prevent arithmetic overflows in the + // `_updateLiquidity` function when the share reserves are as high as + // 200 million. + if (_minimumShareReserves != 1e15) { + revert IHyperdrive.InvalidMinimumShareReserves(); + } + } + + /// Yield Source /// + + /// @dev Accepts a transfer from the user in base or the yield source token. + /// @param _amount The amount of token to transfer. It will be in either + /// base or shares depending on the `asBase` option. + /// @param _options The options that configure the deposit. The only option + /// used in this implementation is "asBase" which determines if + /// the deposit is settled in ETH or stETH shares. + /// @return shares The amount of shares that represents the amount deposited. + /// @return sharePrice The current share price. + function _deposit( + uint256 _amount, + IHyperdrive.Options calldata _options + ) internal override returns (uint256 shares, uint256 sharePrice) { + uint256 refund; + if (_options.asBase) { + // Ensure that sufficient ether was provided. + if (msg.value < _amount) { + revert IHyperdrive.TransferFailed(); + } + + // If the user sent more ether than the amount specified, refund the + // excess ether. + refund = msg.value - _amount; + + // Submit the provided ether to Lido to be deposited. The fee + // collector address is passed as the referral address; however, + // users can specify whatever referrer they'd like by depositing + // stETH instead of WETH. + shares = _lido.submit{ value: _amount }(_feeCollector); + + // Calculate the share price. + sharePrice = _pricePerShare(); + } else { + // Refund any ether that was sent to the contract. + refund = msg.value; + + // Transfer stETH shares into the contract. + _lido.transferSharesFrom(msg.sender, address(this), _amount); + + // Calculate the share price. + shares = _amount; + sharePrice = _pricePerShare(); + } + + // Return excess ether that was sent to the contract. + if (refund > 0) { + (bool success, ) = payable(msg.sender).call{ value: refund }(""); + if (!success) { + revert IHyperdrive.TransferFailed(); + } + } + + return (shares, sharePrice); + } + + /// @notice Processes a trader's withdrawal. This yield source only supports + /// withdrawals in stETH shares. + /// @param _shares The amount of shares to withdraw from Hyperdrive. + /// @param _options The options that configure the withdrawal. The options + /// used in this implementation are "destination" which specifies the + /// recipient of the withdrawal and "asBase" which determines + /// if the withdrawal is settled in base or vault shares. The "asBase" + /// option must be false since stETH withdrawals aren't processed + /// instantaneously. Users that want to withdraw can manage their + /// withdrawal separately. + /// @return The amount of shares withdrawn from the yield source. + function _withdraw( + uint256 _shares, + IHyperdrive.Options calldata _options + ) internal override returns (uint256) { + // stETH withdrawals aren't necessarily instantaneous. Users that want + // to withdraw can manage their withdrawal separately. + if (_options.asBase) { + revert IHyperdrive.UnsupportedToken(); + } + + // If we're withdrawing zero shares, short circuit and return 0. + if (_shares == 0) { + return 0; + } + + // Transfer the stETH shares to the destination. + _lido.transferShares(_options.destination, _shares); + + return _shares; + } + + /// @dev Returns the current share price. We simply use Lido's share price. + /// @return price The current share price. + /// @dev must remain consistent with the impl inside of the DataProvider + function _pricePerShare() internal view override returns (uint256 price) { + return _lido.getTotalPooledEther().divDown(_lido.getTotalShares()); + } + + /// @dev We override the message value check since this integration is + /// payable. + function _checkMessageValue() internal pure override {} +} diff --git a/contracts/src/instances/steth/StETHHyperdrive.sol b/contracts/src/instances/steth/StETHHyperdrive.sol new file mode 100644 index 000000000..386efae7d --- /dev/null +++ b/contracts/src/instances/steth/StETHHyperdrive.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { Hyperdrive } from "../../external/Hyperdrive.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHBase } from "./StETHBase.sol"; + +/// @author DELV +/// @title StETHHyperdrive +/// @notice A Hyperdrive instance that uses StETH 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 StETHHyperdrive is Hyperdrive, StETHBase { + address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @notice Instantiates Hyperdrive with StETH as the yield source. + /// @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 _lido The Lido contract. + constructor( + IHyperdrive.PoolConfig memory _config, + address _target0, + address _target1, + address _target2, + address _target3, + ILido _lido + ) + Hyperdrive(_config, _target0, _target1, _target2, _target3) + StETHBase(_lido) + { + // Ensure that the base token address is properly configured. + if (address(_config.baseToken) != ETH) { + revert IHyperdrive.InvalidBaseToken(); + } + + // Ensure that the initial share price is properly configured. + if (_config.initialSharePrice != _pricePerShare()) { + revert IHyperdrive.InvalidInitialSharePrice(); + } + } + + /// @notice Some yield sources [eg Morpho] pay rewards directly to this + /// contract but we can't handle distributing them internally so we + /// sweep to the fee collector address to then redistribute to users. + /// @dev WARN: It is unlikely but possible that there is a selector overlap + /// with 'transferFrom'. Any integrating contracts should be checked + /// for that, as it may result in an unexpected call from this address. + function sweep(IERC20) external { + _delegate(target0); + } +} diff --git a/contracts/src/instances/steth/StETHTarget0.sol b/contracts/src/instances/steth/StETHTarget0.sol new file mode 100644 index 000000000..2cc339362 --- /dev/null +++ b/contracts/src/instances/steth/StETHTarget0.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; +import { HyperdriveTarget0 } from "../../external/HyperdriveTarget0.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHBase } from "./StETHBase.sol"; + +/// @author DELV +/// @title StETHTarget0 +/// @notice StETHHyperdrive'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 StETHTarget0 is HyperdriveTarget0, StETHBase { + using SafeTransferLib for ERC20; + + /// @notice Initializes the target0 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _lido The Lido contract. + constructor( + IHyperdrive.PoolConfig memory _config, + ILido _lido + ) HyperdriveTarget0(_config) StETHBase(_lido) {} + + /// Extras /// + + /// @notice Some yield sources [eg Morpho] pay rewards directly to this + /// contract but we can't handle distributing them internally so we + /// sweep to the fee collector address to then redistribute to users. + /// @dev WARN: It is unlikely but possible that there is a selector overlap + /// with 'transferFrom'. Any integrating contracts should be checked + /// for that, as it may result in an unexpected call from this address. + /// @param _target The token to sweep. + function sweep(IERC20 _target) external { + // Ensure that the sender is the fee collector or a pauser. + if (msg.sender != _feeCollector && !_pausers[msg.sender]) { + revert IHyperdrive.Unauthorized(); + } + + // Ensure that thet target isn't the stETH token. + if (address(_target) == address(_lido)) { + revert IHyperdrive.UnsupportedToken(); + } + + // Get Hyperdrive's balance of stETH tokens prior to sweeping. + uint256 stETHBalance = _lido.balanceOf(address(this)); + + // Transfer the entire balance of the sweep target to the fee collector. + uint256 balance = _target.balanceOf(address(this)); + ERC20(address(_target)).safeTransfer(_feeCollector, balance); + + // Ensure that the stETH balance hasn't changed. + if (_lido.balanceOf(address(this)) != stETHBalance) { + revert IHyperdrive.SweepFailed(); + } + } + + /// Getters /// + + /// @notice Returns the Lido contract. + /// @return lido The Lido contract. + function lido() external view returns (ILido) { + _revert(abi.encode(_lido)); + } +} diff --git a/contracts/src/instances/steth/StETHTarget1.sol b/contracts/src/instances/steth/StETHTarget1.sol new file mode 100644 index 000000000..29b17ca03 --- /dev/null +++ b/contracts/src/instances/steth/StETHTarget1.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { HyperdriveTarget1 } from "../../external/HyperdriveTarget1.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHBase } from "./StETHBase.sol"; + +/// @author DELV +/// @title StETHTarget1 +/// @notice StETHHyperdrive's target1 logic 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 StETHTarget1 is HyperdriveTarget1, StETHBase { + /// @notice Initializes the target1 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _lido The Lido contract. + constructor( + IHyperdrive.PoolConfig memory _config, + ILido _lido + ) HyperdriveTarget1(_config) StETHBase(_lido) {} +} diff --git a/contracts/src/instances/steth/StETHTarget2.sol b/contracts/src/instances/steth/StETHTarget2.sol new file mode 100644 index 000000000..c41c48123 --- /dev/null +++ b/contracts/src/instances/steth/StETHTarget2.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { HyperdriveTarget2 } from "../../external/HyperdriveTarget2.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHBase } from "./StETHBase.sol"; + +/// @author DELV +/// @title StETHTarget2 +/// @notice StETHHyperdrive's target2 logic 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 StETHTarget2 is HyperdriveTarget2, StETHBase { + /// @notice Initializes the target2 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _lido The Lido contract. + constructor( + IHyperdrive.PoolConfig memory _config, + ILido _lido + ) HyperdriveTarget2(_config) StETHBase(_lido) {} +} diff --git a/contracts/src/instances/steth/StETHTarget3.sol b/contracts/src/instances/steth/StETHTarget3.sol new file mode 100644 index 000000000..5c37b5427 --- /dev/null +++ b/contracts/src/instances/steth/StETHTarget3.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { HyperdriveTarget3 } from "../../external/HyperdriveTarget3.sol"; +import { IHyperdrive } from "../../interfaces/IHyperdrive.sol"; +import { ILido } from "../../interfaces/ILido.sol"; +import { StETHBase } from "./StETHBase.sol"; + +/// @author DELV +/// @title StETHTarget3 +/// @notice StETHHyperdrive's target3 logic 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 StETHTarget3 is HyperdriveTarget3, StETHBase { + /// @notice Initializes the target3 contract. + /// @param _config The configuration of the Hyperdrive pool. + /// @param _lido The Lido contract. + constructor( + IHyperdrive.PoolConfig memory _config, + ILido _lido + ) HyperdriveTarget3(_config) StETHBase(_lido) {} +} diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index 98d1ea681..21dda1a50 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -253,7 +253,6 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { /// ################## /// ### Hyperdrive ### /// ################## - error ApprovalFailed(); error BelowMinimumContribution(); error BelowMinimumShareReserves(); error EndIndexTooLarge(); @@ -367,4 +366,8 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { function target0() external view returns (address); function target1() external view returns (address); + + function target2() external view returns (address); + + function target3() external view returns (address); } diff --git a/contracts/src/interfaces/IERC4626HyperdriveDeployer.sol b/contracts/src/interfaces/IHyperdriveCoreDeployer.sol similarity index 89% rename from contracts/src/interfaces/IERC4626HyperdriveDeployer.sol rename to contracts/src/interfaces/IHyperdriveCoreDeployer.sol index d6a88ce6d..65b730d14 100644 --- a/contracts/src/interfaces/IERC4626HyperdriveDeployer.sol +++ b/contracts/src/interfaces/IHyperdriveCoreDeployer.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import { IHyperdrive } from "./IHyperdrive.sol"; -interface IERC4626HyperdriveDeployer { +interface IHyperdriveCoreDeployer { function deploy( IHyperdrive.PoolConfig memory _config, bytes memory _extraData, diff --git a/contracts/src/interfaces/ILido.sol b/contracts/src/interfaces/ILido.sol index 0ec21f7c5..1af2b760c 100644 --- a/contracts/src/interfaces/ILido.sol +++ b/contracts/src/interfaces/ILido.sol @@ -11,6 +11,12 @@ interface ILido is IERC20 { uint256 _sharesAmount ) external returns (uint256); + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256); + function getBufferedEther() external view returns (uint256); function getTotalPooledEther() external view returns (uint256); diff --git a/contracts/src/interfaces/IStETHHyperdrive.sol b/contracts/src/interfaces/IStETHHyperdrive.sol new file mode 100644 index 000000000..ced14f345 --- /dev/null +++ b/contracts/src/interfaces/IStETHHyperdrive.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IHyperdrive } from "./IHyperdrive.sol"; +import { IStETHHyperdriveCore } from "./IStETHHyperdriveCore.sol"; +import { IStETHHyperdriveRead } from "./IStETHHyperdriveRead.sol"; + +// prettier-ignore +interface IStETHHyperdrive is + IHyperdrive, + IStETHHyperdriveRead, + IStETHHyperdriveCore +{} diff --git a/contracts/src/interfaces/IStETHHyperdriveCore.sol b/contracts/src/interfaces/IStETHHyperdriveCore.sol new file mode 100644 index 000000000..5116c83e8 --- /dev/null +++ b/contracts/src/interfaces/IStETHHyperdriveCore.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC20 } from "./IERC20.sol"; +import { IHyperdriveCore } from "./IHyperdriveCore.sol"; + +interface IStETHHyperdriveCore is IHyperdriveCore { + function sweep(IERC20 _target) external; +} diff --git a/contracts/src/interfaces/IStETHHyperdriveRead.sol b/contracts/src/interfaces/IStETHHyperdriveRead.sol new file mode 100644 index 000000000..453c10eb9 --- /dev/null +++ b/contracts/src/interfaces/IStETHHyperdriveRead.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IHyperdriveRead } from "./IHyperdriveRead.sol"; +import { ILido } from "./ILido.sol"; + +interface IStETHHyperdriveRead is IHyperdriveRead { + function lido() external view returns (ILido); +} diff --git a/contracts/src/interfaces/IWETH.sol b/contracts/src/interfaces/IWETH.sol deleted file mode 100644 index 20d72cab9..000000000 --- a/contracts/src/interfaces/IWETH.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IERC20 } from "./IERC20.sol"; - -interface IWETH is IERC20 { - function deposit() external payable; - - function withdraw(uint256 wad) external; -} diff --git a/contracts/test/MockERC4626Hyperdrive.sol b/contracts/test/MockERC4626Hyperdrive.sol index e70144ff9..2657f9b18 100644 --- a/contracts/test/MockERC4626Hyperdrive.sol +++ b/contracts/test/MockERC4626Hyperdrive.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { ERC4626Hyperdrive, IHyperdrive, IERC4626 } from "contracts/src/instances/ERC4626Hyperdrive.sol"; +import { ERC4626Hyperdrive } from "contracts/src/instances/erc4626/ERC4626Hyperdrive.sol"; +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; // This contract stubs out the yield source implementations of `ERC4626Hyperdrive` // so that we can test the `ERC4626Hyperdrive` contract in isolation. diff --git a/contracts/test/MockLido.sol b/contracts/test/MockLido.sol new file mode 100644 index 000000000..49a34d7ca --- /dev/null +++ b/contracts/test/MockLido.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { Authority } from "solmate/auth/Auth.sol"; +import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; +import { ILido } from "../src/interfaces/ILido.sol"; +import { FixedPointMath } from "../src/libraries/FixedPointMath.sol"; +import { ERC20Mintable } from "./ERC20Mintable.sol"; + +/// @author DELV +/// @title MockLido +/// @notice This mock yield source will accrue interest at a specified rate +/// Every stateful interaction will accrue interest, so the interest +/// accrual will approximate continuous compounding as the contract +/// is called more frequently. +/// @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 MockLido is MultiRolesAuthority, ERC20Mintable { + using FixedPointMath for uint256; + + // Interest State + uint256 internal _rate; + uint256 internal _lastUpdated; + + // Lido State + uint256 totalPooledEther; + uint256 totalShares; + + constructor( + uint256 _initialRate, + address _admin, + bool _isCompetitionMode + ) + ERC20Mintable( + "Liquid staked Ether 2.0", + "stETH", + 18, + _admin, + _isCompetitionMode + ) + { + _rate = _initialRate; + _lastUpdated = block.timestamp; + } + + /// Overrides /// + + function submit(address) external payable returns (uint256) { + // Accrue interest. + _accrue(); + + // If this is the first deposit, mint shares 1:1. + if (getTotalShares() == 0) { + totalShares = msg.value; + totalPooledEther = msg.value; + _mint(msg.sender, msg.value); + return msg.value; + } + + // Calculate the amount of stETH shares that should be minted. + uint256 shares = msg.value.mulDivDown( + getTotalShares(), + getTotalPooledEther() + ); + + // Update the Lido state. + totalPooledEther += msg.value; + totalShares += shares; + + // Mint the stETH tokens to the user. + _mint(msg.sender, msg.value); + + return shares; + } + + function transferShares( + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + // Accrue interest. + _accrue(); + + // Calculate the amount of tokens that should be transferred. + uint256 tokenAmount = _sharesAmount.mulDivDown( + getTotalPooledEther(), + getTotalShares() + ); + + // Transfer the tokens to the user. + transfer(_recipient, tokenAmount); + + return tokenAmount; + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + // Accrue interest. + _accrue(); + + // Calculate the amount of tokens that should be transferred. + uint256 tokenAmount = _sharesAmount.mulDivDown( + getTotalPooledEther(), + getTotalShares() + ); + + // Transfer the tokens to the user. + transferFrom(_sender, _recipient, tokenAmount); + + return tokenAmount; + } + + function getBufferedEther() external pure returns (uint256) { + return 0; + } + + function getTotalPooledEther() public view returns (uint256) { + return totalPooledEther + _getAccruedInterest(); + } + + function getTotalShares() public view returns (uint256) { + return totalShares; + } + + function sharesOf(address _account) external view returns (uint256) { + uint256 tokenBalance = balanceOf[_account]; + return tokenBalance.mulDivDown(getTotalShares(), getTotalPooledEther()); + } + + /// Mock /// + + function setRate(uint256 _rate_) external requiresAuthDuringCompetition { + _accrue(); + _rate = _rate_; + } + + function getRate() external view returns (uint256) { + return _rate; + } + + function _accrue() internal { + uint256 interest = _getAccruedInterest(); + if (interest > 0) { + totalPooledEther += interest; + } + _lastUpdated = block.timestamp; + } + + function _getAccruedInterest() internal view returns (uint256) { + if (_rate == 0) { + return 0; + } + + // base_balance = base_balance * (1 + r * t) + uint256 timeElapsed = (block.timestamp - _lastUpdated).divDown( + 365 days + ); + uint256 accrued = totalPooledEther.mulDown(_rate.mulDown(timeElapsed)); + return accrued; + } +} diff --git a/crates/test-utils/src/chain/test_chain.rs b/crates/test-utils/src/chain/test_chain.rs index cacb04269..93478e4a7 100644 --- a/crates/test-utils/src/chain/test_chain.rs +++ b/crates/test-utils/src/chain/test_chain.rs @@ -335,6 +335,8 @@ impl TestChain { // Get the contract addresses of the vault and the targets. let target0_address = hyperdrive.target_0().call().await?; let target1_address = hyperdrive.target_1().call().await?; + let target2_address = hyperdrive.target_2().call().await?; + let target3_address = hyperdrive.target_3().call().await?; let vault_address = hyperdrive.pool().call().await?; // Deploy templates for each of the contracts that should be etched and @@ -393,12 +395,28 @@ impl TestChain { // Deploy the target1 template. let target1_template = - ERC4626Target0::deploy(client.clone(), (config.clone(), vault_address))? + ERC4626Target1::deploy(client.clone(), (config.clone(), vault_address))? .gas_price(DEFAULT_GAS_PRICE) .send() .await?; pairs.push((target1_address, target1_template.address())); + // Deploy the target2 template. + let target2_template = + ERC4626Target2::deploy(client.clone(), (config.clone(), vault_address))? + .gas_price(DEFAULT_GAS_PRICE) + .send() + .await?; + pairs.push((target2_address, target2_template.address())); + + // Deploy the target3 template. + let target3_template = + ERC4626Target3::deploy(client.clone(), (config.clone(), vault_address))? + .gas_price(DEFAULT_GAS_PRICE) + .send() + .await?; + pairs.push((target3_address, target3_template.address())); + // Etch the "etching vault" onto the current vault contract. The // etching vault implements `convertToAssets` to return the immutable // that was passed on deployment. This is necessary because the @@ -423,6 +441,8 @@ impl TestChain { config, target0_address, target1_address, + target2_address, + target3_address, vault_address, Vec::
::new(), ), diff --git a/migrate.sh b/migrate.sh index de98ee06e..f89c9bccc 100755 --- a/migrate.sh +++ b/migrate.sh @@ -9,7 +9,6 @@ forge script script/DevnetMigration.s.sol:DevnetMigration \ --sender "${ETH_FROM}" \ --private-key "${PRIVATE_KEY}" \ --rpc-url "${RPC_URL}" \ - --code-size-limit 9999999999 \ --slow \ --broadcast diff --git a/script/DevnetMigration.s.sol b/script/DevnetMigration.s.sol index d083d6919..cb39d1c24 100644 --- a/script/DevnetMigration.s.sol +++ b/script/DevnetMigration.s.sol @@ -4,20 +4,29 @@ pragma solidity 0.8.19; import { Script } from "forge-std/Script.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; +import { ERC4626HyperdriveCoreDeployer } from "contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626HyperdriveDeployerCoordinator } from "contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol"; +import { ERC4626Target0Deployer } from "contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol"; +import { ERC4626Target1Deployer } from "contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol"; +import { ERC4626Target2Deployer } from "contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol"; +import { ERC4626Target3Deployer } from "contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol"; +import { StETHHyperdriveCoreDeployer } from "contracts/src/deployers/steth/StETHHyperdriveCoreDeployer.sol"; +import { StETHHyperdriveDeployerCoordinator } from "contracts/src/deployers/steth/StETHHyperdriveDeployerCoordinator.sol"; +import { StETHTarget0Deployer } from "contracts/src/deployers/steth/StETHTarget0Deployer.sol"; +import { StETHTarget1Deployer } from "contracts/src/deployers/steth/StETHTarget1Deployer.sol"; +import { StETHTarget2Deployer } from "contracts/src/deployers/steth/StETHTarget2Deployer.sol"; +import { StETHTarget3Deployer } from "contracts/src/deployers/steth/StETHTarget3Deployer.sol"; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { ERC4626Target2Deployer } from "contracts/src/instances/ERC4626Target2Deployer.sol"; -import { ERC4626Target3Deployer } from "contracts/src/instances/ERC4626Target3Deployer.sol"; -import { ERC4626HyperdriveCoreDeployer } from "contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol"; +import { ILido } from "contracts/src/interfaces/ILido.sol"; import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; import { MockERC4626 } from "contracts/test/MockERC4626.sol"; import { MockHyperdriveMath } from "contracts/test/MockHyperdriveMath.sol"; +import { MockLido } from "contracts/test/MockLido.sol"; +import { ETH } from "test/utils/Constants.sol"; import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; /// @author DELV @@ -46,6 +55,8 @@ contract DevnetMigration is Script { string vaultName; string vaultSymbol; uint256 vaultStartingRate; + // lido configuration + uint256 lidoStartingRate; // factory configuration uint256 factoryCurveFee; uint256 factoryFlatFee; @@ -55,16 +66,22 @@ contract DevnetMigration is Script { uint256 factoryMaxFlatFee; uint256 factoryMaxGovernanceLPFee; uint256 factoryMaxGovernanceZombieFee; - // hyperdrive configuration - uint256 hyperdriveContribution; - uint256 hyperdriveFixedRate; - uint256 hyperdriveMinimumShareReserves; - uint256 hyperdriveMinimumTransactionAmount; - uint256 hyperdrivePositionDuration; - uint256 hyperdriveCheckpointDuration; - uint256 hyperdriveTimeStretchApr; - uint256 hyperdriveOracleSize; - uint256 hyperdriveUpdateGap; + // erc4626 hyperdrive configuration + uint256 erc4626HyperdriveContribution; + uint256 erc4626HyperdriveFixedRate; + uint256 erc4626HyperdriveMinimumShareReserves; + uint256 erc4626HyperdriveMinimumTransactionAmount; + uint256 erc4626HyperdrivePositionDuration; + uint256 erc4626HyperdriveCheckpointDuration; + uint256 erc4626HyperdriveTimeStretchApr; + // steth hyperdrive configuration + uint256 stethHyperdriveContribution; + uint256 stethHyperdriveFixedRate; + uint256 stethHyperdriveMinimumShareReserves; + uint256 stethHyperdriveMinimumTransactionAmount; + uint256 stethHyperdrivePositionDuration; + uint256 stethHyperdriveCheckpointDuration; + uint256 stethHyperdriveTimeStretchApr; } function run() external { @@ -79,17 +96,23 @@ contract DevnetMigration is Script { revert("BASE_TOKEN_DECIMALS exceeds uint8"); } Config memory config = Config({ + // admin configuration admin: vm.envOr("ADMIN", msg.sender), isCompetitionMode: vm.envOr("IS_COMPETITION_MODE", false), + // base token configuration baseTokenName: vm.envOr("BASE_TOKEN_NAME", string("Base")), baseTokenSymbol: vm.envOr("BASE_TOKEN_SYMBOL", string("BASE")), baseTokenDecimals: uint8(baseTokenDecimals), + // vault configuration vaultName: vm.envOr("VAULT_NAME", string("Delvnet Yield Source")), vaultSymbol: vm.envOr("VAULT_SYMBOL", string("DELV")), vaultStartingRate: vm.envOr( "VAULT_STARTING_RATE", uint256(0.05e18) ), + // lido configuration + lidoStartingRate: vm.envOr("LIDO_STARTING_RATE", uint256(0.035e18)), + // factory configuration factoryCurveFee: vm.envOr("FACTORY_CURVE_FEE", uint256(0.1e18)), factoryFlatFee: vm.envOr("FACTORY_FLAT_FEE", uint256(0.0005e18)), factoryGovernanceLPFee: vm.envOr( @@ -116,41 +139,63 @@ contract DevnetMigration is Script { "FACTORY_MAX_GOVERNANCE_ZOMBIE_FEE", uint256(0.3e18) ), - hyperdriveContribution: vm.envOr( - "HYPERDRIVE_CONTRIBUTION", + // erc4626 hyperdrive configuration + erc4626HyperdriveContribution: vm.envOr( + "ERC4626_HYPERDRIVE_CONTRIBUTION", uint256(100_000_000e18) ), - hyperdriveFixedRate: vm.envOr( - "HYPERDRIVE_FIXED_RATE", + erc4626HyperdriveFixedRate: vm.envOr( + "ERC4626_HYPERDRIVE_FIXED_RATE", uint256(0.05e18) ), - hyperdriveMinimumShareReserves: vm.envOr( - "HYPERDRIVE_MINIMUM_SHARE_RESERVES", + erc4626HyperdriveMinimumShareReserves: vm.envOr( + "ERC4626_HYPERDRIVE_MINIMUM_SHARE_RESERVES", uint256(10e18) ), - hyperdriveMinimumTransactionAmount: vm.envOr( - "HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT", + erc4626HyperdriveMinimumTransactionAmount: vm.envOr( + "ERC4626_HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT", uint256(0.001e18) ), - hyperdrivePositionDuration: vm.envOr( - "HYPERDRIVE_POSITION_DURATION", + erc4626HyperdrivePositionDuration: vm.envOr( + "ERC4626_HYPERDRIVE_POSITION_DURATION", uint256(1 weeks) ), - hyperdriveCheckpointDuration: vm.envOr( - "HYPERDRIVE_CHECKPOINT_DURATION", + erc4626HyperdriveCheckpointDuration: vm.envOr( + "ERC4626_HYPERDRIVE_CHECKPOINT_DURATION", uint256(1 hours) ), - hyperdriveTimeStretchApr: vm.envOr( - "HYPERDRIVE_TIME_STRETCH_APR", + erc4626HyperdriveTimeStretchApr: vm.envOr( + "ERC4626_HYPERDRIVE_TIME_STRETCH_APR", uint256(0.05e18) ), - hyperdriveOracleSize: vm.envOr( - "HYPERDRIVE_ORACLE_SIZE", - uint256(10) + // steth hyperdrive configuration + stethHyperdriveContribution: vm.envOr( + "STETH_HYPERDRIVE_CONTRIBUTION", + uint256(50_000e18) ), - hyperdriveUpdateGap: vm.envOr( - "HYPERDRIVE_UPDATE_GAP", + stethHyperdriveFixedRate: vm.envOr( + "STETH_HYPERDRIVE_FIXED_RATE", + uint256(0.035e18) + ), + stethHyperdriveMinimumShareReserves: vm.envOr( + "STETH_HYPERDRIVE_MINIMUM_SHARE_RESERVES", + uint256(1e15) + ), + stethHyperdriveMinimumTransactionAmount: vm.envOr( + "STETH_HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT", + uint256(0.001e18) + ), + stethHyperdrivePositionDuration: vm.envOr( + "STETH_HYPERDRIVE_POSITION_DURATION", + uint256(1 weeks) + ), + stethHyperdriveCheckpointDuration: vm.envOr( + "STETH_HYPERDRIVE_CHECKPOINT_DURATION", uint256(1 hours) + ), + stethHyperdriveTimeStretchApr: vm.envOr( + "STETH_HYPERDRIVE_TIME_STRETCH_APR", + uint256(0.035e18) ) }); @@ -184,6 +229,16 @@ contract DevnetMigration is Script { ); } + // Deploy the mock Lido system. We fund Lido with 1 eth to start to + // avoid reverts when we initialize the pool. + MockLido lido = new MockLido( + config.lidoStartingRate, + msg.sender, + config.isCompetitionMode + ); + vm.deal(msg.sender, 1 ether); + lido.submit{ value: 1 ether }(address(0)); + // Deploy the Hyperdrive factory. HyperdriveFactory factory; { @@ -214,9 +269,9 @@ contract DevnetMigration is Script { factory = new HyperdriveFactory(factoryConfig); } - // Deploy the Hyperdrive deployers and add them to the factory. - address hyperdriveDeployer = address( - new ERC4626HyperdriveDeployer( + // Deploy the ERC4626Hyperdrive deployers and add them to the factory. + address erc4626DeployerCoordinator = address( + new ERC4626HyperdriveDeployerCoordinator( address(new ERC4626HyperdriveCoreDeployer()), address(new ERC4626Target0Deployer()), address(new ERC4626Target1Deployer()), @@ -224,13 +279,13 @@ contract DevnetMigration is Script { address(new ERC4626Target3Deployer()) ) ); - factory.addHyperdriveDeployer(hyperdriveDeployer); + factory.addHyperdriveDeployer(erc4626DeployerCoordinator); - // Deploy and initialize an initial Hyperdrive instance for the devnet. - IHyperdrive hyperdrive; + // Deploy and initialize an initial ERC4626Hyperdrive instance. + IHyperdrive erc4626Hyperdrive; { - uint256 contribution = config.hyperdriveContribution; - uint256 fixedRate = config.hyperdriveFixedRate; + uint256 contribution = config.erc4626HyperdriveContribution; + uint256 fixedRate = config.erc4626HyperdriveFixedRate; baseToken.mint(msg.sender, contribution); baseToken.approve(address(factory), contribution); IHyperdrive.PoolDeployConfig memory poolConfig = IHyperdrive @@ -238,15 +293,72 @@ contract DevnetMigration is Script { baseToken: IERC20(address(baseToken)), linkerFactory: address(0), linkerCodeHash: bytes32(0), - minimumShareReserves: config.hyperdriveMinimumShareReserves, + minimumShareReserves: config + .erc4626HyperdriveMinimumShareReserves, + minimumTransactionAmount: config + .erc4626HyperdriveMinimumTransactionAmount, + positionDuration: config.erc4626HyperdrivePositionDuration, + checkpointDuration: config + .erc4626HyperdriveCheckpointDuration, + timeStretch: config + .erc4626HyperdriveTimeStretchApr + .calculateTimeStretch( + config.erc4626HyperdrivePositionDuration + ), + governance: config.admin, + feeCollector: config.admin, + fees: IHyperdrive.Fees({ + curve: config.factoryCurveFee, + flat: config.factoryFlatFee, + governanceLP: config.factoryGovernanceLPFee, + governanceZombie: config.factoryGovernanceZombieFee + }) + }); + erc4626Hyperdrive = factory.deployAndInitialize( + erc4626DeployerCoordinator, + poolConfig, + abi.encode(address(pool), new address[](0)), + contribution, + fixedRate, + new bytes(0) + ); + } + + // Deploy the StETHHyperdrive deployers and add them to the factory. + address stethDeployerCoordinator = address( + new StETHHyperdriveDeployerCoordinator( + address(new StETHHyperdriveCoreDeployer(ILido(address(lido)))), + address(new StETHTarget0Deployer(ILido(address(lido)))), + address(new StETHTarget1Deployer(ILido(address(lido)))), + address(new StETHTarget2Deployer(ILido(address(lido)))), + address(new StETHTarget3Deployer(ILido(address(lido)))), + ILido(address(lido)) + ) + ); + factory.addHyperdriveDeployer(stethDeployerCoordinator); + + // Deploy and initialize an initial StETHHyperdrive instance. + IHyperdrive stethHyperdrive; + { + uint256 contribution = config.stethHyperdriveContribution; + uint256 fixedRate = config.stethHyperdriveFixedRate; + vm.deal(msg.sender, contribution); + IHyperdrive.PoolDeployConfig memory poolConfig = IHyperdrive + .PoolDeployConfig({ + baseToken: IERC20(address(ETH)), + linkerFactory: address(0), + linkerCodeHash: bytes32(0), + minimumShareReserves: config + .stethHyperdriveMinimumShareReserves, minimumTransactionAmount: config - .hyperdriveMinimumTransactionAmount, - positionDuration: config.hyperdrivePositionDuration, - checkpointDuration: config.hyperdriveCheckpointDuration, + .stethHyperdriveMinimumTransactionAmount, + positionDuration: config.stethHyperdrivePositionDuration, + checkpointDuration: config + .stethHyperdriveCheckpointDuration, timeStretch: config - .hyperdriveTimeStretchApr + .stethHyperdriveTimeStretchApr .calculateTimeStretch( - config.hyperdrivePositionDuration + config.stethHyperdrivePositionDuration ), governance: config.admin, feeCollector: config.admin, @@ -257,8 +369,10 @@ contract DevnetMigration is Script { governanceZombie: config.factoryGovernanceZombieFee }) }); - hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + stethHyperdrive = factory.deployAndInitialize{ + value: contribution + }( + stethDeployerCoordinator, poolConfig, abi.encode(address(pool), new address[](0)), contribution, @@ -270,12 +384,13 @@ contract DevnetMigration is Script { // Deploy the MockHyperdriveMath contract. MockHyperdriveMath mockHyperdriveMath = new MockHyperdriveMath(); - // Transfer ownership of the base token, factory, and vault to the admin - // address now that we're done minting tokens and updating the + // Transfer ownership of the base token, factory, vault, and lido to the + // admin address now that we're done minting tokens and updating the // configuration. baseToken.transferOwnership(config.admin); factory.updateGovernance(config.admin); pool.transferOwnership(config.admin); + lido.transferOwnership(config.admin); vm.stopBroadcast(); @@ -283,8 +398,16 @@ contract DevnetMigration is Script { string memory result = "result"; vm.serializeAddress(result, "baseToken", address(baseToken)); vm.serializeAddress(result, "hyperdriveFactory", address(factory)); - // TODO: This is deprecated and should be removed by 0.0.11. - vm.serializeAddress(result, "mockHyperdrive", address(hyperdrive)); + vm.serializeAddress( + result, + "erc4626Hyperdrive", + address(erc4626Hyperdrive) + ); + vm.serializeAddress( + result, + "stethHyperdrive", + address(stethHyperdrive) + ); result = vm.serializeAddress( result, "mockHyperdriveMath", diff --git a/test/instances/erc4626/AaveV3ERC4626.t.sol b/test/instances/erc4626/AaveV3ERC4626.t.sol index 3a1575846..4cdd129ba 100644 --- a/test/instances/erc4626/AaveV3ERC4626.t.sol +++ b/test/instances/erc4626/AaveV3ERC4626.t.sol @@ -4,23 +4,9 @@ pragma solidity 0.8.19; import { AaveV3ERC4626Factory, IPool, IRewardsController, ERC20 } from "yield-daddy/src/aave-v3/AaveV3ERC4626Factory.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { AssetId } from "contracts/src/libraries/AssetId.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { MockERC4626Hyperdrive } from "contracts/test/MockERC4626Hyperdrive.sol"; -import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; -import { Lib } from "test/utils/Lib.sol"; import { ERC4626ValidationTest } from "./ERC4626Validation.t.sol"; -import { AaveV3ERC4626Factory, IPool, IRewardsController, ERC20 } from "yield-daddy/src/aave-v3/AaveV3ERC4626Factory.sol"; - contract AaveV3ERC4626Test is ERC4626ValidationTest { - using FixedPointMath for uint256; - function setUp() public override __mainnet_fork(17_318_972) { super.setUp(); diff --git a/test/instances/erc4626/ERC4626Hyperdrive.t.sol b/test/instances/erc4626/ERC4626Hyperdrive.t.sol index 666fe4ca6..4e302554b 100644 --- a/test/instances/erc4626/ERC4626Hyperdrive.t.sol +++ b/test/instances/erc4626/ERC4626Hyperdrive.t.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; +import { ERC4626HyperdriveCoreDeployer } from "contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626HyperdriveDeployerCoordinator } from "contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol"; +import { ERC4626Target0Deployer } from "contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol"; +import { ERC4626Target1Deployer } from "contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol"; +import { ERC4626Target2Deployer } from "contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol"; +import { ERC4626Target3Deployer } from "contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol"; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; -import { ERC4626Target0 } from "contracts/src/instances/ERC4626Target0.sol"; -import { ERC4626Target1 } from "contracts/src/instances/ERC4626Target1.sol"; -import { ERC4626Target2 } from "contracts/src/instances/ERC4626Target2.sol"; -import { ERC4626Target3 } from "contracts/src/instances/ERC4626Target3.sol"; -import { ERC4626HyperdriveCoreDeployer } from "contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626Target0 } from "contracts/src/instances/erc4626/ERC4626Target0.sol"; +import { ERC4626Target1 } from "contracts/src/instances/erc4626/ERC4626Target1.sol"; +import { ERC4626Target2 } from "contracts/src/instances/erc4626/ERC4626Target2.sol"; +import { ERC4626Target3 } from "contracts/src/instances/erc4626/ERC4626Target3.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IERC4626Hyperdrive } from "contracts/src/interfaces/IERC4626Hyperdrive.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { ERC4626Target2Deployer } from "contracts/src/instances/ERC4626Target2Deployer.sol"; -import { ERC4626Target3Deployer } from "contracts/src/instances/ERC4626Target3Deployer.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol"; import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; @@ -31,8 +31,8 @@ contract ERC4626HyperdriveTest is HyperdriveTest { HyperdriveFactory factory; - address hyperdriveDeployer; - address hyperdriveCoreDeployer; + address deployerCoordinator; + address coreDeployer; address target0Deployer; address target1Deployer; address target2Deployer; @@ -62,14 +62,14 @@ contract ERC4626HyperdriveTest is HyperdriveTest { ) ) ); - hyperdriveCoreDeployer = address(new ERC4626HyperdriveCoreDeployer()); + coreDeployer = address(new ERC4626HyperdriveCoreDeployer()); target0Deployer = address(new ERC4626Target0Deployer()); target1Deployer = address(new ERC4626Target1Deployer()); target2Deployer = address(new ERC4626Target2Deployer()); target3Deployer = address(new ERC4626Target3Deployer()); - hyperdriveDeployer = address( - new ERC4626HyperdriveDeployer( - hyperdriveCoreDeployer, + deployerCoordinator = address( + new ERC4626HyperdriveDeployerCoordinator( + coreDeployer, target0Deployer, target1Deployer, target2Deployer, @@ -126,7 +126,7 @@ contract ERC4626HyperdriveTest is HyperdriveTest { vm.stopPrank(); vm.startPrank(alice); - factory.addHyperdriveDeployer(hyperdriveDeployer); + factory.addHyperdriveDeployer(deployerCoordinator); dai.approve(address(factory), type(uint256).max); dai.approve(address(hyperdrive), type(uint256).max); dai.approve(address(mockHyperdrive), type(uint256).max); @@ -269,7 +269,7 @@ contract ERC4626HyperdriveTest is HyperdriveTest { }); dai.approve(address(factory), type(uint256).max); hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(pool), new address[](0)), contribution, @@ -323,7 +323,7 @@ contract ERC4626HyperdriveTest is HyperdriveTest { }); dai.approve(address(factory), type(uint256).max); hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(pool), new address[](0)), contribution, diff --git a/test/instances/erc4626/ERC4626Validation.t.sol b/test/instances/erc4626/ERC4626Validation.t.sol index eafe80c2c..f320d9577 100644 --- a/test/instances/erc4626/ERC4626Validation.t.sol +++ b/test/instances/erc4626/ERC4626Validation.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; +import { ERC4626HyperdriveCoreDeployer } from "contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626HyperdriveDeployerCoordinator } from "contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol"; +import { ERC4626Target0Deployer } from "contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol"; +import { ERC4626Target1Deployer } from "contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol"; +import { ERC4626Target2Deployer } from "contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol"; +import { ERC4626Target3Deployer } from "contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol"; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { ERC4626Target2Deployer } from "contracts/src/instances/ERC4626Target2Deployer.sol"; -import { ERC4626Target3Deployer } from "contracts/src/instances/ERC4626Target3Deployer.sol"; -import { ERC4626HyperdriveCoreDeployer } from "contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; @@ -20,18 +20,17 @@ import { MockERC4626Hyperdrive } from "contracts/test/MockERC4626Hyperdrive.sol" import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; import { Lib } from "test/utils/Lib.sol"; -import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; abstract contract ERC4626ValidationTest is HyperdriveTest { using FixedPointMath for *; using Lib for *; - address hyperdriveDeployer; + address deployerCoordinator; + address coreDeployer; address target0Deployer; address target1Deployer; address target2Deployer; address target3Deployer; - address hyperdriveCoreDeployer; HyperdriveFactory internal factory; IERC20 internal underlyingToken; @@ -46,15 +45,15 @@ abstract contract ERC4626ValidationTest is HyperdriveTest { vm.startPrank(deployer); // Deploy the ERC4626Hyperdrive factory and deployer. - hyperdriveCoreDeployer = address(new ERC4626HyperdriveCoreDeployer()); + coreDeployer = address(new ERC4626HyperdriveCoreDeployer()); target0Deployer = address(new ERC4626Target0Deployer()); target1Deployer = address(new ERC4626Target1Deployer()); target2Deployer = address(new ERC4626Target2Deployer()); target3Deployer = address(new ERC4626Target3Deployer()); - hyperdriveDeployer = address( - new ERC4626HyperdriveDeployer( - hyperdriveCoreDeployer, + deployerCoordinator = address( + new ERC4626HyperdriveDeployerCoordinator( + coreDeployer, target0Deployer, target1Deployer, target2Deployer, @@ -90,14 +89,14 @@ abstract contract ERC4626ValidationTest is HyperdriveTest { vm.stopPrank(); vm.startPrank(alice); - factory.addHyperdriveDeployer(hyperdriveDeployer); + factory.addHyperdriveDeployer(deployerCoordinator); // Set approval to allow initial contribution to factory underlyingToken.approve(address(factory), type(uint256).max); // Deploy and set hyperdrive instance hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(token), new address[](0)), contribution, @@ -141,7 +140,7 @@ abstract contract ERC4626ValidationTest is HyperdriveTest { // Deploy a new hyperdrive instance hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(token), new address[](0)), contribution, diff --git a/test/instances/erc4626/StethERC4626.t.sol b/test/instances/erc4626/StethERC4626.t.sol deleted file mode 100644 index 11c44d761..000000000 --- a/test/instances/erc4626/StethERC4626.t.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ILido } from "contracts/src/interfaces/ILido.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { AssetId } from "contracts/src/libraries/AssetId.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; -import { MockERC4626Hyperdrive } from "contracts/test/MockERC4626Hyperdrive.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; -import { Lib } from "test/utils/Lib.sol"; -import { ERC4626ValidationTest } from "./ERC4626Validation.t.sol"; - -contract StethERC4626 is ERC4626ValidationTest { - using FixedPointMath for *; - - function setUp() public override __mainnet_fork(17_376_154) { - super.setUp(); - underlyingToken = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - token = IERC4626(0xF9A98A9452485ed55cd3Ce5260C2b71c9807b11a); - - // Note this is wsteth so it could be somewhat problematic in the future - // depending on whether or not tests interact with wsEth. - address stethWhale = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - whaleTransfer(stethWhale, underlyingToken, alice); - - _setUp(); - } - - function advanceTimeWithYield( - uint256 timeDelta, - int256 variableRate - ) public override { - vm.warp(block.timestamp + timeDelta); - - // The Lido storage location that tracks buffered ether reserves. We can - // simulate the accrual of interest by updating this value. - bytes32 BUFFERED_ETHER_POSITION = keccak256("lido.Lido.bufferedEther"); - - ILido LIDO = ILido(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - - // Accrue interest in Lido. Since the share price is given by - // `getTotalPooledEther() / getTotalShares()`, we can simulate the - // accrual of interest by multiplying the total pooled ether by the - // variable rate plus one. - uint256 bufferedEther = variableRate >= 0 - ? LIDO.getBufferedEther() + - LIDO.getTotalPooledEther().mulDown(uint256(variableRate)) - : LIDO.getBufferedEther() - - LIDO.getTotalPooledEther().mulDown(uint256(variableRate)); - vm.store( - address(LIDO), - BUFFERED_ETHER_POSITION, - bytes32(bufferedEther) - ); - } -} diff --git a/test/instances/erc4626/Sweep.t.sol b/test/instances/erc4626/Sweep.t.sol index 33d15ad07..0e1526641 100644 --- a/test/instances/erc4626/Sweep.t.sol +++ b/test/instances/erc4626/Sweep.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { ERC4626Hyperdrive } from "contracts/src/instances/ERC4626Hyperdrive.sol"; -import { ERC4626Target0 } from "contracts/src/instances/ERC4626Target0.sol"; -import { ERC4626Target1 } from "contracts/src/instances/ERC4626Target1.sol"; -import { ERC4626Target2 } from "contracts/src/instances/ERC4626Target2.sol"; -import { ERC4626Target3 } from "contracts/src/instances/ERC4626Target3.sol"; +import { ERC4626Hyperdrive } from "contracts/src/instances/erc4626/ERC4626Hyperdrive.sol"; +import { ERC4626Target0 } from "contracts/src/instances/erc4626/ERC4626Target0.sol"; +import { ERC4626Target1 } from "contracts/src/instances/erc4626/ERC4626Target1.sol"; +import { ERC4626Target2 } from "contracts/src/instances/erc4626/ERC4626Target2.sol"; +import { ERC4626Target3 } from "contracts/src/instances/erc4626/ERC4626Target3.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IERC4626Hyperdrive } from "contracts/src/interfaces/IERC4626Hyperdrive.sol"; diff --git a/test/instances/erc4626/UsdcERC4626.t.sol b/test/instances/erc4626/UsdcERC4626.t.sol index dc6849699..04b670fd7 100644 --- a/test/instances/erc4626/UsdcERC4626.t.sol +++ b/test/instances/erc4626/UsdcERC4626.t.sol @@ -1,19 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveCoreDeployer } from "contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626HyperdriveDeployerCoordinator } from "contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol"; +import { ERC4626Target0Deployer } from "contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol"; +import { ERC4626Target1Deployer } from "contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol"; +import { ERC4626Target2Deployer } from "contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol"; +import { ERC4626Target3Deployer } from "contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol"; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; import { ILido } from "contracts/src/interfaces/ILido.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { ERC4626Target2Deployer } from "contracts/src/instances/ERC4626Target2Deployer.sol"; -import { ERC4626Target3Deployer } from "contracts/src/instances/ERC4626Target3Deployer.sol"; -import { ERC4626HyperdriveCoreDeployer } from "contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; @@ -25,7 +24,6 @@ import { MockERC4626 } from "contracts/test/MockERC4626.sol"; import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; import { Lib } from "test/utils/Lib.sol"; import { ERC4626ValidationTest } from "./ERC4626Validation.t.sol"; -import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; contract UsdcERC4626 is ERC4626ValidationTest { using FixedPointMath for *; @@ -56,14 +54,14 @@ contract UsdcERC4626 is ERC4626ValidationTest { ERC20Mintable(address(underlyingToken)).mint(bob, monies); // Initialize deployer contracts and forwarder. - hyperdriveCoreDeployer = address(new ERC4626HyperdriveCoreDeployer()); + coreDeployer = address(new ERC4626HyperdriveCoreDeployer()); target0Deployer = address(new ERC4626Target0Deployer()); target1Deployer = address(new ERC4626Target1Deployer()); target2Deployer = address(new ERC4626Target2Deployer()); target3Deployer = address(new ERC4626Target3Deployer()); - hyperdriveDeployer = address( - new ERC4626HyperdriveDeployer( - hyperdriveCoreDeployer, + deployerCoordinator = address( + new ERC4626HyperdriveDeployerCoordinator( + coreDeployer, target0Deployer, target1Deployer, target2Deployer, @@ -101,14 +99,14 @@ contract UsdcERC4626 is ERC4626ValidationTest { vm.stopPrank(); vm.startPrank(alice); - factory.addHyperdriveDeployer(hyperdriveDeployer); + factory.addHyperdriveDeployer(deployerCoordinator); // Set approval to allow initial contribution to factory. underlyingToken.approve(address(factory), type(uint256).max); // Deploy and set hyperdrive instance. hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(token), new address[](0)), contribution, diff --git a/test/instances/erc4626/sDai.t.sol b/test/instances/erc4626/sDai.t.sol index 3d1cfc963..07eec7bfd 100644 --- a/test/instances/erc4626/sDai.t.sol +++ b/test/instances/erc4626/sDai.t.sol @@ -1,19 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { AssetId } from "contracts/src/libraries/AssetId.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { MockERC4626Hyperdrive } from "contracts/test/MockERC4626Hyperdrive.sol"; -import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; -import { Lib } from "test/utils/Lib.sol"; import { ERC4626ValidationTest } from "./ERC4626Validation.t.sol"; // Interface for the `Pot` of the underlying DSR @@ -26,8 +15,6 @@ interface PotLike { } contract sDaiTest is ERC4626ValidationTest { - using FixedPointMath for *; - function setUp() public override __mainnet_fork(17_318_972) { super.setUp(); diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol new file mode 100644 index 000000000..e9cff3f8d --- /dev/null +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -0,0 +1,932 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { StETHHyperdriveCoreDeployer } from "contracts/src/deployers/steth/StETHHyperdriveCoreDeployer.sol"; +import { StETHHyperdriveDeployerCoordinator } from "contracts/src/deployers/steth/StETHHyperdriveDeployerCoordinator.sol"; +import { StETHTarget0Deployer } from "contracts/src/deployers/steth/StETHTarget0Deployer.sol"; +import { StETHTarget1Deployer } from "contracts/src/deployers/steth/StETHTarget1Deployer.sol"; +import { StETHTarget2Deployer } from "contracts/src/deployers/steth/StETHTarget2Deployer.sol"; +import { StETHTarget3Deployer } from "contracts/src/deployers/steth/StETHTarget3Deployer.sol"; +import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "contracts/src/interfaces/ILido.sol"; +import { AssetId } from "contracts/src/libraries/AssetId.sol"; +import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; +import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; +import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; +import { ETH } from "test/utils/Constants.sol"; +import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; +import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; + +contract StETHHyperdriveTest is HyperdriveTest { + using FixedPointMath for uint256; + using Lib for *; + using stdStorage for StdStorage; + + uint256 internal constant FIXED_RATE = 0.05e18; + + // The Lido storage location that tracks buffered ether reserves. We can + // simulate the accrual of interest by updating this value. + bytes32 internal constant BUFFERED_ETHER_POSITION = + keccak256("lido.Lido.bufferedEther"); + + ILido internal constant LIDO = + ILido(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); + + address internal STETH_WHALE = 0x1982b2F5814301d4e9a8b0201555376e62F82428; + address internal ETH_WHALE = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + + HyperdriveFactory factory; + address deployerCoordinator; + + function setUp() public override __mainnet_fork(17_376_154) { + super.setUp(); + + // Deploy the hyperdrive factory. + vm.startPrank(deployer); + address[] memory defaults = new address[](1); + defaults[0] = bob; + forwarderFactory = new ForwarderFactory(); + factory = new HyperdriveFactory( + HyperdriveFactory.FactoryConfig({ + governance: deployer, + hyperdriveGovernance: bob, + feeCollector: bob, + defaultPausers: defaults, + fees: IHyperdrive.Fees(0, 0, 0, 0), + maxFees: IHyperdrive.Fees(0, 0, 0, 0), + linkerFactory: address(forwarderFactory), + linkerCodeHash: forwarderFactory.ERC20LINK_HASH() + }) + ); + + // Deploy the hyperdrive deployers and register the deployer coordinator + // in the factory. + deployerCoordinator = address( + new StETHHyperdriveDeployerCoordinator( + address(new StETHHyperdriveCoreDeployer(LIDO)), + address(new StETHTarget0Deployer(LIDO)), + address(new StETHTarget1Deployer(LIDO)), + address(new StETHTarget2Deployer(LIDO)), + address(new StETHTarget3Deployer(LIDO)), + LIDO + ) + ); + factory.addHyperdriveDeployer(address(deployerCoordinator)); + + // Alice deploys the hyperdrive instance. + vm.stopPrank(); + vm.startPrank(alice); + IHyperdrive.PoolDeployConfig memory config = IHyperdrive + .PoolDeployConfig({ + baseToken: IERC20(ETH), + linkerFactory: address(0), + linkerCodeHash: bytes32(0), + minimumShareReserves: 1e15, + minimumTransactionAmount: 1e14, + positionDuration: POSITION_DURATION, + checkpointDuration: CHECKPOINT_DURATION, + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.05e18, + POSITION_DURATION + ), + governance: governance, + feeCollector: feeCollector, + fees: IHyperdrive.Fees({ + curve: 0, + flat: 0, + governanceLP: 0, + governanceZombie: 0 + }) + }); + uint256 contribution = 10_000e18; + hyperdrive = factory.deployAndInitialize{ value: contribution }( + deployerCoordinator, + config, + new bytes(0), + contribution, + FIXED_RATE, + new bytes(0) + ); + + // Ensure that Bob 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.divDown(hyperdrive.getPoolConfig().initialSharePrice) - + 2 * + hyperdrive.getPoolConfig().minimumShareReserves, + 1e5 + ); + + // Fund the test accounts with stETH and ETH. + address[] memory accounts = new address[](3); + accounts[0] = alice; + accounts[1] = bob; + accounts[2] = celine; + fundAccounts(address(hyperdrive), IERC20(LIDO), STETH_WHALE, accounts); + + // Start recording event logs. + vm.recordLogs(); + } + + /// Deploy and Initialize /// + + function test__steth__deployAndInitialize() external { + // Deploy and Initialize the stETH hyperdrive instance. Excess ether is + // sent, and should be returned to the sender. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bobBalanceBefore = address(bob).balance; + IHyperdrive.PoolDeployConfig memory config = IHyperdrive + .PoolDeployConfig({ + baseToken: IERC20(ETH), + linkerFactory: address(0), + linkerCodeHash: bytes32(0), + minimumShareReserves: 1e15, + minimumTransactionAmount: 1e14, + positionDuration: POSITION_DURATION, + checkpointDuration: CHECKPOINT_DURATION, + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.05e18, + POSITION_DURATION + ), + governance: governance, + feeCollector: feeCollector, + fees: IHyperdrive.Fees({ + curve: 0, + flat: 0, + governanceLP: 0, + governanceZombie: 0 + }) + }); + uint256 contribution = 5_000e18; + hyperdrive = factory.deployAndInitialize{ value: contribution + 1e18 }( + address(deployerCoordinator), + config, + new bytes(0), + contribution, + FIXED_RATE, + new bytes(0) + ); + assertEq(address(bob).balance, bobBalanceBefore - contribution); + + // Ensure that Bob received the correct amount of LP tokens. He should + // receive LP shares totaling the amount of shares that he 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, bob), + contribution.divDown(hyperdrive.getPoolConfig().initialSharePrice) - + 2 * + hyperdrive.getPoolConfig().minimumShareReserves, + 1e5 + ); + + // Ensure that the share reserves and LP total supply are equal and correct. + assertApproxEqAbs( + hyperdrive.getPoolInfo().shareReserves, + contribution.mulDivDown( + LIDO.getTotalShares(), + LIDO.getTotalPooledEther() + ), + 1 + ); + assertEq( + hyperdrive.getPoolInfo().lpTotalSupply, + hyperdrive.getPoolInfo().shareReserves - config.minimumShareReserves + ); + + // Verify that the correct events were emitted. + verifyFactoryEvents( + factory, + hyperdrive, + bob, + contribution, + FIXED_RATE, + config.minimumShareReserves, + new bytes(0), + // NOTE: Tolerance since stETH uses mulDivDown for share calculations. + 1e5 + ); + } + + /// Price Per Share /// + + function test__pricePerShare(uint256 basePaid) external { + // Ensure that the share price is the expected value. + uint256 totalPooledEther = LIDO.getTotalPooledEther(); + uint256 totalShares = LIDO.getTotalShares(); + uint256 sharePrice = hyperdrive.getPoolInfo().sharePrice; + assertEq(sharePrice, totalPooledEther.divDown(totalShares)); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of ETH. This will + // be an approximation since Lido uses `mulDivDown` whereas this test + // pre-computes the share price. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxLong(hyperdrive) + ); + uint256 hyperdriveSharesBefore = LIDO.sharesOf(address(hyperdrive)); + openLong(bob, basePaid); + assertApproxEqAbs( + LIDO.sharesOf(address(hyperdrive)), + hyperdriveSharesBefore + basePaid.divDown(sharePrice), + 1e4 + ); + } + + /// Long /// + + function test_open_long_with_ETH(uint256 basePaid) external { + // Get some balance information before the deposit. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob opens a long by depositing ETH. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxLong(hyperdrive) + ); + openLong(bob, basePaid); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyDeposit( + bob, + basePaid, + true, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_open_long_refunds() external { + vm.startPrank(bob); + + // Ensure that Bob receives a refund on the excess ETH that he sent + // when opening a long with "asBase" set to true. + uint256 ethBalanceBefore = address(bob).balance; + hyperdrive.openLong{ value: 2e18 }( + 1e18, + 0, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore - 1e18); + + // Ensure that Bob receives a refund when he opens a long with "asBase" + // set to false and sends ether to the contract. + ethBalanceBefore = address(bob).balance; + hyperdrive.openLong{ value: 0.5e18 }( + 1e18, + 0, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore); + } + + function test_open_long_with_steth(uint256 basePaid) external { + // Get some balance information before the deposit. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob opens a long by depositing stETH. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxLong(hyperdrive) + ); + uint256 sharesPaid = basePaid.mulDivDown( + LIDO.getTotalShares(), + LIDO.getTotalPooledEther() + ); + openLong(bob, sharesPaid, false); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyDeposit( + bob, + basePaid, + false, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_close_long_with_ETH(uint256 basePaid) external { + // Bob opens a long. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxLong(hyperdrive) + ); + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + + // Bob attempts to close his long with ETH as the target asset. This + // fails since ETH isn't supported as a withdrawal asset. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.UnsupportedToken.selector); + hyperdrive.closeLong( + maturityTime, + longAmount, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + function test_close_long_with_steth(uint256 basePaid) external { + // Bob opens a long. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxLong(hyperdrive) + ); + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + + // Get some balance information before the withdrawal. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with stETH as the target asset. + uint256 shareProceeds = closeLong(bob, maturityTime, longAmount, false); + uint256 baseProceeds = shareProceeds.mulDivDown( + LIDO.getTotalPooledEther(), + LIDO.getTotalShares() + ); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyStethWithdrawal( + bob, + baseProceeds, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + /// Short /// + + function test_open_short_with_ETH(uint256 shortAmount) external { + // Get some balance information before the deposit. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob opens a short by depositing ETH. + shortAmount = shortAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxShort(hyperdrive) + ); + uint256 balanceBefore = bob.balance; + vm.deal(bob, shortAmount); + (, uint256 basePaid) = openShort(bob, shortAmount); + vm.deal(bob, balanceBefore - basePaid); + + // Ensure that the amount of base paid by the short is reasonable. + uint256 realizedRate = HyperdriveUtils.calculateAPRFromRealizedPrice( + shortAmount - basePaid, + shortAmount, + 1e18 + ); + assertGt(basePaid, 0); + assertGe(realizedRate, FIXED_RATE); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyDeposit( + bob, + basePaid, + true, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_open_short_with_steth(uint256 shortAmount) external { + // Get some balance information before the deposit. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob opens a short by depositing ETH. + shortAmount = shortAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxShort(hyperdrive) + ); + (, uint256 sharesPaid) = openShort(bob, shortAmount, false); + uint256 basePaid = sharesPaid.mulDivDown( + LIDO.getTotalPooledEther(), + LIDO.getTotalShares() + ); + + // Ensure that the amount of base paid by the short is reasonable. + uint256 realizedRate = HyperdriveUtils.calculateAPRFromRealizedPrice( + shortAmount - basePaid, + shortAmount, + 1e18 + ); + assertGt(basePaid, 0); + assertGe(realizedRate, FIXED_RATE); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyDeposit( + bob, + basePaid, + false, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_open_short_refunds() external { + vm.startPrank(bob); + + // Ensure that Bob receives a refund on the excess ETH that he sent + // when opening a long with "asBase" set to true. + uint256 ethBalanceBefore = address(bob).balance; + (, uint256 basePaid) = hyperdrive.openShort{ value: 2e18 }( + 1e18, + 1e18, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore - basePaid); + + // Ensure that Bob receives a refund when he opens a long with "asBase" + // set to false and sends ether to the contract. + ethBalanceBefore = address(bob).balance; + hyperdrive.openShort{ value: 0.5e18 }( + 1e18, + 1e18, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore); + } + + function test_close_short_with_eth( + uint256 shortAmount, + int256 variableRate + ) external { + // Bob opens a short. + shortAmount = shortAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxShort(hyperdrive) + ); + uint256 balanceBefore = bob.balance; + vm.deal(bob, shortAmount); + (uint256 maturityTime, uint256 basePaid) = openShort(bob, shortAmount); + vm.deal(bob, balanceBefore - basePaid); + + // The term passes and interest accrues. + variableRate = variableRate.normalizeToRange(0, 2.5e18); + advanceTime(POSITION_DURATION, variableRate); + + // Bob attempts to close his short with ETH as the target asset. This + // fails since ETH isn't supported as a withdrawal asset. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.UnsupportedToken.selector); + hyperdrive.closeShort( + maturityTime, + shortAmount, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + function test_close_short_with_steth( + uint256 shortAmount, + int256 variableRate + ) external { + // Bob opens a short. + shortAmount = shortAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + HyperdriveUtils.calculateMaxShort(hyperdrive) + ); + uint256 balanceBefore = bob.balance; + vm.deal(bob, shortAmount); + (uint256 maturityTime, uint256 basePaid) = openShort(bob, shortAmount); + vm.deal(bob, balanceBefore - basePaid); + + // The term passes and interest accrues. + variableRate = variableRate.normalizeToRange(0, 2.5e18); + advanceTime(POSITION_DURATION, variableRate); + + // Get some balance information before closing the short. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his short with stETH as the target asset. Bob's proceeds + // should be the variable interest that accrued on the shorted bonds. + (, int256 expectedBaseProceeds) = HyperdriveUtils.calculateInterest( + shortAmount, + variableRate, + POSITION_DURATION + ); + uint256 shareProceeds = closeShort( + bob, + maturityTime, + shortAmount, + false + ); + uint256 baseProceeds = shareProceeds.mulDivDown( + LIDO.getTotalPooledEther(), + LIDO.getTotalShares() + ); + assertApproxEqAbs(baseProceeds, uint256(expectedBaseProceeds), 1e9); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyStethWithdrawal( + bob, + baseProceeds, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_attack_long_steth() external { + // Get some balance information before the deposit. + LIDO.sharesOf(address(hyperdrive)); + + // Bob opens a long by depositing ETH. + uint256 basePaid = HyperdriveUtils.calculateMaxLong(hyperdrive); + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + + // Get some balance information before the withdrawal. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Bob closes his long with stETH as the target asset. + uint256 shareProceeds = closeLong(bob, maturityTime, longAmount, false); + uint256 baseProceeds = shareProceeds.mulDivDown( + LIDO.getTotalPooledEther(), + LIDO.getTotalShares() + ); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyStethWithdrawal( + bob, + baseProceeds, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test__DOSStethHyperdriveCloseLong() external { + //###########################################################################" + //#### TEST: Denial of Service when LIDO's `TotalPooledEther` decreases. ####" + //###########################################################################" + + // Ensure that the share price is the expected value. + uint256 totalPooledEther = LIDO.getTotalPooledEther(); + uint256 totalShares = LIDO.getTotalShares(); + uint256 sharePrice = hyperdrive.getPoolInfo().sharePrice; + assertEq(sharePrice, totalPooledEther.divDown(totalShares)); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of ETH. This will + // be an approximation since Lido uses `mulDivDown` whereas this test + // pre-computes the share price. + uint256 basePaid = HyperdriveUtils.calculateMaxLong(hyperdrive) / 10; + uint256 hyperdriveSharesBefore = LIDO.sharesOf(address(hyperdrive)); + + // Bob calls openLong() + (uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid); + // Bob paid basePaid == ", basePaid); + // Bob received longAmount == ", longAmount); + assertApproxEqAbs( + LIDO.sharesOf(address(hyperdrive)), + hyperdriveSharesBefore + basePaid.divDown(sharePrice), + 1e4 + ); + + // Get some balance information before the withdrawal. + uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther(); + uint256 totalSharesBefore = LIDO.getTotalShares(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + uint256 snapshotId = vm.snapshot(); + + // Taking a Snapshot of the state + // Bob closes his long with stETH as the target asset. + uint256 shareProceeds = closeLong( + bob, + maturityTime, + longAmount / 2, + false + ); + uint256 baseProceeds = shareProceeds.mulDivDown( + LIDO.getTotalPooledEther(), + LIDO.getTotalShares() + ); + + // Ensure that Lido's aggregates and the token balances were updated + // correctly during the trade. + verifyStethWithdrawal( + bob, + baseProceeds, + totalPooledEtherBefore, + totalSharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + // # Reverting to the saved state Snapshot #\n"); + vm.revertTo(snapshotId); + + // # Manipulating Lido's totalPooledEther : removing only 1e18 + bytes32 balanceBefore = vm.load( + address(LIDO), + bytes32( + 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483 + ) + ); + // LIDO.CL_BALANCE_POSITION Before: ", uint(balanceBefore)); + uint(LIDO.getTotalPooledEther()); + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + vm.store( + address(LIDO), + bytes32( + uint256( + 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483 + ) + ), + bytes32(uint256(balanceBefore) - 1e18) + ); + + // Avoid Stack too deep + uint256 maturityTime_ = maturityTime; + uint256 longAmount_ = longAmount; + + vm.load( + address(LIDO), + bytes32( + uint256( + 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483 + ) + ) + ); + + // Bob closes his long with stETH as the target asset. + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime_), + bob + ); + + // The fact that this doesn't revert means that it works + closeLong(bob, maturityTime_, longAmount_ / 2, false); + } + + function verifyDeposit( + address trader, + uint256 basePaid, + bool asBase, + uint256 totalPooledEtherBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + if (asBase) { + // Ensure that the amount of pooled ether increased by the base paid. + assertEq( + LIDO.getTotalPooledEther(), + totalPooledEtherBefore + basePaid + ); + + // Ensure that the ETH balances were updated correctly. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(bob.balance, traderBalancesBefore.ETHBalance - basePaid); + + // Ensure that the stETH balances were updated correctly. + assertApproxEqAbs( + LIDO.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethBalance + basePaid, + 1 + ); + assertEq(LIDO.balanceOf(trader), traderBalancesBefore.stethBalance); + + // Ensure that the stETH shares were updated correctly. + uint256 expectedShares = basePaid.mulDivDown( + totalSharesBefore, + totalPooledEtherBefore + ); + assertEq(LIDO.getTotalShares(), totalSharesBefore + expectedShares); + assertEq( + LIDO.sharesOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethShares + expectedShares + ); + assertEq(LIDO.sharesOf(bob), traderBalancesBefore.stethShares); + } else { + // Ensure that the amount of pooled ether stays the same. + assertEq(LIDO.getTotalPooledEther(), totalPooledEtherBefore); + + // Ensure that the ETH balances were updated correctly. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(trader.balance, traderBalancesBefore.ETHBalance); + + // Ensure that the stETH balances were updated correctly. + assertApproxEqAbs( + LIDO.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethBalance + basePaid, + 1 + ); + assertApproxEqAbs( + LIDO.balanceOf(trader), + traderBalancesBefore.stethBalance - basePaid, + 1 + ); + + // Ensure that the stETH shares were updated correctly. + uint256 expectedShares = basePaid.mulDivDown( + totalSharesBefore, + totalPooledEtherBefore + ); + assertEq(LIDO.getTotalShares(), totalSharesBefore); + assertApproxEqAbs( + LIDO.sharesOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethShares + expectedShares, + 1 + ); + assertApproxEqAbs( + LIDO.sharesOf(trader), + traderBalancesBefore.stethShares - expectedShares, + 1 + ); + } + } + + function verifyStethWithdrawal( + address trader, + uint256 baseProceeds, + uint256 totalPooledEtherBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + // Ensure that the total pooled ether and shares stays the same. + assertEq(LIDO.getTotalPooledEther(), totalPooledEtherBefore); + assertApproxEqAbs(LIDO.getTotalShares(), totalSharesBefore, 1); + + // Ensure that the ETH balances were updated correctly. + assertEq( + address(hyperdrive).balance, + hyperdriveBalancesBefore.ETHBalance + ); + assertEq(trader.balance, traderBalancesBefore.ETHBalance); + + // Ensure that the stETH balances were updated correctly. + assertApproxEqAbs( + LIDO.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethBalance - baseProceeds, + 1 + ); + assertApproxEqAbs( + LIDO.balanceOf(trader), + traderBalancesBefore.stethBalance + baseProceeds, + 1 + ); + + // Ensure that the stETH shares were updated correctly. + uint256 expectedShares = baseProceeds.mulDivDown( + totalSharesBefore, + totalPooledEtherBefore + ); + assertApproxEqAbs( + LIDO.sharesOf(address(hyperdrive)), + hyperdriveBalancesBefore.stethShares - expectedShares, + 1 + ); + assertApproxEqAbs( + LIDO.sharesOf(trader), + traderBalancesBefore.stethShares + expectedShares, + 1 + ); + } + + /// Helpers /// + + function advanceTime( + uint256 timeDelta, + int256 variableRate + ) internal override { + // Advance the time. + vm.warp(block.timestamp + timeDelta); + + // Accrue interest in Lido. Since the share price is given by + // `getTotalPooledEther() / getTotalShares()`, we can simulate the + // accrual of interest by multiplying the total pooled ether by the + // variable rate plus one. + uint256 bufferedEther = variableRate >= 0 + ? LIDO.getBufferedEther() + + LIDO.getTotalPooledEther().mulDown(uint256(variableRate)) + : LIDO.getBufferedEther() - + LIDO.getTotalPooledEther().mulDown(uint256(variableRate)); + vm.store( + address(LIDO), + BUFFERED_ETHER_POSITION, + bytes32(bufferedEther) + ); + } + + struct AccountBalances { + uint256 stethShares; + uint256 stethBalance; + uint256 ETHBalance; + } + + function getAccountBalances( + address account + ) internal view returns (AccountBalances memory) { + return + AccountBalances({ + stethShares: LIDO.sharesOf(account), + stethBalance: LIDO.balanceOf(account), + ETHBalance: account.balance + }); + } +} diff --git a/test/instances/steth/Sweep.t.sol b/test/instances/steth/Sweep.t.sol new file mode 100644 index 000000000..45e211ae9 --- /dev/null +++ b/test/instances/steth/Sweep.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { StETHHyperdrive } from "contracts/src/instances/steth/StETHHyperdrive.sol"; +import { StETHTarget0 } from "contracts/src/instances/steth/StETHTarget0.sol"; +import { StETHTarget1 } from "contracts/src/instances/steth/StETHTarget1.sol"; +import { StETHTarget2 } from "contracts/src/instances/steth/StETHTarget2.sol"; +import { StETHTarget3 } from "contracts/src/instances/steth/StETHTarget3.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { ILido } from "contracts/src/interfaces/ILido.sol"; +import { IStETHHyperdrive } from "contracts/src/interfaces/IStETHHyperdrive.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { ONE } from "contracts/src/libraries/FixedPointMath.sol"; +import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; +import { MockLido } from "contracts/test/MockLido.sol"; +import { BaseTest } from "test/utils/BaseTest.sol"; +import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; +import { ETH } from "test/utils/Constants.sol"; + +contract SweepTest is BaseTest { + ForwardingToken lidoForwarder; + ERC20Mintable sweepable; + + IStETHHyperdrive hyperdrive; + + function setUp() public override { + super.setUp(); + + // We'll use Alice to deploy the contracts. + vm.startPrank(alice); + + // Deploy the sweepable ERC20. + sweepable = new ERC20Mintable( + "Sweepable", + "SWEEP", + 18, + address(0), + false + ); + + // Deploy the leaky lido instance. Then deploy forwarding tokens for + // each of the targets. Add some ETH to the leaky lido instance to + // ensure that it has a well-defined share price. + LeakyLido leakyLido = new LeakyLido(); + lidoForwarder = new ForwardingToken(address(leakyLido)); + leakyLido.submit{ value: 1e18 }(address(0)); + + // Deploy Hyperdrive with the leaky lido. + IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ + baseToken: IERC20(address(ETH)), + linkerFactory: address(0), + linkerCodeHash: bytes32(0), + initialSharePrice: ONE, + minimumShareReserves: 1e15, + minimumTransactionAmount: 1e12, + positionDuration: 365 days, + checkpointDuration: 1 days, + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.01e18, + 365 days + ), + governance: alice, + feeCollector: bob, + fees: IHyperdrive.Fees(0, 0, 0, 0) + }); + vm.warp(3 * config.positionDuration); + hyperdrive = IStETHHyperdrive( + address( + new StETHHyperdrive( + config, + address( + new StETHTarget0(config, ILido(address(leakyLido))) + ), + address( + new StETHTarget1(config, ILido(address(leakyLido))) + ), + address( + new StETHTarget2(config, ILido(address(leakyLido))) + ), + address( + new StETHTarget3(config, ILido(address(leakyLido))) + ), + ILido(address(leakyLido)) + ) + ) + ); + + // Initialize Hyperdrive. This ensures that Hyperdrive has vault tokens + // to sweep. + hyperdrive.initialize{ value: 100e18 }( + 100e18, + 0.05e18, + IHyperdrive.Options({ + destination: alice, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Mint some of the sweepable tokens to Hyperdrive. + sweepable.mint(address(hyperdrive), 100e18); + } + + function test_sweep_failure_invalid_sweeper() external { + vm.stopPrank(); + vm.startPrank(celine); + + // Trying to call sweep with an invalid sweeper (an address that isn't + // the fee collector or a pauser) should fail. + vm.expectRevert(IHyperdrive.Unauthorized.selector); + hyperdrive.sweep(IERC20(address(sweepable))); + } + + function test_sweep_failure_direct_sweeps() external { + vm.stopPrank(); + vm.startPrank(bob); + + // Trying to sweep the stETH token should fail. + address lido = address(hyperdrive.lido()); + vm.expectRevert(IHyperdrive.UnsupportedToken.selector); + hyperdrive.sweep(IERC20(lido)); + } + + function test_sweep_failure_indirect_sweeps() external { + vm.stopPrank(); + vm.startPrank(bob); + + // Trying to sweep the stETH via the forwarding token should fail. + vm.expectRevert(IHyperdrive.SweepFailed.selector); + hyperdrive.sweep(IERC20(address(lidoForwarder))); + } + + function test_sweep_success_feeCollector() external { + // The fee collector can sweep a sweepable token. + vm.stopPrank(); + vm.startPrank(bob); + hyperdrive.sweep(IERC20(address(sweepable))); + } + + function test_sweep_success_pauser() external { + // Set up Celine as a pauser. + vm.stopPrank(); + vm.startPrank(alice); + hyperdrive.setPauser(celine, true); + + // A pauser can sweep a sweepable token. + vm.stopPrank(); + vm.startPrank(celine); + hyperdrive.sweep(IERC20(address(sweepable))); + } +} + +contract LeakyLido is MockLido { + constructor() MockLido(0, address(0), false) {} + + // This function allows other addresses to transfer tokens from a spender. + // This is obviously insecure, but it's an easy way to expose a forwarding + // token that can abuse this leaky transfer function. This gives us a way + // to test the `sweep` function. + function leakyTransferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) { + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } +} + +contract ForwardingToken { + address internal target; + + constructor(address _target) { + target = _target; + } + + function balanceOf(address account) external view returns (uint256) { + return LeakyLido(target).balanceOf(account); + } + + function transfer(address to, uint256 amount) external returns (bool) { + return LeakyLido(target).leakyTransferFrom(msg.sender, to, amount); + } +} diff --git a/test/integrations/factory/HyperdriveFactory.t.sol b/test/integrations/factory/HyperdriveFactory.t.sol index cd33e0ad1..52ef4a9f9 100644 --- a/test/integrations/factory/HyperdriveFactory.t.sol +++ b/test/integrations/factory/HyperdriveFactory.t.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; +import { ERC4626HyperdriveCoreDeployer } from "contracts/src/deployers/erc4626/ERC4626HyperdriveCoreDeployer.sol"; +import { ERC4626HyperdriveDeployerCoordinator } from "contracts/src/deployers/erc4626/ERC4626HyperdriveDeployerCoordinator.sol"; +import { ERC4626Target0Deployer } from "contracts/src/deployers/erc4626/ERC4626Target0Deployer.sol"; +import { ERC4626Target1Deployer } from "contracts/src/deployers/erc4626/ERC4626Target1Deployer.sol"; +import { ERC4626Target2Deployer } from "contracts/src/deployers/erc4626/ERC4626Target2Deployer.sol"; +import { ERC4626Target3Deployer } from "contracts/src/deployers/erc4626/ERC4626Target3Deployer.sol"; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { MockERC4626, ERC20 } from "contracts/test/MockERC4626.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { ERC4626Target2Deployer } from "contracts/src/instances/ERC4626Target2Deployer.sol"; -import { ERC4626Target3Deployer } from "contracts/src/instances/ERC4626Target3Deployer.sol"; -import { ERC4626HyperdriveCoreDeployer } from "contracts/src/instances/ERC4626HyperdriveCoreDeployer.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; @@ -105,8 +105,8 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { HyperdriveFactory factory; - address hyperdriveDeployer; - address hyperdriveCoreDeployer; + address deployerCoordinator; + address coreDeployer; address target0Deployer; address target1Deployer; address target2Deployer; @@ -131,15 +131,15 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { vm.startPrank(deployer); // Deploy the ERC4626Hyperdrive factory and deployer. - hyperdriveCoreDeployer = address(new ERC4626HyperdriveCoreDeployer()); + coreDeployer = address(new ERC4626HyperdriveCoreDeployer()); target0Deployer = address(new ERC4626Target0Deployer()); target1Deployer = address(new ERC4626Target1Deployer()); target2Deployer = address(new ERC4626Target2Deployer()); target3Deployer = address(new ERC4626Target3Deployer()); - hyperdriveDeployer = address( - new ERC4626HyperdriveDeployer( - hyperdriveCoreDeployer, + deployerCoordinator = address( + new ERC4626HyperdriveDeployerCoordinator( + coreDeployer, target0Deployer, target1Deployer, target2Deployer, @@ -180,7 +180,7 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { vm.stopPrank(); vm.prank(alice); - factory.addHyperdriveDeployer(hyperdriveDeployer); + factory.addHyperdriveDeployer(deployerCoordinator); // Deploy yield sources pool1 = IERC4626( @@ -223,7 +223,7 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { dai.approve(address(factory), CONTRIBUTION); IHyperdrive hyperdrive = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(pool), new address[](0)), // TODO: Add test with sweeps CONTRIBUTION, @@ -238,15 +238,15 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { } contract ERC4626FactoryMultiDeployTest is HyperdriveFactoryBaseTest { - address hyperdriveDeployer1; + address deployerCoordinator1; function setUp() public override { super.setUp(); // Deploy a new hyperdrive deployer to demonstrate multiple deployers can be used // with different hyperdrive implementations. The first implementation is ERC4626 so // the logic is the same, but future implementations may have different logic. - hyperdriveDeployer1 = address( - new ERC4626HyperdriveDeployer( + deployerCoordinator1 = address( + new ERC4626HyperdriveDeployerCoordinator( address(new ERC4626HyperdriveCoreDeployer()), address(new ERC4626Target0Deployer()), address(new ERC4626Target1Deployer()), @@ -256,7 +256,7 @@ contract ERC4626FactoryMultiDeployTest is HyperdriveFactoryBaseTest { ); vm.prank(alice); - factory.addHyperdriveDeployer(hyperdriveDeployer1); + factory.addHyperdriveDeployer(deployerCoordinator1); } function test_hyperdriveFactoryDeploy_multiDeploy_multiPool() external { @@ -275,7 +275,7 @@ contract ERC4626FactoryMultiDeployTest is HyperdriveFactoryBaseTest { assertEq(dai.balanceOf(address(pool1)), 0); IHyperdrive hyperdrive1 = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(pool1), new address[](0)), CONTRIBUTION, @@ -321,7 +321,7 @@ contract ERC4626FactoryMultiDeployTest is HyperdriveFactoryBaseTest { dai.approve(address(factory), CONTRIBUTION); IHyperdrive hyperdrive2 = factory.deployAndInitialize( - hyperdriveDeployer1, + deployerCoordinator1, config, abi.encode(address(pool2), new address[](0)), CONTRIBUTION, @@ -374,7 +374,7 @@ contract ERC4626FactoryMultiDeployTest is HyperdriveFactoryBaseTest { assertEq(dai.balanceOf(address(pool2)), CONTRIBUTION); // From Charlie IHyperdrive hyperdrive3 = factory.deployAndInitialize( - hyperdriveDeployer, + deployerCoordinator, config, abi.encode(address(pool2), new address[](0)), CONTRIBUTION, @@ -521,7 +521,7 @@ contract HyperdriveDeployerGetterTest is HyperdriveTest { for (uint256 i; i < numberOfHyperdriveDeployers; i++) { hyperdriveDeployers[i] = address( - new ERC4626HyperdriveDeployer( + new ERC4626HyperdriveDeployerCoordinator( address(new ERC4626HyperdriveCoreDeployer()), address(new ERC4626Target0Deployer()), address(new ERC4626Target1Deployer()), @@ -555,7 +555,7 @@ contract HyperdriveDeployerGetterTest is HyperdriveTest { for (uint256 i; i < numberOfHyperdriveDeployers; i++) { hyperdriveDeployers[i] = address( - new ERC4626HyperdriveDeployer( + new ERC4626HyperdriveDeployerCoordinator( address(new ERC4626HyperdriveCoreDeployer()), address(new ERC4626Target0Deployer()), address(new ERC4626Target1Deployer()), @@ -599,7 +599,7 @@ contract HyperdriveDeployerGetterTest is HyperdriveTest { for (uint256 i; i < numberOfHyperdriveDeployers; i++) { hyperdriveDeployers[i] = address( - new ERC4626HyperdriveDeployer( + new ERC4626HyperdriveDeployerCoordinator( address(new ERC4626HyperdriveCoreDeployer()), address(new ERC4626Target0Deployer()), address(new ERC4626Target1Deployer()), diff --git a/test/units/hyperdrive/HyperdriveDeploy.t.sol b/test/units/hyperdrive/HyperdriveDeploy.t.sol index 475b4a4bc..0c297bf56 100644 --- a/test/units/hyperdrive/HyperdriveDeploy.t.sol +++ b/test/units/hyperdrive/HyperdriveDeploy.t.sol @@ -2,21 +2,9 @@ pragma solidity 0.8.19; import { HyperdriveFactory } from "contracts/src/factory/HyperdriveFactory.sol"; -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { ERC4626HyperdriveDeployer } from "contracts/src/instances/ERC4626HyperdriveDeployer.sol"; -import { ERC4626Target0Deployer } from "contracts/src/instances/ERC4626Target0Deployer.sol"; -import { ERC4626Target1Deployer } from "contracts/src/instances/ERC4626Target1Deployer.sol"; -import { AssetId } from "contracts/src/libraries/AssetId.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; -import { MockERC4626, ERC20 } from "contracts/test/MockERC4626.sol"; -import { MockERC4626Hyperdrive } from "contracts/test/MockERC4626Hyperdrive.sol"; import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; contract HyperdriveFactoryTest is HyperdriveTest { function test_hyperdrive_factory_admin_functions() diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index 906587ca8..ece5e625b 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -1625,9 +1625,6 @@ library HyperdriveUtils { if (_selector == IHyperdrive.UnsupportedToken.selector) { return "UnsupportedToken"; } - if (_selector == IHyperdrive.ApprovalFailed.selector) { - return "ApprovalFailed"; - } if (_selector == IHyperdrive.MinimumTransactionAmount.selector) { return "MinimumTransactionAmount"; }