diff --git a/codegen/hyperdrive_codegen/templates.py b/codegen/hyperdrive_codegen/templates.py index f4adadf56..e5cecfbc0 100644 --- a/codegen/hyperdrive_codegen/templates.py +++ b/codegen/hyperdrive_codegen/templates.py @@ -60,6 +60,11 @@ class FileInfo: TemplatePathInfo("interfaces/IYieldSource.sol.jinja", "", "interfaces"), ] +test_templates = [ + TemplatePathInfo("tests/Test.t.sol.jinja", "Test", "test"), + TemplatePathInfo("tests/InstanceTest.t.sol.jinja", "InstanceTest", "test"), +] + def get_templates(env: Environment) -> list[TemplateInfo]: """Returns a list of template files for generating customized Hyperdrive instances. @@ -76,7 +81,7 @@ def get_templates(env: Environment) -> list[TemplateInfo]: """ # Gather the template file strings and return a list of TemplateInfo's. - path_infos = deployer_templates + instance_templates + interface_templates + path_infos = deployer_templates + instance_templates + interface_templates + test_templates return [TemplateInfo(template=env.get_template(path_info.path), path_info=path_info) for path_info in path_infos] @@ -102,12 +107,19 @@ def write_templates_to_files(templates: list[TemplateInfo], output_path: Path, t # Prepend 'I' to the file name if it is an interface file is_interface_file = file_info.template.path_info.folder == "interfaces" + is_test_file = file_info.template.path_info.folder == "test" if is_interface_file: contract_file_name = "I" + contract_file_name # NOTE: don't place interface files in a subfolder contract_file_path = Path( os.path.join(output_path, file_info.template.path_info.folder, contract_file_name) ) + # Put test files in test/instances + elif is_test_file: + # NOTE: don't place interface files in a subfolder + contract_file_path = Path( + os.path.join(file_info.template.path_info.folder, "instances", contract_file_name) + ) else: contract_file_path = Path( os.path.join( diff --git a/codegen/templates/tests/InstanceTest.t.sol.jinja b/codegen/templates/tests/InstanceTest.t.sol.jinja new file mode 100644 index 000000000..a9e1b84e7 --- /dev/null +++ b/codegen/templates/tests/InstanceTest.t.sol.jinja @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { {{ name.capitalized }}HyperdriveCoreDeployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}HyperdriveCoreDeployer.sol"; +import { {{ name.capitalized }}HyperdriveDeployerCoordinator } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}HyperdriveDeployerCoordinator.sol"; +import { {{ name.capitalized }}Target0Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target0Deployer.sol"; +import { {{ name.capitalized }}Target1Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target1Deployer.sol"; +import { {{ name.capitalized }}Target2Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target2Deployer.sol"; +import { {{ name.capitalized }}Target3Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target3Deployer.sol"; +import { {{ name.capitalized }}Target4Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target4Deployer.sol"; +import { {{ name.capitalized }}Conversions } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Conversions.sol"; +import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol"; +import { I{{ name.capitalized }} } from "../../../contracts/src/interfaces/I{{ name.capitalized }}.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { ERC20ForwarderFactory } from "../../../contracts/src/token/ERC20ForwarderFactory.sol"; +import { FixedPointMath } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { ERC20ForwarderFactory } from "../../../contracts/src/token/ERC20ForwarderFactory.sol"; +import { InstanceTest } from "../../utils/InstanceTest.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { Lib } from "../../utils/Lib.sol"; + +abstract contract {{ name.capitalized }}InstanceTest is InstanceTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// Overrides /// + + /// @dev Gets the extra data used to deploy Hyperdrive instances. + /// @return The extra data. + function getExtraData() internal pure override returns (bytes memory) { + return new bytes(0); + } + + /// @dev Converts base amount to the equivalent about in shares. + /// @param baseAmount The base amount. + /// @return The converted share amount. + function convertToShares( + uint256 baseAmount + ) internal view override returns (uint256) { + // FIXME: add the correct conversion + return + {{ name.capitalized }}Conversions.convertToShares( + config.vaultSharesToken, + baseAmount + ); + } + + /// @dev Converts share amount to the equivalent amount in base. + /// @param shareAmount The share amount. + /// @return The converted base amount. + function convertToBase( + uint256 shareAmount + ) internal view override returns (uint256) { + // FIXME: add the correct conversion + return + {{ name.capitalized }}Conversions.convertToBase( + config.vaultSharesToken, + shareAmount + ); + } + + /// @dev Deploys the rsETH Linea deployer coordinator contract. + /// @param _factory The address of the Hyperdrive factory. + /// @return The coordinator address. + function deployCoordinator( + address _factory + ) internal override returns (address) { + vm.startPrank(alice); + return + address( + new {{ name.capitalized }}HyperdriveDeployerCoordinator( + string.concat(config.name, "DeployerCoordinator"), + _factory, + address(new {{ name.capitalized }}HyperdriveCoreDeployer()), + address(new {{ name.capitalized }}Target0Deployer()), + address(new {{ name.capitalized }}Target1Deployer()), + address(new {{ name.capitalized }}Target2Deployer()), + address(new {{ name.capitalized }}Target3Deployer()), + address(new {{ name.capitalized }}Target4Deployer()) + ) + ); + } + + /// @dev Fetches the total supply of the base and share tokens. + /// @return The total supply of base. + /// @return The total supply of vault shares. + function getSupply() + internal + view + virtual + override + returns (uint256, uint256) + { + return ( + I{{ name.capitalized }}(address(config.vaultSharesToken)).totalAssets(), + I{{ name.capitalized }}(address(config.vaultSharesToken)).totalSupply() + ); + } + + /// @dev Fetches the token balance information of an account. + /// @param account The account to query. + /// @return The balance of base. + /// @return The balance of vault shares. + function getTokenBalances( + address account + ) internal view override returns (uint256, uint256) { + return ( + config.baseToken.balanceOf(account), + config.vaultSharesToken.balanceOf(account) + ); + } + + /// Getters /// + + /// @dev Test for the additional getters. In the case of the {{ name.capitalized }}Hyperdrive + /// instance, there are no additional getter and we just test the + /// `totalShares` implementation. + function test_getters() external view { + // FIXME: add t.ests for additional getters here + (, uint256 totalShares) = getTokenBalances(address(hyperdrive)); + assertEq(hyperdrive.totalShares(), totalShares); + } + + /// Price Per Share /// + + /// @dev Fuzz test that verifies that the vault share price is the price + /// that dictates the conversion between base and shares. + /// @param basePaid the fuzz parameter for the base paid. + function test__pricePerVaultShare(uint256 basePaid) external virtual { + // FIXME: add test for the price per vaul share here. {{ name.capitalized }} example + // is provided. + // Ensure that the share price is the expected value. + (uint256 totalBase, uint256 totalSupply) = getSupply(); + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + assertApproxEqAbs(vaultSharePrice, totalBase.divDown(totalSupply), 1); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of shares. This will + // be an approximation. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + hyperdrive.calculateMaxLong() + ); + (, uint256 hyperdriveSharesBefore) = getTokenBalances( + address(hyperdrive) + ); + openLong(bob, basePaid); + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveSharesBefore + basePaid.divDown(vaultSharePrice), + config.shareTolerance + ); + } +} + diff --git a/codegen/templates/tests/Test.t.sol.jinja b/codegen/templates/tests/Test.t.sol.jinja new file mode 100644 index 000000000..3f6f52c96 --- /dev/null +++ b/codegen/templates/tests/Test.t.sol.jinja @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { {{ name.capitalized }}HyperdriveCoreDeployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}HyperdriveCoreDeployer.sol"; +import { {{ name.capitalized }}HyperdriveDeployerCoordinator } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}HyperdriveDeployerCoordinator.sol"; +import { {{ name.capitalized }}Target0Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target0Deployer.sol"; +import { {{ name.capitalized }}Target1Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target1Deployer.sol"; +import { {{ name.capitalized }}Target2Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target2Deployer.sol"; +import { {{ name.capitalized }}Target3Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target3Deployer.sol"; +import { {{ name.capitalized }}Target4Deployer } from "../../../contracts/src/deployers/{{ name.lowercase }}/{{ name.capitalized }}Target4Deployer.sol"; +import { HyperdriveFactory } from "../../../contracts/src/factory/HyperdriveFactory.sol"; +import { {{ name.capitalized }}Target0 } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Target0.sol"; +import { {{ name.capitalized }}Target1 } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Target1.sol"; +import { {{ name.capitalized }}Target2 } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Target2.sol"; +import { {{ name.capitalized }}Target3 } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Target3.sol"; +import { {{ name.capitalized }}Target4 } from "../../../contracts/src/instances/{{ name.lowercase }}/{{ name.capitalized }}Target4.sol"; +import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol"; +import { I{{ name.capitalized }} } from "../../../contracts/src/interfaces/I{{ name.capitalized }}.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveAdminController } from "../../../contracts/src/interfaces/IHyperdriveAdminController.sol"; +import { IHyperdriveDeployerCoordinator } from "../../../contracts/src/interfaces/IHyperdriveDeployerCoordinator.sol"; +import { IHyperdriveFactory } from "../../../contracts/src/interfaces/IHyperdriveFactory.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { {{ name.capitalized }}_HYPERDRIVE_KIND, {{ name.capitalized }}_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND, VERSION } from "../../../contracts/src/libraries/Constants.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { ERC20ForwarderFactory } from "../../../contracts/src/token/ERC20ForwarderFactory.sol"; +import { ERC20Mintable } from "../../../contracts/test/ERC20Mintable.sol"; +import { Mock{{ name.capitalized }} } from "../../../contracts/test/Mock{{ name.capitalized }}.sol"; +import { Mock{{ name.capitalized }}Hyperdrive } from "../../../contracts/test/Mock{{ name.capitalized }}Hyperdrive.sol"; +import { HyperdriveTest } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +contract {{ name.capitalized }}Test is HyperdriveTest { + using FixedPointMath for *; + using Lib for *; + + string internal constant HYPERDRIVE_NAME = "Hyperdrive"; + string internal constant COORDINATOR_NAME = "HyperdriveDeployerCoordinator"; + + address internal deployerCoordinator; + address internal coreDeployer; + address internal target0Deployer; + address internal target1Deployer; + address internal target2Deployer; + address internal target3Deployer; + address internal target4Deployer; + + // FIXME: add the correct base token here. Dai provided as an example. + IERC20 internal dai = + IERC20(address(0x6B175474E89094C44Da98b954EedeAC495271d0F)); + I{{ name.capitalized }} internal pool; + uint256 internal aliceShares; + Mock{{ name.capitalized }}Hyperdrive internal mockHyperdrive; + IHyperdriveFactory internal factory; + + function setUp() public override __mainnet_fork(16_685_972) { + alice = createUser("alice"); + bob = createUser("bob"); + + vm.startPrank(deployer); + + // Deploy the {{ name.capitalized }}Hyperdrive factory and deployer. + pool = I{{ name.capitalized }}( + address( + new Mock{{ name.capitalized }}( + ERC20Mintable(address(dai)), + "yearn dai", + "yDai", + 0, + address(0), + false, + type(uint256).max + ) + ) + ); + address[] memory defaults = new address[](1); + defaults[0] = bob; + forwarderFactory = new ERC20ForwarderFactory("ForwarderFactory"); + factory = new HyperdriveFactory( + HyperdriveFactory.FactoryConfig({ + governance: alice, + deployerCoordinatorManager: celine, + hyperdriveGovernance: bob, + feeCollector: feeCollector, + sweepCollector: sweepCollector, + checkpointRewarder: address(0), + defaultPausers: defaults, + checkpointDurationResolution: 1 hours, + minCheckpointDuration: 8 hours, + maxCheckpointDuration: 1 days, + minPositionDuration: 7 days, + maxPositionDuration: 10 * 365 days, + minCircuitBreakerDelta: 0.15e18, + // NOTE: This is a high max circuit breaker delta to ensure that + // trading during tests isn't impeded by the circuit breaker. + maxCircuitBreakerDelta: 2e18, + minFixedAPR: 0.001e18, + maxFixedAPR: 0.5e18, + minTimeStretchAPR: 0.005e18, + maxTimeStretchAPR: 0.5e18, + minFees: IHyperdrive.Fees({ + curve: 0, + flat: 0, + governanceLP: 0, + governanceZombie: 0 + }), + maxFees: IHyperdrive.Fees({ + curve: ONE, + flat: ONE, + governanceLP: ONE, + governanceZombie: ONE + }), + linkerFactory: address(forwarderFactory), + linkerCodeHash: forwarderFactory.ERC20LINK_HASH() + }), + "HyperdriveFactory" + ); + coreDeployer = address(new {{ name.capitalized }}HyperdriveCoreDeployer()); + target0Deployer = address(new {{ name.capitalized }}Target0Deployer()); + target1Deployer = address(new {{ name.capitalized }}Target1Deployer()); + target2Deployer = address(new {{ name.capitalized }}Target2Deployer()); + target3Deployer = address(new {{ name.capitalized }}Target3Deployer()); + target4Deployer = address(new {{ name.capitalized }}Target4Deployer()); + deployerCoordinator = address( + new {{ name.capitalized }}HyperdriveDeployerCoordinator( + COORDINATOR_NAME, + address(factory), + coreDeployer, + target0Deployer, + target1Deployer, + target2Deployer, + target3Deployer, + target4Deployer + ) + ); + + // Transfer a large amount of DAI to Alice. + address whale = 0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8; + whaleTransfer(whale, dai, alice); + + // Deploy a MockHyperdrive instance. + IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({ + baseToken: dai, + vaultSharesToken: pool, + linkerFactory: address(0), + linkerCodeHash: bytes32(0), + initialVaultSharePrice: ONE, + minimumShareReserves: ONE, + minimumTransactionAmount: 0.001e18, + // NOTE: This is a high max circuit breaker delta to ensure that + // trading during tests isn't impeded by the circuit breaker. + circuitBreakerDelta: 2e18, + positionDuration: 365 days, + checkpointDuration: 1 days, + timeStretch: ONE.divDown(22.186877016851916266e18), + governance: alice, + feeCollector: bob, + sweepCollector: celine, + checkpointRewarder: address(0), + fees: IHyperdrive.Fees(0, 0, 0, 0) + }); + address target0 = address( + new {{ name.capitalized }}Target0( + config, + IHyperdriveAdminController(address(factory)) + ) + ); + address target1 = address( + new {{ name.capitalized }}Target1( + config, + IHyperdriveAdminController(address(factory)) + ) + ); + address target2 = address( + new {{ name.capitalized }}Target2( + config, + IHyperdriveAdminController(address(factory)) + ) + ); + address target3 = address( + new {{ name.capitalized }}Target3( + config, + IHyperdriveAdminController(address(factory)) + ) + ); + address target4 = address( + new {{ name.capitalized }}Target4( + config, + IHyperdriveAdminController(address(factory)) + ) + ); + mockHyperdrive = new Mock{{ name.capitalized }}Hyperdrive( + HYPERDRIVE_NAME, + config, + IHyperdriveAdminController(address(factory)), + target0, + target1, + target2, + target3, + target4 + ); + + vm.stopPrank(); + vm.startPrank(alice); + factory.addDeployerCoordinator(deployerCoordinator); + dai.approve(address(factory), type(uint256).max); + dai.approve(address(hyperdrive), type(uint256).max); + dai.approve(address(mockHyperdrive), type(uint256).max); + dai.approve(address(pool), type(uint256).max); + aliceShares = pool.deposit(10e18, alice); + + 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_{{ name.lowercase }}_name() external view { + assertEq(IHyperdrive(address(mockHyperdrive)).name(), HYPERDRIVE_NAME); + assertEq( + IHyperdriveDeployerCoordinator(deployerCoordinator).name(), + "HyperdriveDeployerCoordinator" + ); + } + + function test_{{ name.lowercase }}_kind() external view { + assertEq( + IHyperdrive(address(mockHyperdrive)).kind(), + {{ name.capitalized }}_HYPERDRIVE_KIND + ); + assertEq( + IHyperdriveDeployerCoordinator(deployerCoordinator).kind(), + {{ name.capitalized }}_HYPERDRIVE_DEPLOYER_COORDINATOR_KIND + ); + } + + function test_{{ name.lowercase }}_version() external view { + assertEq(IHyperdrive(address(mockHyperdrive)).version(), VERSION); + assertEq( + IHyperdriveDeployerCoordinator(deployerCoordinator).version(), + VERSION + ); + } + + function test_{{ name.lowercase }}_deposit() external { + // First we add some interest + vm.startPrank(alice); + dai.transfer(address(pool), 5e18); + // Now we try a deposit + (uint256 sharesMinted, uint256 vaultSharePrice) = mockHyperdrive + .deposit( + 1e18, + IHyperdrive.Options({ + destination: address(0), + asBase: true, + extraData: new bytes(0) + }) + ); + assertEq(vaultSharePrice, 1.5e18); + // 1/1.5 = 0.666666666666666666 + assertEq(sharesMinted, 666666666666666666); + assertEq(pool.balanceOf(address(mockHyperdrive)), 666666666666666666); + + // Now we try to do a deposit from alice's shares + pool.approve(address(mockHyperdrive), type(uint256).max); + (sharesMinted, vaultSharePrice) = mockHyperdrive.deposit( + 3e18, + IHyperdrive.Options({ + destination: address(0), + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(vaultSharePrice, 1.5e18); + assertEq(sharesMinted, 3e18); + // 666666666666666666 shares + 3e18 shares = 3666666666666666666 + assertApproxEqAbs( + pool.balanceOf(address(mockHyperdrive)), + 3666666666666666666, + 2 + ); + } + + function test_{{ name.lowercase }}_withdraw() external { + // First we add some shares and interest + vm.startPrank(alice); + dai.transfer(address(pool), 5e18); + pool.transfer(address(mockHyperdrive), 10e18); + uint256 balanceBefore = dai.balanceOf(alice); + // test an underlying withdraw + uint256 amountWithdrawn = mockHyperdrive.withdraw( + 2e18, + mockHyperdrive.pricePerVaultShare(), + IHyperdrive.Options({ + destination: alice, + asBase: true, + extraData: new bytes(0) + }) + ); + uint256 balanceAfter = dai.balanceOf(alice); + assertEq(balanceAfter, balanceBefore + 3e18); + assertEq(amountWithdrawn, 3e18); + + // Test a share withdraw + amountWithdrawn = mockHyperdrive.withdraw( + 2e18, + mockHyperdrive.pricePerVaultShare(), + IHyperdrive.Options({ + destination: alice, + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(pool.balanceOf(alice), 2e18); + assertEq(amountWithdrawn, 2e18); + } + + function test_{{ name.lowercase }}_withdraw_zero() external { + // First we add some shares and interest. + vm.startPrank(alice); + dai.transfer(address(pool), 5e18); + pool.transfer(address(mockHyperdrive), 10e18); + uint256 balanceBefore = dai.balanceOf(alice); + + // Test an underlying withdraw of zero. + uint256 amountWithdrawn = mockHyperdrive.withdraw( + 0, + mockHyperdrive.pricePerVaultShare(), + IHyperdrive.Options({ + destination: alice, + asBase: true, + extraData: new bytes(0) + }) + ); + uint256 balanceAfter = dai.balanceOf(alice); + assertEq(balanceAfter, balanceBefore); + assertEq(amountWithdrawn, 0); + + // Test a share withdraw of zero. + amountWithdrawn = mockHyperdrive.withdraw( + 0, + mockHyperdrive.pricePerVaultShare(), + IHyperdrive.Options({ + destination: alice, + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(pool.balanceOf(alice), 0); + assertEq(amountWithdrawn, 0); + } + + function test_{{ name.lowercase }}_testDeploy() external { + vm.startPrank(alice); + uint256 apr = 0.01e18; // 1% apr + uint256 contribution = 2_500e18; + IHyperdrive.PoolDeployConfig memory config = IHyperdrive + .PoolDeployConfig({ + baseToken: dai, + vaultSharesToken: pool, + linkerFactory: factory.linkerFactory(), + linkerCodeHash: factory.linkerCodeHash(), + minimumShareReserves: ONE, + minimumTransactionAmount: 0.001e18, + // NOTE: This is a high max circuit breaker delta to ensure that + // trading during tests isn't impeded by the circuit breaker. + circuitBreakerDelta: 2e18, + positionDuration: 365 days, + checkpointDuration: 1 days, + timeStretch: 0, + governance: factory.hyperdriveGovernance(), + feeCollector: factory.feeCollector(), + sweepCollector: factory.sweepCollector(), + checkpointRewarder: address(0), + fees: IHyperdrive.Fees(0, 0, 0, 0) + }); + dai.approve(address(deployerCoordinator), type(uint256).max); + for ( + uint256 i = 0; + i < + IHyperdriveDeployerCoordinator(deployerCoordinator) + .getNumberOfTargets(); + i++ + ) { + factory.deployTarget( + bytes32(uint256(0xdeadbeef)), + deployerCoordinator, + config, + new bytes(0), + apr, + apr, + i, + bytes32(uint256(0xdeadbabe)) + ); + } + hyperdrive = factory.deployAndInitialize( + bytes32(uint256(0xdeadbeef)), + deployerCoordinator, + HYPERDRIVE_NAME, + config, + new bytes(0), + contribution, + apr, + apr, + IHyperdrive.Options({ + asBase: true, + destination: alice, + extraData: new bytes(0) + }), + bytes32(uint256(0xdeadbabe)) + ); + + // 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( + deployerCoordinator, + hyperdrive, + alice, + contribution, + apr, + true, + config.minimumShareReserves, + new bytes(0), + 0 + ); + } + + function test_{{ name.lowercase }}_vaultSharePrice() public { + // This test ensures that `getPoolInfo` returns the correct share price. + vm.startPrank(alice); + uint256 apr = 0.01e18; // 1% apr + uint256 contribution = 2_500e18; + IHyperdrive.PoolDeployConfig memory config = IHyperdrive + .PoolDeployConfig({ + baseToken: dai, + vaultSharesToken: pool, + linkerFactory: factory.linkerFactory(), + linkerCodeHash: factory.linkerCodeHash(), + minimumShareReserves: ONE, + minimumTransactionAmount: 0.001e18, + // NOTE: This is a high max circuit breaker delta to ensure that + // trading during tests isn't impeded by the circuit breaker. + circuitBreakerDelta: 2e18, + positionDuration: 365 days, + checkpointDuration: 1 days, + timeStretch: 0, + governance: factory.hyperdriveGovernance(), + feeCollector: factory.feeCollector(), + sweepCollector: factory.sweepCollector(), + checkpointRewarder: address(0), + fees: IHyperdrive.Fees(0, 0, 0, 0) + }); + dai.approve(address(deployerCoordinator), type(uint256).max); + for ( + uint256 i = 0; + i < + IHyperdriveDeployerCoordinator(deployerCoordinator) + .getNumberOfTargets(); + i++ + ) { + factory.deployTarget( + bytes32(uint256(0xdead)), + deployerCoordinator, + config, + new bytes(0), + apr, + apr, + i, + bytes32(uint256(0xbabe)) + ); + } + hyperdrive = factory.deployAndInitialize( + bytes32(uint256(0xdead)), + deployerCoordinator, + HYPERDRIVE_NAME, + config, + new bytes(0), + contribution, + apr, + apr, + IHyperdrive.Options({ + asBase: true, + destination: alice, + extraData: new bytes(0) + }), + bytes32(uint256(0xbabe)) + ); + + // Ensure the share price is 1 after initialization. + assertEq(hyperdrive.getPoolInfo().vaultSharePrice, 1e18); + + // Simulate interest accrual by sending funds to the pool. + dai.transfer(address(pool), contribution); + + // Ensure that the share price calculations are correct when share price is not equal to 1e18. + assertEq( + hyperdrive.getPoolInfo().vaultSharePrice, + (pool.totalAssets()).divDown(pool.totalSupply()) + ); + } +}