diff --git a/.gitmodules b/.gitmodules index e06477ca3..be2dbaaf5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/yield-daddy"] + path = lib/yield-daddy + url = https://github.com/timeless-fi/yield-daddy diff --git a/README.md b/README.md index 28c599baa..66bce1b0f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ practices and style, prettier is a Solidity formatter that checks for formatting and style, and cSpell is a spell checker. To run all three, run `yarn lint`. If you want to automatically format the code, run `yarn prettier`. +## Yield Sources + +The current suggested way of integrating your yield source with hyperdrive is through the [ERC-4626 standard](https://eips.ethereum.org/EIPS/eip-4626) although accomodations can be made if this is not possible. Hyperdrive currently makes use of [Yield Daddy](https://github.com/timeless-fi/yield-daddy) to wrap many existing yield sources into this standard. + # Disclaimer The language used in this codebase is for coding convenience only, and is not diff --git a/contracts/src/factory/AaveHyperdriveDeployer.sol b/contracts/src/factory/AaveHyperdriveDeployer.sol deleted file mode 100644 index 20f0ff261..000000000 --- a/contracts/src/factory/AaveHyperdriveDeployer.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IPool } from "@aave/interfaces/IPool.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { AaveHyperdrive } from "../instances/AaveHyperdrive.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; - -/// @author DELV -/// @title AaveHyperdriveDeployer -/// @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. -/// @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 AaveHyperdriveDeployer is IHyperdriveDeployer { - IPool public immutable pool; - - constructor(IPool _pool) { - pool = _pool; - } - - /// @notice Deploys a copy of hyperdrive with the given params. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The address of the factory which is used to deploy - /// the ERC20 linker contracts. - /// @param _extraData This extra data contains the address of the aToken. - /// @return The address of the newly deployed AaveHyperdrive Instance - function deploy( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - bytes32[] calldata _extraData - ) external override returns (address) { - // Deploy the hyperdrive instance. - IERC20 aToken = IERC20(address(uint160(uint256(_extraData[0])))); - return ( - address( - new AaveHyperdrive( - _config, - _dataProvider, - _linkerCodeHash, - _linkerFactory, - aToken, - pool - ) - ) - ); - } -} diff --git a/contracts/src/factory/AaveHyperdriveFactory.sol b/contracts/src/factory/AaveHyperdriveFactory.sol deleted file mode 100644 index c20f729b6..000000000 --- a/contracts/src/factory/AaveHyperdriveFactory.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IPool } from "@aave/interfaces/IPool.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { HyperdriveFactory } from "./HyperdriveFactory.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; -import { AaveHyperdriveDataProvider } from "../instances/AaveHyperdriveDataProvider.sol"; - -/// @author DELV -/// @title AaveHyperdriveFactory -/// @notice Deploys hyperdrive instances and initializes them. It also holds a -/// registry of all deployed hyperdrive instances. -/// @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 AaveHyperdriveFactory is HyperdriveFactory { - // solhint-disable no-empty-blocks - /// @notice Deploys the contract - /// @param _governance The address which can update this factory. - /// @param _deployer The contract which holds the bytecode and deploys new versions. - /// @param _hyperdriveGovernance The address which is set as the governor of hyperdrive - /// @param _feeCollector The address which should be set as the fee collector in new deployments - /// @param _fees The fees each deployed instance from this contract will have - /// @param _defaultPausers The default addresses which will be set to have the pauser role - /// @param _linkerFactory The address of the linker factory - /// @param _linkerCodeHash The hash of the linker contract's constructor code. - constructor( - address _governance, - IHyperdriveDeployer _deployer, - address _hyperdriveGovernance, - address _feeCollector, - IHyperdrive.Fees memory _fees, - address[] memory _defaultPausers, - address _linkerFactory, - bytes32 _linkerCodeHash - ) - HyperdriveFactory( - _governance, - _deployer, - _hyperdriveGovernance, - _feeCollector, - _fees, - _defaultPausers, - _linkerFactory, - _linkerCodeHash - ) - {} - - /// @notice Deploys a copy of hyperdrive with the given params - /// @param _config The configuration of the Hyperdrive pool. - /// @param _contribution Base token to call init with - /// @param _apr The apr to call init with - /// @return The hyperdrive address deployed - function deployAndInitialize( - IHyperdrive.PoolConfig memory _config, - bytes32[] memory, - uint256 _contribution, - uint256 _apr - ) public payable override returns (IHyperdrive) { - // Ensure that ether wasn't sent. This is only marked as payable to - // satisfy the interface. - if (msg.value > 0) { - revert IHyperdrive.NotPayable(); - } - - // Encode the aToken address corresponding to the base token in the - // extra data passed to `deployAndInitialize`. - IPool pool = IAaveDeployer(address(hyperdriveDeployer)).pool(); - address aToken = pool - .getReserveData(address(_config.baseToken)) - .aTokenAddress; - if (address(_config.baseToken) == address(0) || aToken == address(0)) { - revert IHyperdrive.InvalidToken(); - } - - bytes32[] memory extraData = new bytes32[](1); - extraData[0] = bytes32(uint256(uint160(address(aToken)))); - - // Deploy and initialize the hyperdrive instance. - return - super.deployAndInitialize(_config, extraData, _contribution, _apr); - } - - /// @notice This deploys a data provider for the aave hyperdrive instance - /// @param _config The configuration of the pool we are deploying - /// @param _extraData An array containing the AToken in the first slot - /// @param _linkerCodeHash The code hash from the multitoken deployer - /// @param _linkerFactory The factory of the multitoken deployer - function deployDataProvider( - IHyperdrive.PoolConfig memory _config, - bytes32[] memory _extraData, - bytes32 _linkerCodeHash, - address _linkerFactory - ) internal override returns (address) { - // Since we know this has to be the aave pool we abuse the interface to do this - IPool pool = IAaveDeployer(address(hyperdriveDeployer)).pool(); - - return ( - address( - new AaveHyperdriveDataProvider( - _config, - _linkerCodeHash, - _linkerFactory, - IERC20(address(uint160(uint256(_extraData[0])))), - pool - ) - ) - ); - } -} - -interface IAaveDeployer { - function pool() external returns (IPool); -} diff --git a/contracts/src/factory/DsrHyperdriveDeployer.sol b/contracts/src/factory/DsrHyperdriveDeployer.sol deleted file mode 100644 index 5c5bfecd3..000000000 --- a/contracts/src/factory/DsrHyperdriveDeployer.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { DsrHyperdrive } from "../instances/DsrHyperdrive.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; -import { DsrManager } from "../interfaces/IMaker.sol"; - -/// @author DELV -/// @title DsrHyperdriveFactory -/// @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. -/// @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 DsrHyperdriveDeployer is IHyperdriveDeployer { - DsrManager internal immutable dsrManager; - - constructor(DsrManager _dsrManager) { - dsrManager = _dsrManager; - } - - /// @notice Deploys a copy of hyperdrive with the given params. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The address of the factory which is used to deploy - /// the ERC20 linker contracts. - /// @return The address of the newly deployed DsrHyperdrive Instance - function deploy( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - bytes32[] calldata - ) external override returns (address) { - return ( - address( - new DsrHyperdrive( - _config, - _dataProvider, - _linkerCodeHash, - _linkerFactory, - dsrManager - ) - ) - ); - } -} diff --git a/contracts/src/factory/DsrHyperdriveFactory.sol b/contracts/src/factory/DsrHyperdriveFactory.sol deleted file mode 100644 index cf8f03709..000000000 --- a/contracts/src/factory/DsrHyperdriveFactory.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; -import { DsrHyperdriveDataProvider } from "../instances/DsrHyperdriveDataProvider.sol"; -import { HyperdriveFactory } from "./HyperdriveFactory.sol"; -import { DsrManager } from "../interfaces/IMaker.sol"; - -/// @author DELV -/// @title DSRHyperdriveFactory -/// @notice Deploys hyperdrive instances and initializes them. It also holds a -/// registry of all deployed hyperdrive instances. -/// @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 DsrHyperdriveFactory is HyperdriveFactory { - DsrManager internal immutable manager; - - /// @notice Deploys the contract - /// @param _governance The address which can update this factory. - /// @param _deployer The contract which holds the bytecode and deploys new versions. - /// @param _hyperdriveGovernance The address which is set as the governor of hyperdrive - /// @param _feeCollector The address which should be set as the fee collector in new deployments - /// @param _fees The fees each deployed instance from this contract will have - /// @param _defaultPausers The default addresses which will be set to have the pauser role - /// @param _linkerFactory The address of the linker factory - /// @param _linkerCodeHash The hash of the linker contract's constructor code. - /// @param dsrManager The Maker DSR manger contract address - constructor( - address _governance, - IHyperdriveDeployer _deployer, - address _hyperdriveGovernance, - address _feeCollector, - IHyperdrive.Fees memory _fees, - address[] memory _defaultPausers, - address _linkerFactory, - bytes32 _linkerCodeHash, - address dsrManager - ) - HyperdriveFactory( - _governance, - _deployer, - _hyperdriveGovernance, - _feeCollector, - _fees, - _defaultPausers, - _linkerFactory, - _linkerCodeHash - ) - { - manager = DsrManager(dsrManager); - } - - /// @notice This deploys a data provider for the DSR hyperdrive instance - /// @param _config The configuration of the pool we are deploying - /// @param _linkerCodeHash The code hash from the multitoken deployer - /// @param _linkerFactory The factory of the multitoken deployer - function deployDataProvider( - IHyperdrive.PoolConfig memory _config, - bytes32[] memory, - bytes32 _linkerCodeHash, - address _linkerFactory - ) internal override returns (address) { - return ( - address( - new DsrHyperdriveDataProvider( - _config, - _linkerCodeHash, - _linkerFactory, - manager - ) - ) - ); - } -} diff --git a/contracts/src/factory/HyperdriveFactory.sol b/contracts/src/factory/HyperdriveFactory.sol index 7939519e3..f6ff2d6f3 100644 --- a/contracts/src/factory/HyperdriveFactory.sol +++ b/contracts/src/factory/HyperdriveFactory.sol @@ -195,6 +195,9 @@ abstract contract HyperdriveFactory { } /// @notice Deploys a copy of hyperdrive with the given params + /// @dev Function is declared payable to allow payable overrides + /// for accepting Ether on initialization, but not supported + /// by default within this instance. /// @param _config The configuration of the Hyperdrive pool. /// @param _extraData The extra data is used by some factories /// @param _contribution Base token to call init with @@ -206,6 +209,9 @@ abstract contract HyperdriveFactory { uint256 _contribution, uint256 _apr ) public payable virtual returns (IHyperdrive) { + if (msg.value > 0) { + revert IHyperdrive.NonPayableInitialization(); + } // Overwrite the governance and fees field of the config. _config.feeCollector = feeCollector; _config.governance = address(this); @@ -230,35 +236,18 @@ abstract contract HyperdriveFactory { ) ); - // We only do ERC20 transfers when we deploy an ERC20 pool - if (address(_config.baseToken) != ETH) { - // Initialize the Hyperdrive instance. - _config.baseToken.transferFrom( - msg.sender, - address(this), - _contribution - ); - if ( - !_config.baseToken.approve( - address(hyperdrive), - type(uint256).max - ) - ) { - revert IHyperdrive.ApprovalFailed(); - } - hyperdrive.initialize(_contribution, _apr, msg.sender, true); - } else { - // Require the caller sent value - if (msg.value != _contribution) { - revert IHyperdrive.TransferFailed(); - } - hyperdrive.initialize{ value: _contribution }( - _contribution, - _apr, - msg.sender, - true - ); + // Initialize the Hyperdrive instance. + _config.baseToken.transferFrom( + msg.sender, + address(this), + _contribution + ); + if ( + !_config.baseToken.approve(address(hyperdrive), type(uint256).max) + ) { + revert IHyperdrive.ApprovalFailed(); } + hyperdrive.initialize(_contribution, _apr, msg.sender, true); // Setup the pausers roles from the default array for (uint256 i = 0; i < defaultPausers.length; i++) { diff --git a/contracts/src/factory/StethHyperdriveDeployer.sol b/contracts/src/factory/StethHyperdriveDeployer.sol deleted file mode 100644 index 654ab5808..000000000 --- a/contracts/src/factory/StethHyperdriveDeployer.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IERC20 } from "../interfaces/IERC20.sol"; -import { StethHyperdrive } from "../instances/StethHyperdrive.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; -import { ILido } from "../interfaces/ILido.sol"; -import { IWETH } from "../interfaces/IWETH.sol"; - -/// @author DELV -/// @title StethHyperdriveDeployer -/// @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. -/// @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 StethHyperdriveDeployer is IHyperdriveDeployer { - /// @dev The Lido contract. - ILido internal immutable lido; - - /// @dev A constant for the ETH value - address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - /// @notice Initializes the factory. - /// @param _lido The Lido contract. - constructor(ILido _lido) { - lido = _lido; - } - - /// @notice Deploys a copy of hyperdrive with the given params. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The address of the factory which is used to deploy - /// the ERC20 linker contracts. - /// @return The address of the newly deployed StethHyperdrive Instance - function deploy( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - bytes32[] calldata - ) external override returns (address) { - // Ensure that the base token is configured properly. - if (address(_config.baseToken) != ETH) { - revert IHyperdrive.InvalidBaseToken(); - } - - return ( - address( - new StethHyperdrive( - _config, - _dataProvider, - _linkerCodeHash, - _linkerFactory, - lido - ) - ) - ); - } -} diff --git a/contracts/src/factory/StethHyperdriveFactory.sol b/contracts/src/factory/StethHyperdriveFactory.sol deleted file mode 100644 index d7649cec3..000000000 --- a/contracts/src/factory/StethHyperdriveFactory.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { HyperdriveFactory } from "./HyperdriveFactory.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "../interfaces/IHyperdriveDeployer.sol"; -import { ILido } from "../interfaces/ILido.sol"; -import { IWETH } from "../interfaces/IWETH.sol"; -import { StethHyperdriveDataProvider } from "../instances/StethHyperdriveDataProvider.sol"; - -/// @author DELV -/// @title StethHyperdriveFactory -/// @notice Deploys StethHyperdrive instances and initializes them. It also -/// holds a registry of all deployed hyperdrive instances. -/// @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 StethHyperdriveFactory is HyperdriveFactory { - /// @dev The Lido contract. - ILido internal immutable lido; - - /// @notice Deploys the contract - /// @param _governance The address which can update this factory. - /// @param _deployer The contract which holds the bytecode and deploys new versions. - /// @param _hyperdriveGovernance The address which is set as the governor of hyperdrive - /// @param _feeCollector The address which should be set as the fee collector in new deployments - /// @param _fees The fees each deployed instance from this contract will have - /// @param _defaultPausers The default addresses which will be set to have the pauser role - /// @param _linkerFactory The address of the linker factory - /// @param _linkerCodeHash The hash of the linker contract's constructor code. - /// @param _lido The Lido contract. - constructor( - address _governance, - IHyperdriveDeployer _deployer, - address _hyperdriveGovernance, - address _feeCollector, - IHyperdrive.Fees memory _fees, - address[] memory _defaultPausers, - address _linkerFactory, - bytes32 _linkerCodeHash, - ILido _lido - ) - HyperdriveFactory( - _governance, - _deployer, - _hyperdriveGovernance, - _feeCollector, - _fees, - _defaultPausers, - _linkerFactory, - _linkerCodeHash - ) - { - lido = _lido; - } - - /// @notice This deploys a data provider for the aave hyperdrive instance - /// @param _config The configuration of the pool we are deploying - /// @param _linkerCodeHash The code hash from the multitoken deployer - /// @param _linkerFactory The factory of the multitoken deployer - function deployDataProvider( - IHyperdrive.PoolConfig memory _config, - bytes32[] memory, - bytes32 _linkerCodeHash, - address _linkerFactory - ) internal override returns (address) { - return ( - address( - new StethHyperdriveDataProvider( - _config, - _linkerCodeHash, - _linkerFactory, - lido - ) - ) - ); - } -} diff --git a/contracts/src/instances/AaveHyperdrive.sol b/contracts/src/instances/AaveHyperdrive.sol deleted file mode 100644 index 328f511f6..000000000 --- a/contracts/src/instances/AaveHyperdrive.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IPool } from "@aave/interfaces/IPool.sol"; -import { Hyperdrive } from "../Hyperdrive.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; - -/// @author DELV -/// @title AaveHyperdrive -/// @notice An instance of Hyperdrive that utilizes Aave's lending pool as a 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 AaveHyperdrive is Hyperdrive { - using FixedPointMath for uint256; - - // The aave deployment details, the a token for this asset and the aave pool - IERC20 internal immutable aToken; - IPool internal immutable pool; - - // The shares created by this pool, starts at one to one with deposits and increases - uint256 internal totalShares; - - /// @notice Initializes a Hyperdrive pool. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The factory which is used to deploy the ERC20 - /// linker contracts. - /// @param _aToken The assets aToken. - /// @param _pool The aave pool. - constructor( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - IERC20 _aToken, - IPool _pool - ) Hyperdrive(_config, _dataProvider, _linkerCodeHash, _linkerFactory) { - // Ensure that the Hyperdrive pool was configured properly. - if (_config.initialSharePrice != FixedPointMath.ONE_18) { - revert IHyperdrive.InvalidInitialSharePrice(); - } - - aToken = _aToken; - pool = _pool; - if (!_config.baseToken.approve(address(pool), type(uint256).max)) { - revert IHyperdrive.ApprovalFailed(); - } - } - - /// @notice Transfers amount of 'token' from the user and commits it to the yield source. - /// @param amount The amount of token to transfer - /// @param asUnderlying If true the yield source will transfer underlying tokens - /// if false it will transfer the yielding asset directly - /// @return sharesMinted The shares this deposit creates - /// @return sharePrice The share price at time of deposit - function _deposit( - uint256 amount, - bool asUnderlying - ) internal override returns (uint256 sharesMinted, uint256 sharePrice) { - // Load the balance of this pool - uint256 assets = aToken.balanceOf(address(this)); - - if (asUnderlying) { - // Transfer from user - bool success = _baseToken.transferFrom( - msg.sender, - address(this), - amount - ); - if (!success) { - revert IHyperdrive.TransferFailed(); - } - // Supply for the user - pool.supply(address(_baseToken), amount, address(this), 0); - } else { - // aTokens are known to be revert on failed transfer tokens - aToken.transferFrom(msg.sender, address(this), amount); - } - - // Do share calculations - uint256 totalShares_ = totalShares; - if (totalShares_ == 0) { - totalShares = amount; - return (amount, FixedPointMath.ONE_18); - } else { - uint256 newShares = totalShares_.mulDivDown(amount, assets); - totalShares += newShares; - return (newShares, _pricePerShare()); - } - } - - /// @notice Withdraws shares from the yield source and sends the resulting tokens to the destination - /// @param shares The shares to withdraw from the yield source - /// @param asUnderlying If true the yield source will transfer underlying tokens - /// if false it will transfer the yielding asset directly - /// @param destination The address which is where to send the resulting tokens - /// @return amountWithdrawn the amount of 'token' produced by this withdraw - function _withdraw( - uint256 shares, - address destination, - bool asUnderlying - ) internal override returns (uint256 amountWithdrawn) { - // The withdrawer receives a proportional amount of the assets held by - // the contract to the amount of shares that they are redeeming. Small - // numerical errors can result in the shares value being slightly larger - // than the total shares, so we clamp the shares to the total shares to - // avoid reverts. - uint256 totalShares_ = totalShares; - if (shares > totalShares_) { - shares = totalShares_; - } - uint256 assets = aToken.balanceOf(address(this)); - uint256 withdrawValue = assets != 0 - ? shares.mulDown(assets.divDown(totalShares_)) - : 0; - - if (withdrawValue == 0) { - revert IHyperdrive.NoAssetsToWithdraw(); - } - - // Remove the shares from the total share supply - totalShares -= shares; - - // If the user wants underlying we withdraw for them otherwise send the base - if (asUnderlying) { - // Now we call aave to fulfill this withdraw for the user - uint256 amountOut = pool.withdraw( - address(_baseToken), - withdrawValue, - destination - ); - require(amountOut == withdrawValue); - } else { - // Otherwise we simply transfer to them - aToken.transfer(destination, withdrawValue); - } - - return withdrawValue; - } - - /// @notice Loads the share price from the yield source. - /// @dev This must remain consistent with the impl inside of the DataProvider - /// @return The current share price. - function _pricePerShare() internal view override returns (uint256) { - uint256 assets = aToken.balanceOf(address(this)); - uint256 totalShares_ = totalShares; - if (totalShares_ != 0) { - return assets.divDown(totalShares_); - } - return 0; - } -} diff --git a/contracts/src/instances/AaveHyperdriveDataProvider.sol b/contracts/src/instances/AaveHyperdriveDataProvider.sol deleted file mode 100644 index 617bafec2..000000000 --- a/contracts/src/instances/AaveHyperdriveDataProvider.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IPool } from "@aave/interfaces/IPool.sol"; -import { HyperdriveDataProvider } from "../HyperdriveDataProvider.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; -import { MultiTokenDataProvider } from "../token/MultiTokenDataProvider.sol"; - -/// @author DELV -/// @title AaveHyperdriveDataProvider -/// @notice The data provider for AaveHyperdrive instances. -/// @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 AaveHyperdriveDataProvider is - MultiTokenDataProvider, - HyperdriveDataProvider -{ - using FixedPointMath for uint256; - - // The aave deployment details, the aave pool - IERC20 internal immutable _aToken; - IPool internal immutable _pool; - - // The shares created by this pool, starts at one to one with deposits and increases - uint256 internal _totalShares; - - /// @notice Initializes the data provider. - /// @param _linkerCodeHash_ The hash of the erc20 linker contract deploy code - /// @param _factory_ The factory which is used to deploy the linking contracts - /// @param _aToken_ The assets aToken. - /// @param _pool_ The aave pool. - constructor( - IHyperdrive.PoolConfig memory _config, - bytes32 _linkerCodeHash_, - address _factory_, - IERC20 _aToken_, - IPool _pool_ - ) - HyperdriveDataProvider(_config) - MultiTokenDataProvider(_linkerCodeHash_, _factory_) - { - _aToken = _aToken_; - _pool = _pool_; - } - - /// Yield Source /// - - /// @notice Loads the share price from the yield source. - /// @return sharePrice The current share price. - /// @dev must remain consistent with the impl inside of the HyperdriveInstance - function _pricePerShare() - internal - view - override - returns (uint256 sharePrice) - { - uint256 assets = _aToken.balanceOf(address(this)); - sharePrice = _totalShares != 0 ? assets.divDown(_totalShares) : 0; - return sharePrice; - } - - /// Getters /// - - /// @notice Gets the aave aToken. - /// @return The aave aToken. - function aToken() external view returns (IERC20) { - _revert(abi.encode(_aToken)); - } - - /// @notice Gets the aave pool. - /// @return The aave pool. - function pool() external view returns (IPool) { - _revert(abi.encode(_pool)); - } - - /// @notice Gets the total shares. - /// @return The total shares. - function totalShares() external view returns (uint256) { - _revert(abi.encode(_totalShares)); - } -} diff --git a/contracts/src/instances/DsrHyperdrive.sol b/contracts/src/instances/DsrHyperdrive.sol deleted file mode 100644 index 9e6d456d9..000000000 --- a/contracts/src/instances/DsrHyperdrive.sol +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { Hyperdrive } from "../Hyperdrive.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { Pot, DsrManager } from "../interfaces/IMaker.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; - -/// @author DELV -/// @title DsrHyperdrive -/// @notice An instance of Hyperdrive that utilizes Maker's DSR as a 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 DsrHyperdrive is Hyperdrive { - using FixedPointMath for uint256; - - // @notice The shares created by this pool, starts at 1 to one with - // deposits and increases - uint256 internal totalShares; - - // @notice The pool management contract - DsrManager internal immutable dsrManager; - - // @notice The core Maker accounting module for the Dai Savings Rate - Pot internal immutable pot; - - // @notice Maker constant - uint256 internal constant RAY = 1e27; - - /// @notice Initializes a Hyperdrive pool. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The factory which is used to deploy the ERC20 - /// linker contracts. - /// @param _dsrManager The "dai savings rate" manager contract - constructor( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - DsrManager _dsrManager - ) Hyperdrive(_config, _dataProvider, _linkerCodeHash, _linkerFactory) { - // Ensure that the base token is DAI. - if (address(_config.baseToken) != address(_dsrManager.dai())) { - revert IHyperdrive.InvalidBaseToken(); - } - - // Ensure that the minimum share reserves are equal to 10e18. This value - // has been tested to prevent arithmetic overflows in the - // `_updateLiquidity` function when the share reserves are as high as - // 100 billion. - if (_config.minimumShareReserves != 10e18) { - revert IHyperdrive.InvalidMinimumShareReserves(); - } - - // Ensure that the initial share price is one. - if (_config.initialSharePrice != FixedPointMath.ONE_18) { - revert IHyperdrive.InvalidInitialSharePrice(); - } - - dsrManager = _dsrManager; - pot = Pot(dsrManager.pot()); - if (!_baseToken.approve(address(dsrManager), type(uint256).max)) { - revert IHyperdrive.ApprovalFailed(); - } - } - - /// @notice Transfers base or shares from the user and commits it to the yield source. - /// @param amount The amount of base tokens to deposit. - /// @param asUnderlying The DSR yield source only supports depositing the - /// underlying token. If this is false, the transaction will revert. - /// @return sharesMinted The shares this deposit creates. - /// @return sharePrice The share price at time of deposit. - function _deposit( - uint256 amount, - bool asUnderlying - ) internal override returns (uint256 sharesMinted, uint256 sharePrice) { - if (!asUnderlying) { - revert IHyperdrive.UnsupportedToken(); - } - - // Transfer the base token from the user to this contract - bool success = _baseToken.transferFrom( - msg.sender, - address(this), - amount - ); - if (!success) { - revert IHyperdrive.TransferFailed(); - } - - // Get total invested balance of pool, deposits + interest - uint256 totalBase = dsrManager.daiBalance(address(this)); - - // Deposit the base tokens into the dsr - dsrManager.join(address(this), amount); - - // Do share calculations - uint256 totalShares_ = totalShares; - if (totalShares_ == 0) { - totalShares = amount; - // Initial deposits are always 1:1 - return (amount, FixedPointMath.ONE_18); - } else { - uint256 newShares = totalShares_.mulDivDown(amount, totalBase); - totalShares += newShares; - return (newShares, _pricePerShare()); - } - } - - /// @notice Withdraws shares from the yield source and sends the resulting tokens to the destination - /// @param shares The shares to withdraw from the yield source - /// @param destination The address which is where to send the resulting tokens - /// @param asUnderlying The DSR yield source only supports depositing the - /// underlying token. If this is false, the transaction will revert. - /// @return amountWithdrawn the amount of 'token' produced by this withdraw - function _withdraw( - uint256 shares, - address destination, - bool asUnderlying - ) internal override returns (uint256 amountWithdrawn) { - if (!asUnderlying) { - revert IHyperdrive.UnsupportedToken(); - } - - // Small numerical errors can result in the shares value being slightly - // larger than the total shares, so we clamp the shares to the total - // shares to avoid reverts. - uint256 totalShares_ = totalShares; - if (shares > totalShares_) { - shares = totalShares_; - } - - // Load the balance of this contract - this calls drip internally so - // this is real deposits + interest accrued at point in time - uint256 totalBase = dsrManager.daiBalance(address(this)); - - // The withdraw is the percent of shares the user has times the total assets - amountWithdrawn = totalBase.mulDivDown(shares, totalShares_); - - // Remove shares from the total supply - totalShares -= shares; - - // Withdraw pro-rata share of underlying to user - dsrManager.exit(destination, amountWithdrawn); - - return amountWithdrawn; - } - - /// @notice Loads the share price from the yield source. - /// @return sharePrice The current share price. - /// @dev must remain consistent with the impl inside of the DataProvider - function _pricePerShare() - internal - view - override - returns (uint256 sharePrice) - { - uint256 pie = dsrManager.pieOf(address(this)); - uint256 totalBase = pie.mulDivDown(chi(), RAY); - // The share price is assets divided by shares - uint256 totalShares_ = totalShares; - if (totalShares_ != 0) { - return (totalBase.divDown(totalShares_)); - } - return 0; - } - - /// @notice Gets the current up to date value of the rate accumulator - /// @dev The Maker protocol uses a tick based accounting mechanic to - /// accumulate interest in a single variable called the rate - /// accumulator or more commonly "chi". - /// This is re-calibrated on any interaction with the maker protocol by - /// a function pot.drip(). The rationale for not using this is that it - /// is not a view function and so the purpose of this function is to - /// get the real chi value without interacting with the core maker - /// system and expensively mutating state. - /// return chi The rate accumulator - function chi() internal view returns (uint256) { - // timestamp when drip was last called - uint256 rho = pot.rho(); - // Rate accumulator as of last drip - uint256 _chi = pot.chi(); - // Annualized interest rate - uint256 dsr = pot.dsr(); - // Calibrates the rate accumulator to current time - return - (block.timestamp > rho) - ? _rpow(dsr, block.timestamp - rho, RAY).mulDivDown(_chi, RAY) - : _chi; - } - - /// @notice Taken from https://github.com/makerdao/dss/blob/master/src/pot.sol#L85 - /// @return z - function _rpow( - uint256 x, - uint256 n, - uint256 base - ) internal pure returns (uint z) { - assembly ("memory-safe") { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/contracts/src/instances/DsrHyperdriveDataProvider.sol b/contracts/src/instances/DsrHyperdriveDataProvider.sol deleted file mode 100644 index 0096d94b1..000000000 --- a/contracts/src/instances/DsrHyperdriveDataProvider.sol +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { HyperdriveDataProvider } from "../HyperdriveDataProvider.sol"; -import { IERC20 } from "../interfaces/IERC20.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { Pot, DsrManager } from "../interfaces/IMaker.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; -import { MultiTokenDataProvider } from "../token/MultiTokenDataProvider.sol"; - -/// @author DELV -/// @title DsrHyperdriveDataProvider -/// @notice The data provider for DsrHyperdrive instances. -/// @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 DsrHyperdriveDataProvider is - MultiTokenDataProvider, - HyperdriveDataProvider -{ - using FixedPointMath for uint256; - - // @notice The shares created by this pool, starts at 1 to one with - // deposits and increases - uint256 internal _totalShares; - - // @notice The pool management contract - DsrManager internal immutable _dsrManager; - - // @notice The core Maker accounting module for the Dai Savings Rate - Pot internal immutable _pot; - - // @notice Maker constant - uint256 internal constant RAY = 1e27; - - /// @notice Initializes the data provider. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _linkerCodeHash_ The hash of the erc20 linker contract deploy code - /// @param _factory_ The factory which is used to deploy the linking contracts - /// @param _dsrManager_ The "dai savings rate" manager contract - constructor( - IHyperdrive.PoolConfig memory _config, - bytes32 _linkerCodeHash_, - address _factory_, - DsrManager _dsrManager_ - ) - HyperdriveDataProvider(_config) - MultiTokenDataProvider(_linkerCodeHash_, _factory_) - { - _dsrManager = _dsrManager_; - _pot = Pot(_dsrManager_.pot()); - } - - /// Getters /// - - /// @notice Gets the DSRManager. - /// @return The DSRManager. - function dsrManager() external view returns (DsrManager) { - _revert(abi.encode(_dsrManager)); - } - - /// @notice The accounting module for the Dai Savings Rate. - /// @return Maker's Pot contract. - function pot() external view returns (Pot) { - _revert(abi.encode(_pot)); - } - - /// @notice Gets the total number of shares in existence. - /// @return The total number of shares. - function totalShares() external view returns (uint256) { - _revert(abi.encode(_totalShares)); - } - - /// Yield Source /// - - /// @notice Loads the share price from the yield source. - /// @return sharePrice The current share price. - /// @dev must remain consistent with the impl inside of the HyperdriveInstance - function _pricePerShare() - internal - view - override - returns (uint256 sharePrice) - { - // The normalized DAI amount owned by this contract - uint256 pie = _dsrManager.pieOf(address(this)); - // Load the balance of this contract - uint256 totalBase = pie.mulDivDown(chi(), RAY); - // The share price is assets divided by shares - return (totalBase.divDown(_totalShares)); - } - - /// TODO Is this actually worthwhile versus using drip? - /// @notice Gets the current up to date value of the rate accumulator - /// @dev The Maker protocol uses a tick based accounting mechanic to - /// accumulate interest in a single variable called the rate - /// accumulator or more commonly "chi". - /// This is re-calibrated on any interaction with the maker protocol by - /// a function pot.drip(). The rationale for not using this is that it - /// is not a view function and so the purpose of this function is to - /// get the real chi value without interacting with the core maker - /// system and expensively mutating state. - /// return chi The rate accumulator - function chi() internal view returns (uint256) { - // timestamp when drip was last called - uint256 rho = _pot.rho(); - // Rate accumulator as of last drip - uint256 _chi = _pot.chi(); - // Annualized interest rate - uint256 dsr = _pot.dsr(); - // Calibrates the rate accumulator to current time - return - (block.timestamp > rho) - ? _rpow(dsr, block.timestamp - rho, RAY).mulDivDown(_chi, RAY) - : _chi; - } - - /// @notice Taken from https://github.com/makerdao/dss/blob/master/src/pot.sol#L85 - /// @return z - function _rpow( - uint256 x, - uint256 n, - uint256 base - ) internal pure returns (uint z) { - assembly ("memory-safe") { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/contracts/src/instances/ERC4626Hyperdrive.sol b/contracts/src/instances/ERC4626Hyperdrive.sol index 9e44dce84..3474dddd3 100644 --- a/contracts/src/instances/ERC4626Hyperdrive.sol +++ b/contracts/src/instances/ERC4626Hyperdrive.sol @@ -120,8 +120,7 @@ contract ERC4626Hyperdrive is Hyperdrive { /// @return The current share price. /// @dev must remain consistent with the impl inside of the DataProvider function _pricePerShare() internal view override returns (uint256) { - uint256 shareEstimate = pool.convertToShares(FixedPointMath.ONE_18); - return (FixedPointMath.ONE_18.divDown(shareEstimate)); + return pool.convertToAssets(FixedPointMath.ONE_18); } /// @notice Some yield sources [eg Morpho] pay rewards directly to this contract diff --git a/contracts/src/instances/StethHyperdrive.sol b/contracts/src/instances/StethHyperdrive.sol deleted file mode 100644 index d5e36e480..000000000 --- a/contracts/src/instances/StethHyperdrive.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { Hyperdrive } from "../Hyperdrive.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { ILido } from "../interfaces/ILido.sol"; -import { IWETH } from "../interfaces/IWETH.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; - -/// @author DELV -/// @title StethHyperdrive -/// @notice An instance of Hyperdrive that utilizes Lido's staked ether (stETH) -/// as a yield source. -/// @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. -contract StethHyperdrive is Hyperdrive { - using FixedPointMath for uint256; - - /// @dev The Lido contract. - ILido internal immutable lido; - - /// @notice Initializes a Hyperdrive pool. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _dataProvider The address of the data provider. - /// @param _linkerCodeHash The hash of the ERC20 linker contract's - /// constructor code. - /// @param _linkerFactory The factory which is used to deploy the ERC20 - /// linker contracts. - /// @param _lido The Lido contract. This is the stETH token. - constructor( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - ILido _lido - ) Hyperdrive(_config, _dataProvider, _linkerCodeHash, _linkerFactory) { - lido = _lido; - - // Ensure that the initial share price is equal to the current share - // price. - if (_initialSharePrice != _pricePerShare()) { - revert IHyperdrive.InvalidInitialSharePrice(); - } - - // Ensure that the minimum share reserves are equal to 1e18. This value - // has been tested to prevent arithmetic overflows in the - // `_updateLiquidity` function when the share reserves are as high as - // 200 million. - if (_config.minimumShareReserves != 1e15) { - revert IHyperdrive.InvalidMinimumShareReserves(); - } - } - - /// @dev We override the message value check since this integration is - /// payable. - function _checkMessageValue() internal pure override {} // solhint-disable-line no-empty-blocks - - /// @dev Accepts a transfer from the user in base or the yield source token. - /// @param _amount The amount to deposit. - /// @param _asUnderlying A flag indicating that the deposit is paid in ETH - /// if true and in stETH if false. If ETH msg.value must equal amount - /// @return shares The amount of shares that represents the amount deposited. - /// @return sharePrice The current share price. - function _deposit( - uint256 _amount, - bool _asUnderlying - ) internal override returns (uint256 shares, uint256 sharePrice) { - if (_asUnderlying) { - // Ensure that sufficient ether was provided and refund any excess. - if (msg.value < _amount) { - revert IHyperdrive.TransferFailed(); - } - if (msg.value > _amount) { - // Return excess ether to the user. - (bool success, ) = payable(msg.sender).call{ - value: msg.value - _amount - }(""); - if (!success) { - revert IHyperdrive.TransferFailed(); - } - } - - // 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 { - // Ensure that the user didn't send ether to the contract. - if (msg.value > 0) { - revert IHyperdrive.NotPayable(); - } - - // Transfer stETH into the contract. - bool success = lido.transferFrom( - msg.sender, - address(this), - _amount - ); - if (!success) { - revert IHyperdrive.TransferFailed(); - } - - // Calculate the share price and the amount of shares deposited. - sharePrice = _pricePerShare(); - shares = _amount.divDown(sharePrice); - } - - return (shares, sharePrice); - } - - /// @dev Withdraws stETH to the destination address. - /// @param _shares The amount of shares to withdraw. - /// @param _destination The recipient of the withdrawal. - /// @param _asUnderlying This must be false since stETH withdrawals aren't - /// processed instantaneously. Users that want to withdraw can manage - /// their withdrawal separately. - /// @return amountWithdrawn The amount of stETH withdrawn. - function _withdraw( - uint256 _shares, - address _destination, - bool _asUnderlying - ) internal override returns (uint256 amountWithdrawn) { - // At the time of writing there's no stETH -> eth withdraw path - if (_asUnderlying) { - revert IHyperdrive.UnsupportedToken(); - } - - // Transfer stETH to the destination. - amountWithdrawn = lido.transferShares(_destination, _shares); - - return amountWithdrawn; - } - - /// @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()); - } -} diff --git a/contracts/src/instances/StethHyperdriveDataProvider.sol b/contracts/src/instances/StethHyperdriveDataProvider.sol deleted file mode 100644 index 8af75a4d6..000000000 --- a/contracts/src/instances/StethHyperdriveDataProvider.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { HyperdriveDataProvider } from "../HyperdriveDataProvider.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; -import { ILido } from "../interfaces/ILido.sol"; -import { FixedPointMath } from "../libraries/FixedPointMath.sol"; -import { MultiTokenDataProvider } from "../token/MultiTokenDataProvider.sol"; - -/// @author DELV -/// @title StethHyperdriveDataProvider -/// @notice The data provider for StethHyperdrive instances. -/// @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 StethHyperdriveDataProvider is - MultiTokenDataProvider, - HyperdriveDataProvider -{ - using FixedPointMath for uint256; - - /// @dev The Lido contract. - ILido internal immutable _lido; - - /// @notice Initializes the data provider. - /// @param _config The configuration of the Hyperdrive pool. - /// @param _linkerCodeHash_ The hash of the erc20 linker contract deploy code. - /// @param _factory_ The factory which is used to deploy the linking contracts. - /// @param _lido_ The Lido contract. This is the stETH token. - constructor( - IHyperdrive.PoolConfig memory _config, - bytes32 _linkerCodeHash_, - address _factory_, - ILido _lido_ - ) - HyperdriveDataProvider(_config) - MultiTokenDataProvider(_linkerCodeHash_, _factory_) - { - _lido = _lido_; - } - - /// Yield Source /// - - /// @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 HyperdriveInstance - function _pricePerShare() internal view override returns (uint256 price) { - return _lido.getTotalPooledEther().divDown(_lido.getTotalShares()); - } - - /// Getters /// - - /// @notice Gets the Lido contract. - /// @return The Lido contract. - function lido() external view returns (ILido) { - _revert(abi.encode(_lido)); - } -} diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index 496ec7c76..ca2e46687 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -236,7 +236,7 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveWrite, IMultiToken { error Unauthorized(); error InvalidContribution(); error InvalidToken(); - + error NonPayableInitialization(); /// ###################### /// ### ERC20Forwarder ### /// ###################### diff --git a/contracts/test/MockDsrHyperdrive.sol b/contracts/test/MockDsrHyperdrive.sol deleted file mode 100644 index 9e3434806..000000000 --- a/contracts/test/MockDsrHyperdrive.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { DsrHyperdrive, DsrManager } from "../src/instances/DsrHyperdrive.sol"; -import { DsrHyperdriveDataProvider } from "../src/instances/DsrHyperdriveDataProvider.sol"; -import { IERC20 } from "../src/interfaces/IERC20.sol"; -import { IHyperdrive } from "../src/interfaces/IHyperdrive.sol"; -import { FixedPointMath } from "../src/libraries/FixedPointMath.sol"; -import { ForwarderFactory } from "../src/token/ForwarderFactory.sol"; - -interface IMockDsrHyperdrive is IHyperdrive { - function totalShares() external view returns (uint256); - - function deposit( - uint256 amount, - bool asUnderlying - ) external returns (uint256, uint256); - - function withdraw( - uint256 shares, - address destination, - bool asUnderlying - ) external returns (uint256); - - function pricePerShare() external view returns (uint256); -} - -contract MockDsrHyperdrive is DsrHyperdrive { - using FixedPointMath for uint256; - - constructor( - address _dataProvider, - DsrManager _dsrManager - ) - DsrHyperdrive( - IHyperdrive.PoolConfig({ - baseToken: IERC20(address(_dsrManager.dai())), - initialSharePrice: FixedPointMath.ONE_18, - minimumShareReserves: 10e18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: FixedPointMath.ONE_18.divDown( - 22.186877016851916266e18 - ), - governance: address(0), - feeCollector: address(0), - fees: IHyperdrive.Fees({ curve: 0, flat: 0, governance: 0 }), - oracleSize: 2, - updateGap: 0 - }), - _dataProvider, - bytes32(0), - address(0), - _dsrManager - ) - {} - - function deposit( - uint256 amount, - bool asUnderlying - ) external returns (uint256, uint256) { - return _deposit(amount, asUnderlying); - } - - function withdraw( - uint256 shares, - address destination, - bool asUnderlying - ) external returns (uint256) { - return _withdraw(shares, destination, asUnderlying); - } - - function pricePerShare() external view returns (uint256) { - return _pricePerShare(); - } -} - -contract MockDsrHyperdriveDataProvider is DsrHyperdriveDataProvider { - using FixedPointMath for uint256; - - constructor( - DsrManager _dsrManager - ) - DsrHyperdriveDataProvider( - IHyperdrive.PoolConfig({ - baseToken: IERC20(address(_dsrManager.dai())), - initialSharePrice: FixedPointMath.ONE_18, - minimumShareReserves: 10e18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: FixedPointMath.ONE_18.divDown( - 22.186877016851916266e18 - ), - governance: address(0), - feeCollector: address(0), - fees: IHyperdrive.Fees({ curve: 0, flat: 0, governance: 0 }), - oracleSize: 2, - updateGap: 0 - }), - bytes32(0), - address(0), - _dsrManager - ) - {} -} diff --git a/lib/yield-daddy b/lib/yield-daddy new file mode 160000 index 000000000..d9ffa1cf2 --- /dev/null +++ b/lib/yield-daddy @@ -0,0 +1 @@ +Subproject commit d9ffa1cf23a630112851285207d8c9fdcc2af748 diff --git a/package.json b/package.json index 1cffa038e..6cd06872f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "code-size-check": "FOUNDRY_PROFILE=production forge build && python python/contract_size.py out", "spell-check": "npx cspell ./**/**/**.sol --gitignore", "style-check": "npx prettier --check .", - "warnings-check": "FOUNDRY_PROFILE=production forge build --deny-warnings --force", + "warnings-check": "FOUNDRY_PROFILE=production forge build --deny-warnings --force --skip test", "postinstall": "patch-package --patch-dir .patches", "configure-husky": "npx husky install && npx husky add .husky/pre-commit \"npx --no-install lint-staged\"", "testnet": "forge script migrations/MockHyperdrive.s.sol:MockHyperdriveScript --rpc-url http://localhost:8545 --broadcast -vvvv" diff --git a/script/CloseLong.s.sol b/script/CloseLong.s.sol index 1b62f90e0..3d4b27bdc 100644 --- a/script/CloseLong.s.sol +++ b/script/CloseLong.s.sol @@ -5,8 +5,6 @@ import "forge-std/Script.sol"; import "forge-std/console.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { DsrHyperdrive } from "contracts/src/instances/DsrHyperdrive.sol"; -import { DsrHyperdriveDataProvider } from "contracts/src/instances/DsrHyperdriveDataProvider.sol"; import { DsrManager } from "contracts/src/interfaces/IMaker.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; diff --git a/script/DsrHyperdrive.s.sol b/script/DsrHyperdrive.s.sol deleted file mode 100644 index 2a0d1e432..000000000 --- a/script/DsrHyperdrive.s.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import "forge-std/Script.sol"; -import "forge-std/console.sol"; - -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { DsrHyperdrive } from "contracts/src/instances/DsrHyperdrive.sol"; -import { DsrHyperdriveDataProvider } from "contracts/src/instances/DsrHyperdriveDataProvider.sol"; -import { DsrManager } from "contracts/src/interfaces/IMaker.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; - -interface Faucet { - function mint(address token, address to, uint256 amount) external; -} - -contract DsrHyperdriveScript is Script { - using FixedPointMath for uint256; - - Faucet internal constant FAUCET = - Faucet(0xe2bE5BfdDbA49A86e27f3Dd95710B528D43272C2); - DsrManager internal constant DSR_MANAGER = - DsrManager(0xF7F0de3744C82825D77EdA8ce78f07A916fB6bE7); - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - // Deploy an instance of DsrHyperdrive. - console.log("Deploying DsrHyperdrive..."); - IERC20 dai = IERC20(DSR_MANAGER.dai()); - IHyperdrive.Fees memory fees = IHyperdrive.Fees({ - curve: 0.1e18, // 10% curve fee - flat: 0.05e18, // 5% flat fee - governance: 0.1e18 // 10% governance fee - }); - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: dai, - initialSharePrice: 1e18, - minimumShareReserves: 10e18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.02e18), - governance: address(0), - feeCollector: address(0), - fees: fees, - oracleSize: 10, - updateGap: 1 hours - }); - DsrHyperdriveDataProvider dataProvider = new DsrHyperdriveDataProvider( - config, - bytes32(0), - address(0), - DSR_MANAGER - ); - IHyperdrive hyperdrive = IHyperdrive( - address( - new DsrHyperdrive( - config, - address(dataProvider), - bytes32(0), - address(0), - DSR_MANAGER - ) - ) - ); - - // Initialize the Hyperdrive instance. - console.log("Initializing DsrHyperdrive..."); - uint256 contribution = 50_000e18; - FAUCET.mint(address(dai), msg.sender, contribution); - dai.approve(address(hyperdrive), contribution); - hyperdrive.initialize(contribution, 0.02e18, msg.sender, true); - - // Ensure that the Hyperdrive instance was initialized properly. - console.log("Verifying deployment..."); - IHyperdrive.PoolConfig memory config_ = hyperdrive.getPoolConfig(); - require(config_.baseToken == dai); - require(config_.initialSharePrice == FixedPointMath.ONE_18); - IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); - require(info.shareReserves == contribution); - require(info.sharePrice == FixedPointMath.ONE_18); - console.log("DsrHyperdrive was deployed successfully."); - - vm.stopBroadcast(); - - console.log("Deployed DsrHyperdrive to: %s", address(hyperdrive)); - } -} diff --git a/script/OpenLong.s.sol b/script/OpenLong.s.sol index 2ce4471c1..b41f0179d 100644 --- a/script/OpenLong.s.sol +++ b/script/OpenLong.s.sol @@ -5,9 +5,6 @@ import "forge-std/Script.sol"; import "forge-std/console.sol"; import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { DsrHyperdrive } from "contracts/src/instances/DsrHyperdrive.sol"; -import { DsrHyperdriveDataProvider } from "contracts/src/instances/DsrHyperdriveDataProvider.sol"; -import { DsrManager } from "contracts/src/interfaces/IMaker.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; diff --git a/script/StethHyperdrive.s.sol b/script/StethHyperdrive.s.sol deleted file mode 100644 index 01de4c6fc..000000000 --- a/script/StethHyperdrive.s.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import "forge-std/Script.sol"; -import "forge-std/console.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { StethHyperdrive } from "contracts/src/instances/StethHyperdrive.sol"; -import { StethHyperdriveDataProvider } from "contracts/src/instances/StethHyperdriveDataProvider.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { ILido } from "contracts/src/interfaces/ILido.sol"; -import { IWETH } from "contracts/src/interfaces/IWETH.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; - -interface Faucet { - function mint(address token, address to, uint256 amount) external; -} - -contract StethHyperdriveScript is Script { - using FixedPointMath for uint256; - - ILido internal constant LIDO = - ILido(0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F); - IWETH internal constant WETH = - IWETH(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6); - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - // Deploy an instance of StethHyperdrive. - console.log("Deploying StethHyperdrive..."); - uint256 initialSharePrice = LIDO.getTotalPooledEther().divDown( - LIDO.getTotalShares() - ); - IHyperdrive.Fees memory fees = IHyperdrive.Fees({ - curve: 0.1e18, // 10% curve fee - flat: 0.05e18, // 5% flat fee - governance: 0.1e18 // 10% governance fee - }); - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: WETH, - initialSharePrice: initialSharePrice, - minimumShareReserves: 1e15, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), - governance: address(0), - feeCollector: address(0), - fees: fees, - oracleSize: 10, - updateGap: 1 hours - }); - StethHyperdriveDataProvider dataProvider = new StethHyperdriveDataProvider( - config, - bytes32(0), - address(0), - LIDO - ); - IHyperdrive hyperdrive = IHyperdrive( - address( - new StethHyperdrive( - config, - address(dataProvider), - bytes32(0), - address(0), - LIDO - ) - ) - ); - - // Initialize the Hyperdrive instance. - console.log("Initializing StethHyperdrive..."); - uint256 contribution = 1e18; - WETH.deposit{ value: contribution }(); - WETH.approve(address(hyperdrive), contribution); - hyperdrive.initialize(contribution, 0.05e18, msg.sender, true); - - // Ensure that the Hyperdrive instance was initialized properly. - console.log("Verifying deployment..."); - IHyperdrive.PoolConfig memory config_ = hyperdrive.getPoolConfig(); - require(config_.baseToken == WETH); - require(config_.initialSharePrice == initialSharePrice); - IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); - require( - info.shareReserves - contribution.divDown(initialSharePrice) <= 1e5 - ); - require(info.sharePrice == initialSharePrice); - console.log("StethHyperdrive was deployed successfully."); - - vm.stopBroadcast(); - - console.log("Deployed StethHyperdrive to: %s", address(hyperdrive)); - } -} diff --git a/test/integrations/AaveHyperdrive.t.sol b/test/integrations/AaveHyperdrive.t.sol deleted file mode 100644 index d27ced501..000000000 --- a/test/integrations/AaveHyperdrive.t.sol +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IPool } from "@aave/interfaces/IPool.sol"; -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { AaveHyperdriveDeployer } from "contracts/src/factory/AaveHyperdriveDeployer.sol"; -import { AaveHyperdriveFactory } from "contracts/src/factory/AaveHyperdriveFactory.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.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 "../utils/HyperdriveTest.sol"; -import { Mock4626, ERC20 } from "../mocks/Mock4626.sol"; -import { MockAaveHyperdrive } from "../mocks/MockAaveHyperdrive.sol"; -import { HyperdriveUtils } from "../utils/HyperdriveUtils.sol"; -import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; - -contract AaveHyperdriveTest is HyperdriveTest { - using FixedPointMath for *; - - AaveHyperdriveFactory factory; - IERC20 dai = IERC20(address(0x6B175474E89094C44Da98b954EedeAC495271d0F)); - IPool pool = IPool(address(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2)); - IERC20 aDAI = IERC20(address(0x018008bfb33d285247A21d44E50697654f754e63)); - uint256 aliceShares; - MockAaveHyperdrive mockHyperdrive; - - function setUp() public override __mainnet_fork(16_685_972) { - alice = createUser("alice"); - bob = createUser("bob"); - - vm.startPrank(deployer); - - AaveHyperdriveDeployer simpleDeployer = new AaveHyperdriveDeployer( - pool - ); - address[] memory defaults = new address[](1); - defaults[0] = bob; - forwarderFactory = new ForwarderFactory(); - factory = new AaveHyperdriveFactory( - alice, - simpleDeployer, - bob, - bob, - IHyperdrive.Fees(0, 0, 0), - defaults, - address(forwarderFactory), - forwarderFactory.ERC20LINK_HASH() - ); - - address daiWhale = 0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8; - whaleTransfer(daiWhale, dai, alice); - whaleTransfer(daiWhale, dai, bob); - - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: dai, - initialSharePrice: FixedPointMath.ONE_18, - minimumShareReserves: FixedPointMath.ONE_18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: FixedPointMath.ONE_18.divDown( - 22.186877016851916266e18 - ), - governance: alice, - feeCollector: bob, - fees: IHyperdrive.Fees(0, 0, 0), - oracleSize: 2, - updateGap: 0 - }); - - // Create a mock hyperdrive with functions available - mockHyperdrive = new MockAaveHyperdrive( - config, - address(0), - bytes32(0), - address(0), - aDAI, - pool - ); - - vm.stopPrank(); - vm.startPrank(alice); - dai.approve(address(hyperdrive), type(uint256).max); - dai.approve(address(mockHyperdrive), type(uint256).max); - dai.approve(address(pool), type(uint256).max); - pool.supply(address(dai), 100e18, alice, 0); - - vm.stopPrank(); - vm.startPrank(bob); - dai.approve(address(hyperdrive), type(uint256).max); - dai.approve(address(mockHyperdrive), type(uint256).max); - vm.stopPrank(); - - // Start recording events. - vm.recordLogs(); - } - - function test_aave_hyperdrive_deposit() external { - vm.startPrank(alice); - // Do a first deposit - (uint256 sharesMinted, uint256 sharePrice) = mockHyperdrive.deposit( - 10e18, - true - ); - assertEq(sharePrice, 1e18); - // 0.6 repeating - assertEq(sharesMinted, 10e18); - assertEq(aDAI.balanceOf(address(mockHyperdrive)), 10e18); - - // add interest - aDAI.transfer(address(mockHyperdrive), 5e18); - // Now we try a deposit - (sharesMinted, sharePrice) = mockHyperdrive.deposit(1e18, true); - assertEq(sharePrice, 1.5e18); - // 0.6 repeating - assertEq(sharesMinted, 666666666666666666); - assertEq(aDAI.balanceOf(address(mockHyperdrive)), 16e18); - - // Now we try to do a deposit from alice's aDAI - aDAI.approve(address(mockHyperdrive), type(uint256).max); - (sharesMinted, sharePrice) = mockHyperdrive.deposit(3e18, false); - assertEq(sharePrice, 1.5e18); - assertApproxEqAbs(sharesMinted, 2e18, 1); - assertApproxEqAbs(aDAI.balanceOf(address(mockHyperdrive)), 19e18, 2); - } - - function test_aave_hyperdrive_withdraw() external { - // First we add some shares and interest - vm.startPrank(alice); - //init pool - mockHyperdrive.deposit(10e18, true); - // add interest - aDAI.transfer(address(mockHyperdrive), 5e18); - - uint256 balanceBefore = dai.balanceOf(alice); - // test an underlying withdraw - uint256 amountWithdrawn = mockHyperdrive.withdraw(2e18, alice, true); - uint256 balanceAfter = dai.balanceOf(alice); - assertEq(balanceAfter, balanceBefore + 3e18); - assertEq(amountWithdrawn, 3e18); - - // Test a share withdraw - balanceBefore = aDAI.balanceOf(alice); - amountWithdrawn = mockHyperdrive.withdraw(2e18, alice, false); - assertEq(aDAI.balanceOf(alice), 3e18 + balanceBefore); - assertEq(amountWithdrawn, 3e18); - - // Check the zero withdraw revert - vm.expectRevert(IHyperdrive.NoAssetsToWithdraw.selector); - mockHyperdrive.withdraw(0, alice, false); - } - - function test_aave_hyperdrive_pricePerShare() external { - // First we add some shares and interest - vm.startPrank(alice); - // check it's zero before deposit - assertEq(0, mockHyperdrive.pricePerShare()); - // deposit the initial shares - mockHyperdrive.deposit(10e18, true); - // add interest - aDAI.transfer(address(mockHyperdrive), 2e18); - - uint256 price = mockHyperdrive.pricePerShare(); - assertEq(price, 1.2e18); - } - - function test_aave_hyperdrive_testDeploy() external { - vm.startPrank(alice); - uint256 apr = 0.01e18; // 1% apr - uint256 contribution = 2_500e18; - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: dai, - initialSharePrice: FixedPointMath.ONE_18, - minimumShareReserves: FixedPointMath.ONE_18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(apr), - governance: alice, - feeCollector: bob, - fees: IHyperdrive.Fees(0, 0, 0), - oracleSize: 2, - updateGap: 0 - }); - dai.approve(address(factory), type(uint256).max); - hyperdrive = factory.deployAndInitialize( - config, - new bytes32[](0), - contribution, - apr - ); - - // The initial price per share is one so the LP shares will initially - // be worth one base. Alice should receive LP shares equaling her - // contribution minus the shares that she set aside for the minimum - // share reserves and the zero address's initial LP contribution. - assertEq( - hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), - contribution - 2 * config.minimumShareReserves - ); - - // Verify that the correct events were emitted. - bytes32[] memory aDaiEncoding = new bytes32[](1); - aDaiEncoding[0] = bytes32(uint256(uint160(address(aDAI)))); - verifyFactoryEvents( - factory, - alice, - contribution, - apr, - config.minimumShareReserves, - aDaiEncoding, - 0 - ); - - // Test the revert condition for eth payment - vm.expectRevert(IHyperdrive.NotPayable.selector); - hyperdrive = factory.deployAndInitialize{ value: 100 }( - config, - new bytes32[](0), - contribution, - apr - ); - - config.baseToken = IERC20(address(0)); - vm.expectRevert(IHyperdrive.InvalidToken.selector); - hyperdrive = factory.deployAndInitialize( - config, - new bytes32[](0), - 2500e18, - //1% apr - 1e16 - ); - } -} diff --git a/test/integrations/AaveV3ERC4626.t.sol b/test/integrations/AaveV3ERC4626.t.sol new file mode 100644 index 000000000..809459f2f --- /dev/null +++ b/test/integrations/AaveV3ERC4626.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { ERC4626HyperdriveDeployer } from "contracts/src/factory/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveFactory } from "contracts/src/factory/ERC4626HyperdriveFactory.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.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 "../utils/HyperdriveTest.sol"; +import { MockERC4626Hyperdrive } from "../mocks/Mock4626Hyperdrive.sol"; +import { HyperdriveUtils } from "../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(); + + // Aave v3 Lending Pool Contract + IPool pool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); + AaveV3ERC4626Factory yieldDaddyFactory = new AaveV3ERC4626Factory( + pool, + address(0), + IRewardsController(address(0)) + ); + + // Dai is the underlying token used for Aave instances + ERC20 dai = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + + // Deploy a new instance of an Aave v3 ERC4626 token, for aDai + token = IERC4626(address(yieldDaddyFactory.createERC4626(dai))); + underlyingToken = IERC20(address(dai)); + + // Alice account must be prefunded with lots of the underlyingToken. + address daiWhale = 0x60FaAe176336dAb62e284Fe19B885B095d29fB7F; + whaleTransfer(daiWhale, IERC20(address(dai)), alice); + + _setUp(); + } + + function advanceTimeWithYield(uint256 timeDelta) public override { + // Aave derives interest based on time, so all we need + // to do is advance the block timestamp. + vm.warp(block.timestamp + timeDelta); + } +} diff --git a/test/integrations/DsrHyperdrive.t.sol b/test/integrations/DsrHyperdrive.t.sol deleted file mode 100644 index 2b607f2cb..000000000 --- a/test/integrations/DsrHyperdrive.t.sol +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; -import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; -import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { IMockDsrHyperdrive, MockDsrHyperdrive, MockDsrHyperdriveDataProvider, DsrManager } from "contracts/test/MockDsrHyperdrive.sol"; -import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; -import { BaseTest } from "test/utils/BaseTest.sol"; -import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; - -contract DsrHyperdriveTest is BaseTest { - using FixedPointMath for uint256; - - IMockDsrHyperdrive hyperdrive; - IERC20 dai; - IERC20 chai; - DsrManager dsrManager; - - function setUp() public override __mainnet_fork(16_685_972) { - super.setUp(); - - dai = IERC20(address(0x6B175474E89094C44Da98b954EedeAC495271d0F)); - dsrManager = DsrManager( - address(0x373238337Bfe1146fb49989fc222523f83081dDb) - ); - - vm.startPrank(deployer); - address dataProvider = address( - new MockDsrHyperdriveDataProvider(dsrManager) - ); - hyperdrive = IMockDsrHyperdrive( - address(new MockDsrHyperdrive(dataProvider, dsrManager)) - ); - - address daiWhale = 0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8; - - whaleTransfer(daiWhale, dai, alice); - - vm.stopPrank(); - vm.startPrank(alice); - dai.approve(address(hyperdrive), type(uint256).max); - - vm.stopPrank(); - vm.startPrank(bob); - dai.approve(address(hyperdrive), type(uint256).max); - } - - function test__base_token_is_dai() public { - assertEq( - address(hyperdrive.baseToken()), - address(dai), - "constructor call to dsrManager.dai() returned an invalid dai contract" - ); - } - - function test__dai_token_is_approved() public { - uint256 allowance = dai.allowance( - address(hyperdrive), - address(dsrManager) - ); - assertEq( - allowance, - type(uint256).max, - "dsrManager should be an approved DAI spender of hyperdrive" - ); - } - - function test__initial_base_token_deposit() public { - // as Alice - vm.stopPrank(); - vm.startPrank(alice); - - // Get balance of Alice prior to depositing - uint256 preBaseBalance = dai.balanceOf(alice); - - // Deposit amount of base - uint256 depositAmount = 2500e18; - (uint256 shares, uint256 sharePrice) = hyperdrive.deposit( - depositAmount, - true - ); - - // Validate that initial deposits are 1:1 - assertEq( - shares, - depositAmount, - "initial shares should be 1:1 with base" - ); - assertEq(sharePrice, 1e18, "initial share price should be 1"); - - // Validate that tokens have been transferred - assertEq( - preBaseBalance - dai.balanceOf(alice), - depositAmount, - "hyperdrive should have transferred tokens" - ); - } - - function test__multiple_deposits() public { - // as Alice - vm.stopPrank(); - vm.startPrank(alice); - - // Deposit arbitrary amount in pool - (uint256 sharesAlice, ) = hyperdrive.deposit(4545e18, true); - - // As, Alice transfer Dai to Bob and fast-forward an arbitrary amount of - // time accruing arbitrary interest for Alice - dai.transfer(bob, 1000e18); - vm.warp(block.timestamp + 1212 days + 54); - - // as Bob - vm.stopPrank(); - vm.startPrank(bob); - - // Deposit amount of base and fast-forward a year accruing 1% interest - // for all pooled deposits - (uint256 sharesBob, ) = hyperdrive.deposit(1000e18, true); - vm.warp(block.timestamp + 365 days); - - // Get total and per-user amounts of underlying invested - uint256 underlyingInvested = dsrManager.daiBalance(address(hyperdrive)); - uint256 pricePerShare = hyperdrive.pricePerShare(); - uint256 underlyingForBob = sharesBob.mulDown(pricePerShare); - uint256 underlyingForAlice = sharesAlice.mulDown(pricePerShare); - - assertApproxEqAbs( - underlyingForBob, - 1010e18, - 10000, - "Bob should have accrued approximately 1% interest" - ); - assertApproxEqAbs( - underlyingForAlice, - underlyingInvested - underlyingForBob, - 10000, - "Alice's shares should reflect all remaining deposits" - ); - } - - function test__multiple_withdrawals() public { - // as Alice - vm.stopPrank(); - vm.startPrank(alice); - - // Deposit arbitrary amount in pool - (uint256 sharesAlice, ) = hyperdrive.deposit(4545.1115e18, true); - - // As, Alice transfer Dai to Bob and fast-forward an arbitrary amount of - // time accruing arbitrary interest for Alice - dai.transfer(bob, 1000e18); - vm.warp(block.timestamp + 1212 days + 54); - - // as Bob - vm.stopPrank(); - vm.startPrank(bob); - - // Deposit amount of base and fast-forward a year accruing 1% interest - // for all pooled deposits - (uint256 sharesBob, ) = hyperdrive.deposit(1000e18, true); - vm.warp(block.timestamp + 365 days); - - // Get total and per-user amounts of underlying invested - uint256 underlyingInvested = dsrManager.daiBalance(address(hyperdrive)); - - // Bob should have accrued 1% - uint256 amountWithdrawnBob = hyperdrive.withdraw(sharesBob, bob, true); - assertApproxEqAbs( - amountWithdrawnBob - 1000e18, - 10e18, - 10000, - "Bob should have accrued approximately 1% interest" - ); - - // Alice shares should make up the rest of the pool - uint256 amountWithdrawnAlice = hyperdrive.withdraw( - sharesAlice, - alice, - true - ); - assertApproxEqAbs( - amountWithdrawnAlice, - underlyingInvested - amountWithdrawnBob, - 10000, - "Alice's shares should reflect all remaining deposits" - ); - - assertEq(hyperdrive.totalShares(), 0, "all shares should be exited"); - } - - function test__pricePerShare() public { - // as Alice - vm.stopPrank(); - vm.startPrank(alice); - - // Initialize - hyperdrive.deposit(1000e18, true); - - for (uint256 i = 1; i <= 365; i++) { - vm.warp(block.timestamp + (0.1 days) * i); - - uint256 pricePerShare = hyperdrive.pricePerShare(); - - (, uint256 sharePriceOnDeposit) = hyperdrive.deposit( - 100e18 * i, - true - ); - - assertApproxEqAbs( - pricePerShare, - sharePriceOnDeposit, - 5000, - "emulated share price should match pool ratio after deposit" - ); - } - } - - function test__unsupported_deposit() public { - vm.expectRevert(IHyperdrive.UnsupportedToken.selector); - hyperdrive.deposit(1, false); - } - - function test__unsupported_withdraw() public { - vm.expectRevert(IHyperdrive.UnsupportedToken.selector); - hyperdrive.withdraw(1, alice, false); - } - - // Ensures issue described in https://github.com/delvtech/hyperdrive/issues/357 is patched - function test_avoids_donation_attack() public { - vm.stopPrank(); - vm.startPrank(alice); - uint256 apr = 0.05e18; - - // The pool gets initialized with an amount that is slightly higher - // than the minimum possible contribution. - uint256 contribution = 3 * - hyperdrive.getPoolConfig().minimumShareReserves; - hyperdrive.initialize(contribution, apr, bob, true); - - // Ensure that Bob's contribution was added to the pool correctly. - assertEq(hyperdrive.totalShares(), contribution); - assertApproxEqAbs( - dsrManager.daiBalance(address(hyperdrive)), - contribution, - 1 - ); - - vm.stopPrank(); - vm.startPrank(bob); - // Bob attempts to rug the pool by removing all liquidity except a small amount of shares - hyperdrive.removeLiquidity( - (contribution - - 2 * - hyperdrive.getPoolConfig().minimumShareReserves) - 10, - 0, - bob, - true - ); - vm.stopPrank(); - vm.startPrank(alice); - - uint256 donation = 2000.01e18; - dai.transfer(bob, donation); - - vm.stopPrank(); - vm.startPrank(bob); - - // Bob front-runs Alice with a call to dsrManager.join() with 2000.01 DAI - dai.approve(address(dsrManager), donation); - dsrManager.join(address(hyperdrive), donation); - - // The minimum share reserves, the zero address's LP capital, and some - // dust remaining from Bob's withdrawal should be left in the pool. - // This is all that is accounted for in the total shares, but the base - // balance also includes the donation. - assertEq( - dsrManager.daiBalance(address(hyperdrive)), - donation + 2 * hyperdrive.getPoolConfig().minimumShareReserves + 9 - ); - assertEq( - hyperdrive.totalShares(), - 2 * hyperdrive.getPoolConfig().minimumShareReserves + 10 - ); - - uint256 shareReserves = hyperdrive.getPoolInfo().shareReserves; - uint256 bondReserves = hyperdrive.getPoolInfo().bondReserves; - assert(shareReserves != 0); - assert(bondReserves != 0); - uint256 initialSharePrice = hyperdrive - .getPoolConfig() - .initialSharePrice; - uint256 positionDuration = hyperdrive.getPoolConfig().positionDuration; - uint256 timeStretch = hyperdrive.getPoolConfig().timeStretch; - - apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - initialSharePrice, - positionDuration, - timeStretch - ); - - vm.stopPrank(); - vm.startPrank(alice); - - // Alice calls addLiquidity() with 1000 DAI. She should receive a - // substantial amount of shares (which helps to avoid numerical issues) - // that are close in value to her contribution. - uint256 aliceContribution = 1_000e18; - uint256 newShares = hyperdrive.addLiquidity( - aliceContribution, - apr, - apr, - alice, - true - ); - assertGt(newShares, 1e16); - assertApproxEqAbs( - newShares.mulDown(HyperdriveUtils.lpSharePrice(hyperdrive)), - aliceContribution, - 1e6 - ); - } - - // Tests for https://github.com/delvtech/hyperdrive/issues/356 - function testMinimalDeploymentReceivesLiquidity() public { - vm.stopPrank(); - vm.startPrank(alice); - uint256 apr = 0.05e18; - - // The pool gets initialized with a minimal contribution - uint256 contribution = 2 * - hyperdrive.getPoolConfig().minimumShareReserves; - hyperdrive.initialize(contribution, apr, bob, true); - - uint256 shareReserves = hyperdrive.getPoolInfo().shareReserves; - uint256 bondReserves = hyperdrive.getPoolInfo().bondReserves; - assert(shareReserves != 0); - assert(bondReserves != 0); - uint256 initialSharePrice = hyperdrive - .getPoolConfig() - .initialSharePrice; - uint256 positionDuration = hyperdrive.getPoolConfig().positionDuration; - uint256 timeStretch = hyperdrive.getPoolConfig().timeStretch; - - apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - initialSharePrice, - positionDuration, - timeStretch - ); - - // Alice calls addLiquidity() with 1000 DAI - // This would have reverted if minimum contribution was small enough to - // cause division by zero - hyperdrive.addLiquidity(1000e18, apr, apr, alice, true); - } - - function testCannotInitializeBelowMinimumContribution() public { - vm.stopPrank(); - vm.startPrank(alice); - uint256 apr = 0.05e18; - - // The pool gets initialized with a minimal contribution - uint256 contribution = hyperdrive.getPoolConfig().minimumShareReserves - - 1; - vm.expectRevert(IHyperdrive.BelowMinimumContribution.selector); - hyperdrive.initialize(contribution, apr, bob, true); - } -} diff --git a/test/integrations/ERC4626Hyperdrive.t.sol b/test/integrations/ERC4626Hyperdrive.t.sol index 1154a7d47..82f66556f 100644 --- a/test/integrations/ERC4626Hyperdrive.t.sol +++ b/test/integrations/ERC4626Hyperdrive.t.sol @@ -108,7 +108,7 @@ contract ER4626HyperdriveTest is HyperdriveTest { 1e18, true ); - assertEq(sharePrice, 1.5e18 + 1); + assertEq(sharePrice, 1.5e18); // 0.6 repeating assertEq(sharesMinted, 666666666666666666); assertEq(pool.balanceOf(address(mockHyperdrive)), 666666666666666666); @@ -116,7 +116,7 @@ contract ER4626HyperdriveTest is HyperdriveTest { // Now we try to do a deposit from alice's shares pool.approve(address(mockHyperdrive), type(uint256).max); (sharesMinted, sharePrice) = mockHyperdrive.deposit(3e18, false); - assertEq(sharePrice, 1.5e18 + 1); + assertEq(sharePrice, 1.5e18); assertApproxEqAbs(sharesMinted, 2e18, 1); assertApproxEqAbs( pool.balanceOf(address(mockHyperdrive)), diff --git a/test/integrations/ERC4626Validation.t.sol b/test/integrations/ERC4626Validation.t.sol new file mode 100644 index 000000000..a11aca554 --- /dev/null +++ b/test/integrations/ERC4626Validation.t.sol @@ -0,0 +1,746 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { ERC4626HyperdriveDeployer } from "contracts/src/factory/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveFactory } from "contracts/src/factory/ERC4626HyperdriveFactory.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.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 "../utils/HyperdriveTest.sol"; +import { MockERC4626Hyperdrive } from "../mocks/Mock4626Hyperdrive.sol"; +import { HyperdriveUtils } from "../utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; + +abstract contract ERC4626ValidationTest is HyperdriveTest { + using FixedPointMath for *; + using Lib for *; + + ERC4626HyperdriveFactory internal factory; + IERC20 internal underlyingToken; + IERC4626 internal token; + MockERC4626Hyperdrive hyperdriveInstance; + + uint256 internal constant FIXED_RATE = 0.05e18; + + function _setUp() internal { + super.setUp(); + + vm.startPrank(deployer); + + // Initialize deployer contracts and forwarder + ERC4626HyperdriveDeployer simpleDeployer = new ERC4626HyperdriveDeployer( + token + ); + address[] memory defaults = new address[](1); + defaults[0] = bob; + forwarderFactory = new ForwarderFactory(); + + // Hyperdrive factory to produce ERC4626 instances for stethERC4626 + factory = new ERC4626HyperdriveFactory( + alice, + simpleDeployer, + bob, + bob, + IHyperdrive.Fees(0, 0, 0), + defaults, + address(forwarderFactory), + forwarderFactory.ERC20LINK_HASH(), + token + ); + + // Config changes required to support ERC4626 with the correct initial Share Price + IHyperdrive.PoolConfig memory config = testConfig(FIXED_RATE); + config.baseToken = underlyingToken; + config.initialSharePrice = token.convertToAssets(FixedPointMath.ONE_18); + uint256 contribution = 7_500e18; + vm.stopPrank(); + vm.startPrank(alice); + + // Set approval to allow initial contribution to factory + underlyingToken.approve(address(factory), type(uint256).max); + + // Deploy and set hyperdrive instance + hyperdrive = factory.deployAndInitialize( + config, + new bytes32[](0), + contribution, + FIXED_RATE + ); + + // Setup maximum approvals so transfers don't require further approval + underlyingToken.approve(address(hyperdrive), type(uint256).max); + underlyingToken.approve(address(token), type(uint256).max); + token.approve(address(hyperdrive), type(uint256).max); + vm.stopPrank(); + + // Start recording events. + vm.recordLogs(); + } + + function advanceTimeWithYield(uint256 timeDelta) public virtual; + + function test_deployAndInitialize() external { + vm.startPrank(alice); + + IHyperdrive.PoolConfig memory config = testConfig(FIXED_RATE); + // Required to support ERC4626, since the test config initialSharePrice is wrong + config.baseToken = underlyingToken; + // Designed to ensure compatibility ../../contracts/src/instances/ERC4626Hyperdrive.sol#L122C1-L122C1 + config.initialSharePrice = token.convertToAssets(FixedPointMath.ONE_18); + + uint256 contribution = 10_000e18; + + underlyingToken.approve(address(factory), type(uint256).max); + + // Deploy a new hyperdrive instance + hyperdrive = factory.deployAndInitialize( + config, + new bytes32[](0), + contribution, + FIXED_RATE + ); + + // Ensure minimumShareReserves were added, and lpTotalSupply increased + assertEq( + hyperdrive.getPoolInfo().lpTotalSupply, + hyperdrive.getPoolInfo().shareReserves - config.minimumShareReserves + ); + + // Verify that the correct events were emitted during creation + verifyFactoryEvents( + factory, + alice, + contribution, + FIXED_RATE, + config.minimumShareReserves, + new bytes32[](0), + 1e5 + ); + } + + function test_OpenLongWithUnderlying(uint256 basePaid) external { + // Establish baseline variables + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + vm.startPrank(alice); + + basePaid = basePaid.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxLong(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + // Open a long with underlying tokens + openLongERC4626(alice, basePaid, true); + + // Verify balances correctly updated + verifyDepositUnderlying( + alice, + basePaid, + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_OpenLongWithShares(uint256 basePaid) external { + vm.startPrank(alice); + basePaid = basePaid.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxLong(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + underlyingToken.approve(address(token), type(uint256).max); + // Deposit into the ERC4626 so underlying doesn't need to be used + token.deposit(basePaid, alice); + + // Establish baseline, important underlying balance must be taken AFTER + // deposit into ERC4626 token + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Open the long + openLongERC4626(alice, basePaid, false); + + // Ensure balances correctly updated + verifyDepositShares( + alice, + basePaid, + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_CloseLongWithUnderlying(uint256 basePaid) external { + vm.startPrank(alice); + // Alice opens a long. + basePaid = basePaid.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxLong(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + // Open a long + (uint256 maturityTime, uint256 longAmount) = openLongERC4626( + alice, + basePaid, + true + ); + + // Establish a baseline of balances before closing long + uint256 totalPooledAssetsBefore = token.totalAssets(); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Close long with underlying assets + uint256 baseProceeds = hyperdrive.closeLong( + maturityTime, + longAmount, + 0, + alice, + true + ); + + // Ensure that the ERC4626 aggregates and the token balances were updated + // correctly during the trade. + verifyWithdrawalShares( + alice, + baseProceeds, + totalPooledAssetsBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_CloseLongWithShares(uint256 basePaid) external { + vm.startPrank(alice); + // Alice opens a long. + basePaid = basePaid.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxLong(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + // Open a long + (uint256 maturityTime, uint256 longAmount) = openLongERC4626( + alice, + basePaid, + true + ); + + // Establish a baseline of balances before closing long + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Close the long + uint256 baseProceeds = hyperdrive.closeLong( + maturityTime, + longAmount, + 0, + alice, + false + ); + + // Ensure balances updated correctly + verifyWithdrawalToken( + alice, + token.convertToShares(baseProceeds), + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_OpenShortWithUnderlying() external { + vm.startPrank(alice); + uint256 shortAmount = 0.001e18; + shortAmount = shortAmount.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxShort(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + // Take a baseline + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Open a short + (, uint256 basePaid) = openShortERC4626(alice, shortAmount, true); + + // Ensure that the amount of base paid by the short is reasonable. + uint256 realizedRate = HyperdriveUtils.calculateAPRFromRealizedPrice( + shortAmount - basePaid, + shortAmount, + 1e18 + ); + + // Ensure some base was paid + assertGt(basePaid, 0); + assertGe(realizedRate, FIXED_RATE); + + // Ensure the balances updated correctly + verifyDepositUnderlying( + alice, + basePaid, + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_OpenShortWithShares(uint256 shortAmount) external { + vm.startPrank(alice); + shortAmount = shortAmount.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxShort(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + underlyingToken.approve(address(token), type(uint256).max); + // Deposit into the actual ERC4626 token + token.deposit(shortAmount, alice); + + // Establish a baseline before we open the short + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Open the short + (, uint256 basePaid) = openShortERC4626(alice, shortAmount, false); + + // Ensure we did actually paid a non-Zero amount of base + assertGt(basePaid, 0); + uint256 realizedRate = HyperdriveUtils.calculateAPRFromRealizedPrice( + shortAmount - basePaid, + shortAmount, + 1e18 + ); + assertGe(realizedRate, FIXED_RATE); + + // Ensure balances were correctly updated + verifyDepositShares( + alice, + basePaid, + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_CloseShortWithUnderlying( + uint256 shortAmount, + int256 variableRate + ) external { + vm.startPrank(alice); + shortAmount = shortAmount.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxShort(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + (uint256 maturityTime, ) = openShortERC4626(alice, shortAmount, true); + // The term passes and interest accrues. + variableRate = variableRate.normalizeToRange(0, 2.5e18); + + // Accumulate yield and let the short mature + advanceTimeWithYield(POSITION_DURATION); + + // Establish a baseline before closing the short + uint256 totalPooledAssetsBefore = token.totalAssets(); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Close the short + uint256 baseProceeds = hyperdrive.closeShort( + maturityTime, + shortAmount, + 0, + alice, + true + ); + + // Ensure that the ERC4626 aggregates and the token balances were updated + // correctly during the trade. + verifyWithdrawalShares( + alice, + baseProceeds, + totalPooledAssetsBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function test_CloseShortWithShares( + uint256 shortAmount, + int256 variableRate + ) external { + vm.startPrank(alice); + shortAmount = shortAmount.normalizeToRange( + 0.00001e18, + Lib.min( + HyperdriveUtils.calculateMaxShort(hyperdrive), + underlyingToken.balanceOf(alice) + ) + ); + + // Deposit into the actual ERC4626 + underlyingToken.approve(address(token), type(uint256).max); + token.deposit(shortAmount, alice); + + // Open the short + (uint256 maturityTime, ) = openShortERC4626(alice, shortAmount, true); + + // The term passes and interest accrues. + variableRate = variableRate.normalizeToRange(0, 2.5e18); + + // Advance time and accumulate the yield + advanceTimeWithYield(POSITION_DURATION); + + // Establish a baseline before closing the short + uint256 totalPooledAssetsBefore = token.totalAssets(); + uint256 totalSharesBefore = token.convertToShares( + totalPooledAssetsBefore + ); + AccountBalances memory aliceBalancesBefore = getAccountBalances(alice); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // Close the short + uint256 baseProceeds = hyperdrive.closeShort( + maturityTime, + shortAmount, + 0, + alice, + false + ); + + // Ensure that the ERC4626 aggregates and the token balances were updated + // correctly during the trade. + verifyWithdrawalToken( + alice, + token.convertToShares(baseProceeds), + totalPooledAssetsBefore, + totalSharesBefore, + aliceBalancesBefore, + hyperdriveBalancesBefore + ); + } + + function openLongERC4626( + address trader, + uint256 baseAmount, + bool asUnderlying + ) internal returns (uint256 maturityTime, uint256 bondAmount) { + vm.stopPrank(); + vm.startPrank(trader); + + // Open the long. + if (asUnderlying) { + underlyingToken.approve(address(hyperdrive), baseAmount); + (maturityTime, bondAmount) = hyperdrive.openLong( + baseAmount, + 0, + trader, + asUnderlying + ); + } else { + token.approve(address(hyperdrive), baseAmount); + (maturityTime, bondAmount) = hyperdrive.openLong( + baseAmount, + 0, + trader, + asUnderlying + ); + } + + return (maturityTime, bondAmount); + } + + function openShortERC4626( + address trader, + uint256 bondAmount, + bool asUnderlying + ) internal returns (uint256 maturityTime, uint256 baseAmount) { + vm.stopPrank(); + vm.startPrank(trader); + // Open the short + if (asUnderlying) { + underlyingToken.approve(address(hyperdrive), bondAmount); + (maturityTime, baseAmount) = hyperdrive.openShort( + bondAmount, + type(uint256).max, + trader, + asUnderlying + ); + } else { + token.approve(address(hyperdrive), bondAmount); + (maturityTime, baseAmount) = hyperdrive.openShort( + bondAmount, + type(uint256).max, + trader, + asUnderlying + ); + } + return (maturityTime, baseAmount); + } + + function verifyDepositUnderlying( + address trader, + uint256 basePaid, + uint256 totalPooledAssetsBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + // Ensure that the amount of assets increased by the base paid. + assertApproxEqAbs( + token.totalAssets(), + totalPooledAssetsBefore + basePaid, + 2 + ); + + // Ensure that the underlyingToken balances were updated correctly. + assertApproxEqAbs( + underlyingToken.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.underlyingBalance, + 2 + ); + assertApproxEqAbs( + underlyingToken.balanceOf(trader), + traderBalancesBefore.underlyingBalance - basePaid, + 2 + ); + + // Ensure that the token balances were updated correctly. + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance + + token.convertToShares(basePaid), + 1 + ); + assertApproxEqAbs( + token.balanceOf(trader), + traderBalancesBefore.shareBalance, + 2 + ); + // Ensure that the token shares were updated correctly. + uint256 expectedShares = basePaid.mulDivDown( + totalSharesBefore, + totalPooledAssetsBefore + ); + assertApproxEqAbs( + token.convertToShares(token.totalAssets()), + totalSharesBefore + expectedShares, + 2 + ); + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance + expectedShares, + 2 + ); + assertApproxEqAbs( + token.balanceOf(trader), + traderBalancesBefore.shareBalance, + 2 + ); + } + + function verifyDepositShares( + address trader, + uint256 basePaid, + uint256 totalPooledAssetsBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + // Ensure that the totalAssets of the token stays the same. + assertEq(token.totalAssets(), totalPooledAssetsBefore); + + // Ensure that the underlying token balances were updated correctly. + assertEq( + underlyingToken.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.underlyingBalance + ); + assertEq( + underlyingToken.balanceOf(trader), + traderBalancesBefore.underlyingBalance + ); + + // Ensure that the token balances were updated correctly. + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance + + token.convertToShares(basePaid), + 1 + ); + assertApproxEqAbs( + token.balanceOf(trader), + traderBalancesBefore.shareBalance - token.convertToShares(basePaid), + 1 + ); + + // Ensure that the token shares were updated correctly. + uint256 expectedShares = basePaid.mulDivDown( + totalSharesBefore, + totalPooledAssetsBefore + ); + assertApproxEqAbs( + token.convertToShares(token.totalAssets()), + totalSharesBefore, + 2 + ); + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance + expectedShares, + 1 + ); + assertApproxEqAbs( + token.balanceOf(trader), + traderBalancesBefore.shareBalance - expectedShares, + 1 + ); + } + + function verifyWithdrawalToken( + address trader, + uint256 shareProceeds, + uint256 totalPooledAssetsBefore, + uint256 totalSharesBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + // Ensure that the total pooled assets and shares stays the same. + assertEq(token.totalAssets(), totalPooledAssetsBefore); + assertApproxEqAbs( + token.convertToShares(token.totalAssets()), + totalSharesBefore, + 1 + ); + + // Ensure that the underlying balances were updated correctly. + assertEq( + underlyingToken.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.underlyingBalance + ); + assertEq( + underlyingToken.balanceOf(trader), + traderBalancesBefore.underlyingBalance + ); + + // Ensure that the token balances were updated correctly. + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance - shareProceeds, + 1 + ); + assertApproxEqAbs( + token.balanceOf(trader), + traderBalancesBefore.shareBalance + shareProceeds, + 1 + ); + } + + function verifyWithdrawalShares( + address trader, + uint256 baseProceeds, + uint256 totalPooledAssetsBefore, + AccountBalances memory traderBalancesBefore, + AccountBalances memory hyperdriveBalancesBefore + ) internal { + // Allowances are set to 3 due to the expected number of conversions which can occur. + // Ensure that the total pooled assets decreased by the amount paid out + assertApproxEqAbs( + token.totalAssets() + baseProceeds, + totalPooledAssetsBefore, + 3 + ); + + // Ensure that the underlying balances were updated correctly. + // Token should be converted to underlyingToken and set to the trader + assertApproxEqAbs( + underlyingToken.balanceOf(trader), + traderBalancesBefore.underlyingBalance + baseProceeds, + 3 + ); + assertApproxEqAbs( + token.balanceOf(address(hyperdrive)), + hyperdriveBalancesBefore.shareBalance - + token.convertToShares(baseProceeds), + 3 + ); + } + + struct AccountBalances { + uint256 shareBalance; + uint256 underlyingBalance; + } + + function getAccountBalances( + address account + ) internal view returns (AccountBalances memory) { + return + AccountBalances({ + shareBalance: token.balanceOf(account), + underlyingBalance: underlyingToken.balanceOf(account) + }); + } +} diff --git a/test/integrations/HyperdriveDSRDeploy.t.sol b/test/integrations/HyperdriveDSRDeploy.t.sol deleted file mode 100644 index a8a79796e..000000000 --- a/test/integrations/HyperdriveDSRDeploy.t.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { DsrHyperdriveDeployer } from "contracts/src/factory/DsrHyperdriveDeployer.sol"; -import { DsrHyperdriveFactory } from "contracts/src/factory/DsrHyperdriveFactory.sol"; -import { DsrHyperdriveDataProvider } from "contracts/src/instances/DsrHyperdriveDataProvider.sol"; -import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; -import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.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 { DsrManager } from "contracts/test/MockDsrHyperdrive.sol"; -import { HyperdriveTest } from "../utils/HyperdriveTest.sol"; -import { HyperdriveUtils } from "../utils/HyperdriveUtils.sol"; - -contract HyperdriveDsrTest is HyperdriveTest { - using FixedPointMath for *; - - DsrHyperdriveFactory factory; - IERC20 dai = IERC20(address(0x6B175474E89094C44Da98b954EedeAC495271d0F)); - DsrManager manager = - DsrManager(address(0x373238337Bfe1146fb49989fc222523f83081dDb)); - - function setUp() public override __mainnet_fork(16_685_972) { - super.setUp(); - - vm.startPrank(deployer); - - // Deploy the DsrHyperdrive deployer and factory. - DsrHyperdriveDeployer simpleDeployer = new DsrHyperdriveDeployer( - manager - ); - address[] memory defaults = new address[](1); - defaults[0] = bob; - forwarderFactory = new ForwarderFactory(); - factory = new DsrHyperdriveFactory( - alice, - simpleDeployer, - bob, - bob, - IHyperdrive.Fees(0, 0, 0), - defaults, - address(forwarderFactory), - forwarderFactory.ERC20LINK_HASH(), - address(manager) - ); - - // Set up DAI balances for Alice. - address daiWhale = 0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8; - whaleTransfer(daiWhale, dai, alice); - - // Start recording event logs. - vm.recordLogs(); - } - - function test_hyperdrive_dsr_deploy_and_init() external { - setUp(); - // We've just copied the values used by the original tests to ensure this runs - - vm.startPrank(alice); - bytes32[] memory empty = new bytes32[](0); - dai.approve(address(factory), type(uint256).max); - uint256 apr = 0.01e18; // 1% apr - uint256 contribution = 2_500e18; - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: dai, - initialSharePrice: FixedPointMath.ONE_18, - minimumShareReserves: 10e18, - positionDuration: 365 days, - checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(apr), - governance: address(0), - feeCollector: address(0), - fees: IHyperdrive.Fees(0, 0, 0), - oracleSize: 2, - updateGap: 0 - }); - hyperdrive = factory.deployAndInitialize( - config, - empty, - contribution, - apr - ); - - // The initial price per share is one so the LP shares will initially - // be worth one base. Alice should receive LP shares equaling her - // contribution minus the shares that she set aside for the minimum - // share reserves and the zero address's initial LP contribution. - assertEq( - hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), - contribution - 2 * config.minimumShareReserves - ); - - // Verify that the correct events were emitted. - verifyFactoryEvents( - factory, - alice, - contribution, - apr, - config.minimumShareReserves, - new bytes32[](0), - 0 - ); - } -} diff --git a/test/integrations/StethERC4626.t.sol b/test/integrations/StethERC4626.t.sol new file mode 100644 index 000000000..d9fe051a4 --- /dev/null +++ b/test/integrations/StethERC4626.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { ERC4626HyperdriveDeployer } from "contracts/src/factory/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveFactory } from "contracts/src/factory/ERC4626HyperdriveFactory.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.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 "../utils/HyperdriveTest.sol"; +import { MockERC4626Hyperdrive } from "../mocks/Mock4626Hyperdrive.sol"; +import { HyperdriveUtils } from "../utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; +import { ILido } from "contracts/src/interfaces/ILido.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) 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); + + uint256 variableRate = 1.5e18; + + // 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/integrations/StethHyperdrive.t.sol b/test/integrations/StethHyperdrive.t.sol deleted file mode 100644 index ff6c8c922..000000000 --- a/test/integrations/StethHyperdrive.t.sol +++ /dev/null @@ -1,788 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { stdStorage, StdStorage } from "forge-std/Test.sol"; -import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; -import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; -import { StethHyperdriveDeployer } from "contracts/src/factory/StethHyperdriveDeployer.sol"; -import { StethHyperdriveFactory } from "contracts/src/factory/StethHyperdriveFactory.sol"; -import { StethHyperdrive } from "contracts/src/instances/StethHyperdrive.sol"; -import { StethHyperdriveDataProvider } from "contracts/src/instances/StethHyperdriveDataProvider.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 { 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 stdStorage for StdStorage; - using Lib for *; - - 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; - - StethHyperdriveFactory factory; - - function setUp() public override __mainnet_fork(17_376_154) { - super.setUp(); - - // Deploy the StethHyperdrive deployer and factory. - vm.startPrank(deployer); - StethHyperdriveDeployer simpleDeployer = new StethHyperdriveDeployer( - LIDO - ); - address[] memory defaults = new address[](1); - defaults[0] = bob; - forwarderFactory = new ForwarderFactory(); - factory = new StethHyperdriveFactory( - alice, - simpleDeployer, - bob, - bob, - IHyperdrive.Fees(0, 0, 0), - defaults, - address(forwarderFactory), - forwarderFactory.ERC20LINK_HASH(), - LIDO - ); - - // Alice deploys the hyperdrive instance. - vm.stopPrank(); - vm.startPrank(alice); - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: IERC20(ETH), - initialSharePrice: LIDO.getTotalPooledEther().divDown( - LIDO.getTotalShares() - ), - minimumShareReserves: 1e15, - positionDuration: POSITION_DURATION, - checkpointDuration: CHECKPOINT_DURATION, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), - governance: governance, - feeCollector: feeCollector, - fees: IHyperdrive.Fees({ curve: 0, flat: 0, governance: 0 }), - oracleSize: ORACLE_SIZE, - updateGap: UPDATE_GAP - }); - uint256 contribution = 10_000e18; - hyperdrive = factory.deployAndInitialize{ value: contribution }( - config, - new bytes32[](0), - contribution, - FIXED_RATE - ); - - // 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(config.initialSharePrice) - - 2 * - config.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 { - vm.stopPrank(); - vm.startPrank(bob); - IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ - baseToken: IERC20(ETH), - initialSharePrice: LIDO.getTotalPooledEther().divDown( - LIDO.getTotalShares() - ), - minimumShareReserves: 1e15, - positionDuration: POSITION_DURATION, - checkpointDuration: CHECKPOINT_DURATION, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), - governance: governance, - feeCollector: feeCollector, - fees: IHyperdrive.Fees({ curve: 0, flat: 0, governance: 0 }), - oracleSize: ORACLE_SIZE, - updateGap: UPDATE_GAP - }); - uint256 contribution = 10_000e18; - hyperdrive = factory.deployAndInitialize{ value: contribution }( - config, - new bytes32[](0), - contribution, - FIXED_RATE - ); - - // 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(config.initialSharePrice) - - 2 * - config.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, - bob, - contribution, - FIXED_RATE, - config.minimumShareReserves, - new bytes32[](0), - 1e5 // NOTE: We need some tolerance since stETH uses mulDivDown for share calculations. - ); - } - - /// 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( - 0.00001e18, - 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( - 0.00001e18, - 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_failures() external { - // Too little eth - vm.expectRevert(IHyperdrive.TransferFailed.selector); - hyperdrive.openLong{ value: 1e18 - 1 }(1e18, 0, bob, true); - // Paying eth to the steth flow - vm.expectRevert(IHyperdrive.NotPayable.selector); - hyperdrive.openLong{ value: 1 }(1e18, 0, bob, false); - } - - 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( - 0.00001e18, - HyperdriveUtils.calculateMaxLong(hyperdrive) - ); - openLong(bob, basePaid, 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( - 0.00001e18, - 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, bob, true); - } - - function test_close_long_with_steth(uint256 basePaid) external { - // Bob opens a long. - basePaid = basePaid.normalizeToRange( - 0.00001e18, - 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 baseProceeds = closeLong(bob, maturityTime, longAmount, false); - - // 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() 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) - ); - uint256 shortAmount = 0.001e18; - // Bob opens a short by depositing ETH. - shortAmount = shortAmount.normalizeToRange( - 0.001e18, - 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( - 0.001e18, - HyperdriveUtils.calculateMaxShort(hyperdrive) - ); - (, uint256 basePaid) = openShort(bob, shortAmount, false); - - // 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_close_short_with_ETH( - uint256 shortAmount, - int256 variableRate - ) external { - // Bob opens a short. - shortAmount = shortAmount.normalizeToRange( - 0.001e18, - 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, bob, true); - } - - function test_close_short_with_steth( - uint256 shortAmount, - int256 variableRate - ) external { - // Bob opens a short. - shortAmount = shortAmount.normalizeToRange( - 0.001e18, - 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 baseProceeds = closeShort( - bob, - maturityTime, - shortAmount, - false - ); - 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 baseProceeds = closeLong(bob, maturityTime, longAmount, false); - - // 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 baseProceeds = closeLong( - bob, - maturityTime, - longAmount / 2, - false - ); - - // 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 asUnderlying, - uint256 totalPooledEtherBefore, - uint256 totalSharesBefore, - AccountBalances memory traderBalancesBefore, - AccountBalances memory hyperdriveBalancesBefore - ) internal { - if (asUnderlying) { - // 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); - assertEq( - LIDO.sharesOf(address(hyperdrive)), - hyperdriveBalancesBefore.stethShares + expectedShares - ); - assertEq( - LIDO.sharesOf(trader), - traderBalancesBefore.stethShares - expectedShares - ); - } - } - - 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/integrations/sDai.t.sol b/test/integrations/sDai.t.sol new file mode 100644 index 000000000..be5cbe9a9 --- /dev/null +++ b/test/integrations/sDai.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { ERC4626HyperdriveDeployer } from "contracts/src/factory/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveFactory } from "contracts/src/factory/ERC4626HyperdriveFactory.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.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 "../utils/HyperdriveTest.sol"; +import { MockERC4626Hyperdrive } from "../mocks/Mock4626Hyperdrive.sol"; +import { HyperdriveUtils } from "../utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; +import { ERC4626ValidationTest } from "./ERC4626Validation.t.sol"; + +// Interface for the `Pot` of the underlying DSR +interface PotLike { + function rho() external view returns (uint256); + + function dsr() external view returns (uint256); + + function drip() external returns (uint256); +} + +contract sDaiTest is ERC4626ValidationTest { + using FixedPointMath for *; + + function setUp() public override __mainnet_fork(17_318_972) { + super.setUp(); + + underlyingToken = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + token = IERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA); + IERC20 dai = underlyingToken; + + // Fund alice with DAI + address daiWhale = 0x60FaAe176336dAb62e284Fe19B885B095d29fB7F; + whaleTransfer(daiWhale, dai, alice); + + _setUp(); + } + + function advanceTimeWithYield(uint256 timeDelta) public override { + vm.warp(block.timestamp + timeDelta); + // Interest accumulates in the dsr based on time passed. + // This may caused insolvency if too much interest accrues as no real dai is being + // accrued. + + // Note - Mainnet only address for Pot, but fine since this test explicitly uses a Mainnet fork in test + PotLike(0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7).drip(); + } +} diff --git a/test/mocks/MockAaveHyperdrive.sol b/test/mocks/MockAaveHyperdrive.sol deleted file mode 100644 index 1cb0bc0c4..000000000 --- a/test/mocks/MockAaveHyperdrive.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.19; - -import { AaveHyperdrive, IHyperdrive, IERC20 } from "contracts/src/instances/AaveHyperdrive.sol"; -import { IPool } from "@aave/interfaces/IPool.sol"; - -// We make a contract which can directly access the underlying yield source -// functions so that we can test them directly - -contract MockAaveHyperdrive is AaveHyperdrive { - constructor( - IHyperdrive.PoolConfig memory _config, - address _dataProvider, - bytes32 _linkerCodeHash, - address _linkerFactory, - IERC20 _aToken, - IPool _pool - ) - AaveHyperdrive( - _config, - _dataProvider, - _linkerCodeHash, - _linkerFactory, - _aToken, - _pool - ) - {} - - function deposit( - uint256 amount, - bool asUnderlying - ) public returns (uint256 sharesMinted, uint256 sharePrice) { - return _deposit(amount, asUnderlying); - } - - function withdraw( - uint256 shares, - address destination, - bool asUnderlying - ) public returns (uint256 amountWithdrawn) { - return _withdraw(shares, destination, asUnderlying); - } - - /// @notice Loads the share price from the yield source - /// @return sharePrice The current share price. - function pricePerShare() public view returns (uint256 sharePrice) { - return _pricePerShare(); - } -} diff --git a/test/units/hyperdrive/HyperdriveDeploy.t.sol b/test/units/hyperdrive/HyperdriveDeploy.t.sol index 9bfede760..e1b1e470d 100644 --- a/test/units/hyperdrive/HyperdriveDeploy.t.sol +++ b/test/units/hyperdrive/HyperdriveDeploy.t.sol @@ -4,34 +4,57 @@ pragma solidity 0.8.19; import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; import { IHyperdriveDeployer } from "contracts/src/interfaces/IHyperdriveDeployer.sol"; import { AssetId } from "contracts/src/libraries/AssetId.sol"; -import { DsrHyperdriveDeployer } from "contracts/src/factory/DsrHyperdriveDeployer.sol"; -import { DsrHyperdriveFactory } from "contracts/src/factory/DsrHyperdriveFactory.sol"; +import { ERC4626HyperdriveDeployer } from "contracts/src/factory/ERC4626HyperdriveDeployer.sol"; +import { ERC4626HyperdriveFactory } from "contracts/src/factory/ERC4626HyperdriveFactory.sol"; import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; -import { DsrManager } from "contracts/test/MockDsrHyperdrive.sol"; import { HyperdriveTest } from "../../utils/HyperdriveTest.sol"; +import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol"; +import { IERC20 } from "contracts/src/interfaces/IERC20.sol"; +import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; +import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol"; +import { Mock4626, ERC20 } from "../../mocks/Mock4626.sol"; +import { MockERC4626Hyperdrive } from "../../mocks/Mock4626Hyperdrive.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; contract HyperdriveFactoryTest is HyperdriveTest { - function test_hyperdrive_factory_admin_functions() external { + function test_hyperdrive_factory_admin_functions() + external + __mainnet_fork(16_685_972) + { // Deploy the DsrHyperdrive factory and deployer. - DsrManager manager = DsrManager( - address(0x373238337Bfe1146fb49989fc222523f83081dDb) + alice = createUser("alice"); + bob = createUser("bob"); + + IERC20 dai = IERC20( + address(0x6B175474E89094C44Da98b954EedeAC495271d0F) ); - DsrHyperdriveDeployer simpleDeployer = new DsrHyperdriveDeployer( - manager + + vm.startPrank(deployer); + + // Deploy the ERC4626Hyperdrive factory and deployer. + IERC4626 pool = IERC4626( + address(new Mock4626(ERC20(address(dai)), "yearn dai", "yDai")) ); + + ERC4626HyperdriveDeployer simpleDeployer = new ERC4626HyperdriveDeployer( + pool + ); address[] memory defaults = new address[](1); defaults[0] = bob; - DsrHyperdriveFactory factory = new DsrHyperdriveFactory( + forwarderFactory = new ForwarderFactory(); + ERC4626HyperdriveFactory factory = new ERC4626HyperdriveFactory( alice, simpleDeployer, bob, bob, IHyperdrive.Fees(0, 0, 0), defaults, - address(0), - bytes32(0), - address(manager) + address(forwarderFactory), + forwarderFactory.ERC20LINK_HASH(), + pool ); + assertEq(factory.governance(), alice); // Bob can't change access the admin functions. diff --git a/test/utils/Lib.sol b/test/utils/Lib.sol index a3768e247..06feec801 100644 --- a/test/utils/Lib.sol +++ b/test/utils/Lib.sol @@ -162,32 +162,39 @@ library Lib { function normalizeToRange( uint256 value, - uint256 min, - uint256 max + uint256 minimum, + uint256 maximum ) internal pure returns (uint256) { - require(min <= max, "Lib: min > max"); + require(minimum <= maximum, "Lib: min > max"); - uint256 rangeSize = max - min + 1; + uint256 rangeSize = maximum - minimum + 1; uint256 modValue = value % rangeSize; - return modValue + min; + return modValue + minimum; } function normalizeToRange( int256 value, - int256 min, - int256 max + int256 minimum, + int256 maximum ) internal pure returns (int256) { - require(min <= max, "Lib: min > max"); + require(minimum <= maximum, "Lib: min > max"); - int256 rangeSize = max - min + 1; + int256 rangeSize = maximum - minimum + 1; int256 modValue = value % rangeSize; if (modValue < 0) { modValue += rangeSize; } - return modValue + min; + return modValue + minimum; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; } function eq(bytes memory b1, bytes memory b2) public pure returns (bool) {