From f4aeeeed480ee219bdcc512c43de1b982e53b522 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 8 Jan 2024 11:05:56 -0600 Subject: [PATCH] Adjusts the time stretch calculation to hold the ratio of reserves constant (#716) * Adjust the time stretch to hold the ratio of reserves constant across position durations * Adds a test for the time stretch adjustment --- contracts/test/MockMultiToken.sol | 10 +++- script/DevnetMigration.s.sol | 4 +- test/integrations/ERC4626Hyperdrive.t.sol | 15 ++++- test/integrations/ERC4626Validation.t.sol | 6 +- test/integrations/HyperdriveFactory.t.sol | 2 +- test/integrations/UsdcERC4626.t.sol | 3 +- .../hyperdrive/LPWithdrawalTest.t.sol | 5 +- .../hyperdrive/NonstandardDecimals.sol | 55 ++++++++++++++---- .../hyperdrive/ReentrancyTest.t.sol | 10 +++- .../hyperdrive/SandwichTest.t.sol | 5 +- test/units/ForceRevertDelegatecall.t.sol | 5 +- test/units/hyperdrive/CloseLongTest.t.sol | 9 ++- test/units/hyperdrive/CloseShortTest.t.sol | 16 ++++-- test/units/hyperdrive/ExtremeInputs.t.sol | 50 +++++++++++++---- test/units/hyperdrive/InitializeTest.t.sol | 6 +- test/units/hyperdrive/OpenLongTest.t.sol | 5 +- test/units/hyperdrive/OpenShortTest.t.sol | 16 ++++-- .../hyperdrive/UpdateLiquidityTest.t.sol | 5 +- test/units/libraries/HyperdriveMath.t.sol | 10 +++- .../units/libraries/HyperdriveUtilsTest.t.sol | 56 +++++++++++++++++++ test/units/libraries/LPMath.t.sol | 20 +++++-- test/units/libraries/YieldSpaceMath.t.sol | 8 ++- test/utils/HyperdriveTest.sol | 26 ++++++--- test/utils/HyperdriveUtils.sol | 50 ++++++++++++++++- 24 files changed, 327 insertions(+), 70 deletions(-) create mode 100644 test/units/libraries/HyperdriveUtilsTest.t.sol diff --git a/contracts/test/MockMultiToken.sol b/contracts/test/MockMultiToken.sol index 66884ff32..5f5cfc7cd 100644 --- a/contracts/test/MockMultiToken.sol +++ b/contracts/test/MockMultiToken.sol @@ -57,7 +57,10 @@ contract MockMultiToken is HyperdriveMultiToken, MockHyperdriveBase { minimumTransactionAmount: 1e15, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.05e18, + 365 days + ), governance: address(0), feeCollector: address(0), fees: IHyperdrive.Fees({ @@ -81,7 +84,10 @@ contract MockMultiToken is HyperdriveMultiToken, MockHyperdriveBase { minimumTransactionAmount: 1e15, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.05e18, + 365 days + ), governance: address(0), feeCollector: address(0), fees: IHyperdrive.Fees({ diff --git a/script/DevnetMigration.s.sol b/script/DevnetMigration.s.sol index d85ba0591..942b76c43 100644 --- a/script/DevnetMigration.s.sol +++ b/script/DevnetMigration.s.sol @@ -250,7 +250,9 @@ contract DevnetMigration is Script { checkpointDuration: config.hyperdriveCheckpointDuration, timeStretch: config .hyperdriveTimeStretchApr - .calculateTimeStretch(), + .calculateTimeStretch( + config.hyperdrivePositionDuration + ), governance: config.admin, feeCollector: config.admin, fees: IHyperdrive.Fees({ diff --git a/test/integrations/ERC4626Hyperdrive.t.sol b/test/integrations/ERC4626Hyperdrive.t.sol index 304ea929d..0f9e0407b 100644 --- a/test/integrations/ERC4626Hyperdrive.t.sol +++ b/test/integrations/ERC4626Hyperdrive.t.sol @@ -260,7 +260,10 @@ contract ERC4626HyperdriveTest is HyperdriveTest { minimumTransactionAmount: 0.001e18, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(apr), + timeStretch: HyperdriveUtils.calculateTimeStretch( + apr, + 365 days + ), governance: alice, feeCollector: bob, fees: IHyperdrive.Fees(0, 0, 0, 0) @@ -311,7 +314,10 @@ contract ERC4626HyperdriveTest is HyperdriveTest { minimumTransactionAmount: 0.001e18, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(apr), + timeStretch: HyperdriveUtils.calculateTimeStretch( + apr, + 365 days + ), governance: alice, feeCollector: bob, fees: IHyperdrive.Fees(0, 0, 0, 0) @@ -355,7 +361,10 @@ contract ERC4626HyperdriveTest is HyperdriveTest { minimumTransactionAmount: 0.001e18, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.01e18), + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.01e18, + 365 days + ), governance: alice, feeCollector: bob, fees: IHyperdrive.Fees(0, 0, 0, 0) diff --git a/test/integrations/ERC4626Validation.t.sol b/test/integrations/ERC4626Validation.t.sol index a7652d56c..eafe80c2c 100644 --- a/test/integrations/ERC4626Validation.t.sol +++ b/test/integrations/ERC4626Validation.t.sol @@ -82,7 +82,8 @@ abstract contract ERC4626ValidationTest is HyperdriveTest { // Config changes required to support ERC4626 with the correct initial Share Price IHyperdrive.PoolDeployConfig memory config = testDeployConfig( - FIXED_RATE + FIXED_RATE, + POSITION_DURATION ); config.baseToken = underlyingToken; uint256 contribution = 7_500e18; @@ -123,7 +124,8 @@ abstract contract ERC4626ValidationTest is HyperdriveTest { vm.startPrank(alice); IHyperdrive.PoolDeployConfig memory config = testDeployConfig( - FIXED_RATE + FIXED_RATE, + POSITION_DURATION ); // Required to support ERC4626, since the test config initialSharePrice is wrong config.baseToken = underlyingToken; diff --git a/test/integrations/HyperdriveFactory.t.sol b/test/integrations/HyperdriveFactory.t.sol index e4e5ed703..cd33e0ad1 100644 --- a/test/integrations/HyperdriveFactory.t.sol +++ b/test/integrations/HyperdriveFactory.t.sol @@ -169,7 +169,7 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { minimumTransactionAmount: 1e15, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(APR), + timeStretch: HyperdriveUtils.calculateTimeStretch(APR, 365 days), governance: alice, feeCollector: bob, fees: IHyperdrive.Fees(0, 0, 0, 0), diff --git a/test/integrations/UsdcERC4626.t.sol b/test/integrations/UsdcERC4626.t.sol index e6366e1ec..dc6849699 100644 --- a/test/integrations/UsdcERC4626.t.sol +++ b/test/integrations/UsdcERC4626.t.sol @@ -91,7 +91,8 @@ contract UsdcERC4626 is ERC4626ValidationTest { // Config changes required to support ERC4626 with the correct initial share price. IHyperdrive.PoolDeployConfig memory config = testDeployConfig( - FIXED_RATE + FIXED_RATE, + POSITION_DURATION ); config.baseToken = underlyingToken; config.minimumTransactionAmount = 1e6; diff --git a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol index 33cce9357..a0d7964c5 100644 --- a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol +++ b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol @@ -29,7 +29,10 @@ contract LPWithdrawalTest is HyperdriveTest { super.setUp(); // Deploy a Hyperdrive pool with the standard config and a 5% APR. - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); deploy(deployer, config); } diff --git a/test/integrations/hyperdrive/NonstandardDecimals.sol b/test/integrations/hyperdrive/NonstandardDecimals.sol index 423616588..284c702a3 100644 --- a/test/integrations/hyperdrive/NonstandardDecimals.sol +++ b/test/integrations/hyperdrive/NonstandardDecimals.sol @@ -21,7 +21,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // Deploy the pool with a small minimum share reserves since we're // using nonstandard decimals in this suite. - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -29,7 +32,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { function test_nonstandard_decimals_symmetry() external { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(1e18); + IHyperdrive.PoolConfig memory config = testConfig( + 1e18, + POSITION_DURATION + ); config.initialSharePrice = 0.7348e18; config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; @@ -53,7 +59,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { function test_nonstandard_decimals_longs_outstanding() external { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -83,7 +92,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { function test_nonstandard_decimals_shorts_outstanding() external { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -144,7 +156,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // essentially all of his capital back. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -165,7 +180,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // duration. He should receive the base he paid plus fixed interest. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -199,7 +217,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // value of the bonds. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -241,7 +262,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // essentially all of his capital back. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -263,7 +287,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // interest minus the fixed interest. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -303,7 +330,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { // variable interest earned by the short. { // Deploy and initialize the pool. - IHyperdrive.PoolConfig memory config = testConfig(0.02e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); @@ -385,7 +415,10 @@ contract NonstandardDecimalsTest is HyperdriveTest { uint256 shortAmount ) internal { // Redeploy the pool so that the edge cases function can call it repeatedly. - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); diff --git a/test/integrations/hyperdrive/ReentrancyTest.t.sol b/test/integrations/hyperdrive/ReentrancyTest.t.sol index f702b798f..c16e45b23 100644 --- a/test/integrations/hyperdrive/ReentrancyTest.t.sol +++ b/test/integrations/hyperdrive/ReentrancyTest.t.sol @@ -252,7 +252,10 @@ contract ReentrancyTest is HyperdriveTest { vm.startPrank(deployer); tester = new ReentrantERC20(); baseToken = ERC20Mintable(address(tester)); - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); config.baseToken = IERC20(address(baseToken)); deploy(deployer, config); } @@ -263,7 +266,10 @@ contract ReentrancyTest is HyperdriveTest { tester = new ReentrantEthReceiver(); vm.deal(address(tester), 10_000e18); baseToken = ERC20Mintable(address(ETH)); - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); config.baseToken = IERC20(address(ETH)); deploy(deployer, config); } diff --git a/test/integrations/hyperdrive/SandwichTest.t.sol b/test/integrations/hyperdrive/SandwichTest.t.sol index e8565027a..d47ecfbb5 100644 --- a/test/integrations/hyperdrive/SandwichTest.t.sol +++ b/test/integrations/hyperdrive/SandwichTest.t.sol @@ -134,7 +134,10 @@ contract SandwichTest is HyperdriveTest { uint256 tradeAmount, uint256 sandwichAmount ) external { - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); deploy(alice, config); fixedRate = fixedRate.normalizeToRange(0.001e18, 1e18); contribution = contribution.normalizeToRange(1_000e18, 500_000_000e18); diff --git a/test/units/ForceRevertDelegatecall.t.sol b/test/units/ForceRevertDelegatecall.t.sol index bf18efcf1..91bd5cccc 100644 --- a/test/units/ForceRevertDelegatecall.t.sol +++ b/test/units/ForceRevertDelegatecall.t.sol @@ -36,7 +36,10 @@ contract DummyHyperdrive is Hyperdrive, MockHyperdriveBase { minimumTransactionAmount: 1e15, positionDuration: 365 days, checkpointDuration: 1 days, - timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), + timeStretch: HyperdriveUtils.calculateTimeStretch( + 0.05e18, + 365 days + ), governance: address(0), feeCollector: address(0), fees: IHyperdrive.Fees({ diff --git a/test/units/hyperdrive/CloseLongTest.t.sol b/test/units/hyperdrive/CloseLongTest.t.sol index e65b2175c..2db82f156 100644 --- a/test/units/hyperdrive/CloseLongTest.t.sol +++ b/test/units/hyperdrive/CloseLongTest.t.sol @@ -684,7 +684,10 @@ contract CloseLongTest is HyperdriveTest { uint256 contribution = 500_000_000e18; // 1. Deploy a pool with zero fees - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); deploy(address(deployer), config); // Initialize the pool with a large amount of capital. initialize(alice, fixedRate, contribution); @@ -701,7 +704,7 @@ contract CloseLongTest is HyperdriveTest { // 4. deploy a pool with 100% curve fees and 100% gov fees (this is nice bc // it ensures that all the fees are credited to governance and thus subtracted // from the shareReserves - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 0, flat: 0.01e18, @@ -725,7 +728,7 @@ contract CloseLongTest is HyperdriveTest { assert(govFees > 1e5); // 7. deploy a pool with 100% curve fees and 0% gov fees - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 0, flat: 0.01e18, diff --git a/test/units/hyperdrive/CloseShortTest.t.sol b/test/units/hyperdrive/CloseShortTest.t.sol index 67d4171d7..0364f2480 100644 --- a/test/units/hyperdrive/CloseShortTest.t.sol +++ b/test/units/hyperdrive/CloseShortTest.t.sol @@ -482,7 +482,10 @@ contract CloseShortTest is HyperdriveTest { uint256 contribution = 500_000_000e18; // 1. Deploy a pool with zero fees - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); deploy(address(deployer), config); // Initialize the pool with a large amount of capital. initialize(alice, fixedRate, contribution); @@ -499,7 +502,7 @@ contract CloseShortTest is HyperdriveTest { // 4. deploy a pool with 100% curve fees and 100% gov fees (this is nice bc // it ensures that all the fees are credited to governance and thus subtracted // from the shareReserves - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 0, flat: 1e18, @@ -534,7 +537,7 @@ contract CloseShortTest is HyperdriveTest { assert(govFees > 1e5); // 7. deploy a pool with 100% curve fees and 0% gov fees - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 0, flat: 1e18, @@ -579,7 +582,10 @@ contract CloseShortTest is HyperdriveTest { uint256 maturityTime; // Initialize a pool with no flat fee as a baseline - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.fees = IHyperdrive.Fees({ curve: 0, flat: 0, @@ -613,7 +619,7 @@ contract CloseShortTest is HyperdriveTest { IHyperdrive.MarketState memory noFlatFee = hyperdrive.getMarketState(); // Configure a pool with a 100% flatFee - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 0, flat: 1e18, diff --git a/test/units/hyperdrive/ExtremeInputs.t.sol b/test/units/hyperdrive/ExtremeInputs.t.sol index 2ea41b5f4..2f9d30417 100644 --- a/test/units/hyperdrive/ExtremeInputs.t.sol +++ b/test/units/hyperdrive/ExtremeInputs.t.sol @@ -173,7 +173,10 @@ contract ExtremeInputs is HyperdriveTest { uint256 fixedRate = 0.02e18; // Deploy the pool with a small minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 0.01e6; deploy(deployer, config); @@ -356,7 +359,10 @@ contract ExtremeInputs is HyperdriveTest { 1_000e6, 100_000_000_000e6 ); - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -406,7 +412,10 @@ contract ExtremeInputs is HyperdriveTest { 1e18, 200_000_000e18 ); - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -459,7 +468,10 @@ contract ExtremeInputs is HyperdriveTest { 1_000e18, 100_000_000_000e18 ); - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -515,7 +527,10 @@ contract ExtremeInputs is HyperdriveTest { // `z_1 > z_0` and `y_0` is very large. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -549,7 +564,10 @@ contract ExtremeInputs is HyperdriveTest { // edge case where `z_1 < z_0` and `y_0` is very small. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -617,7 +635,10 @@ contract ExtremeInputs is HyperdriveTest { // edge case where `z_1 > z_0` and `y_0` is very small. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -697,7 +718,10 @@ contract ExtremeInputs is HyperdriveTest { // This tests the edge case where `z_1 > z_0` and `y_0` is very large. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -752,7 +776,10 @@ contract ExtremeInputs is HyperdriveTest { // edge case where `z_1 < z_0` and `y_0` is very large. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); @@ -820,7 +847,10 @@ contract ExtremeInputs is HyperdriveTest { // edge case where `z_1 > z_0` and `y_0` is very large. { // Deploy the pool with the specified minimum share reserves. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.minimumShareReserves = minimumShareReserves; config.minimumTransactionAmount = minimumTransactionAmount; deploy(deployer, config); diff --git a/test/units/hyperdrive/InitializeTest.t.sol b/test/units/hyperdrive/InitializeTest.t.sol index 35a4bbab4..27bba4e70 100644 --- a/test/units/hyperdrive/InitializeTest.t.sol +++ b/test/units/hyperdrive/InitializeTest.t.sol @@ -85,10 +85,12 @@ contract InitializeTest is HyperdriveTest { contribution = contribution.normalizeToRange(1_000e18, 100_000_000e18); // Deploy a Hyperdrive pool with the given parameters. - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + checkpointDuration * checkpointsPerTerm + ); config.initialSharePrice = initialSharePrice; config.checkpointDuration = checkpointDuration; - config.positionDuration = checkpointDuration * checkpointsPerTerm; deploy(alice, config); // Initialize the pool with Alice. diff --git a/test/units/hyperdrive/OpenLongTest.t.sol b/test/units/hyperdrive/OpenLongTest.t.sol index 7c62f20c6..d8a5f8f70 100644 --- a/test/units/hyperdrive/OpenLongTest.t.sol +++ b/test/units/hyperdrive/OpenLongTest.t.sol @@ -296,7 +296,10 @@ contract OpenLongTest is HyperdriveTest { // Deploy the pool with a minimum share reserves that is significantly // smaller than the contribution. - IHyperdrive.PoolConfig memory config = testConfig(apr); + IHyperdrive.PoolConfig memory config = testConfig( + apr, + POSITION_DURATION + ); config.minimumShareReserves = 1e6; config.minimumTransactionAmount = 1e6; deploy(deployer, config); diff --git a/test/units/hyperdrive/OpenShortTest.t.sol b/test/units/hyperdrive/OpenShortTest.t.sol index ce2fd1655..f1ae73536 100644 --- a/test/units/hyperdrive/OpenShortTest.t.sol +++ b/test/units/hyperdrive/OpenShortTest.t.sol @@ -204,7 +204,10 @@ contract OpenShortTest is HyperdriveTest { uint256 contribution = 500_000_000e18; // 1. Deploy a pool with zero fees - IHyperdrive.PoolConfig memory config = testConfig(apr); + IHyperdrive.PoolConfig memory config = testConfig( + apr, + POSITION_DURATION + ); deploy(address(deployer), config); // Initialize the pool with a large amount of capital. initialize(alice, apr, contribution); @@ -220,7 +223,7 @@ contract OpenShortTest is HyperdriveTest { // 4. deploy a pool with 100% curve fees and 100% gov fees (this is nice bc // it ensures that all the fees are credited to governance and thus subtracted // from the shareReserves - config = testConfig(apr); + config = testConfig(apr, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 1e18, flat: 1e18, @@ -255,7 +258,7 @@ contract OpenShortTest is HyperdriveTest { assert(govFees > 1e5); // 7. deploy a pool with 100% curve fees and 0% gov fees - config = testConfig(apr); + config = testConfig(apr, POSITION_DURATION); config.fees = IHyperdrive.Fees({ curve: 1e18, flat: 0, @@ -287,7 +290,10 @@ contract OpenShortTest is HyperdriveTest { // Alice initializes the pool. The pool has a curve fee of 100% and // governance fees of 0%. - IHyperdrive.PoolConfig memory config = testConfig(fixedRate); + IHyperdrive.PoolConfig memory config = testConfig( + fixedRate, + POSITION_DURATION + ); config.fees.curve = 1e18; config.fees.governanceLP = 0; config.fees.governanceZombie = 0; @@ -300,7 +306,7 @@ contract OpenShortTest is HyperdriveTest { // Alice initializes the pool. The pool has a curve fee of 100% and // governance fees of 100%. - config = testConfig(fixedRate); + config = testConfig(fixedRate, POSITION_DURATION); config.fees.curve = 1e18; config.fees.governanceLP = 1e18; config.fees.governanceZombie = 1e18; diff --git a/test/units/hyperdrive/UpdateLiquidityTest.t.sol b/test/units/hyperdrive/UpdateLiquidityTest.t.sol index 8b64712f7..bf745a1ce 100644 --- a/test/units/hyperdrive/UpdateLiquidityTest.t.sol +++ b/test/units/hyperdrive/UpdateLiquidityTest.t.sol @@ -20,7 +20,10 @@ contract UpdateLiquidityTest is HyperdriveTest { function setUp() public override { super.setUp(); - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); config.baseToken = IERC20( address(new ERC20Mintable("Base", "BASE", 18, address(0), false)) ); diff --git a/test/units/libraries/HyperdriveMath.t.sol b/test/units/libraries/HyperdriveMath.t.sol index df424a005..6847d3e0b 100644 --- a/test/units/libraries/HyperdriveMath.t.sol +++ b/test/units/libraries/HyperdriveMath.t.sol @@ -441,7 +441,10 @@ contract HyperdriveMathTest is HyperdriveTest { fixedRate = fixedRate.normalizeToRange(0.005e18, 1e18); uint256 initialShareReserves = 500_000_000e18; uint256 initialSharePrice = INITIAL_SHARE_PRICE; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(fixedRate); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + fixedRate, + POSITION_DURATION + ); uint256 normalizedTimeRemaining = 1e18; uint256 initialBondReserves = hyperdriveMath .calculateInitialBondReserves( @@ -509,7 +512,10 @@ contract HyperdriveMathTest is HyperdriveTest { fixedRate = fixedRate.normalizeToRange(0.005e18, 1e18); uint256 initialShareReserves = 500_000_000e18; uint256 initialSharePrice = INITIAL_SHARE_PRICE; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(fixedRate); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + fixedRate, + POSITION_DURATION + ); uint256 initialBondReserves = hyperdriveMath .calculateInitialBondReserves( initialShareReserves, diff --git a/test/units/libraries/HyperdriveUtilsTest.t.sol b/test/units/libraries/HyperdriveUtilsTest.t.sol new file mode 100644 index 000000000..bbb448df7 --- /dev/null +++ b/test/units/libraries/HyperdriveUtilsTest.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.19; + +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveTest } from "test/utils/HyperdriveTest.sol"; +import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol"; +import { Lib } from "test/utils/Lib.sol"; + +contract HyperdriveUtilsTest is HyperdriveTest { + using FixedPointMath for *; + using HyperdriveUtils for *; + using Lib for *; + + // This test verifies that the time stretch calculation holds the ratio of + // reserves constant after different position durations. + function test_calculateTimeStretch( + uint256 apr, + uint256 positionDuration + ) external { + // Warp time forward by 50 years to avoid any issues handling long terms. + vm.warp(50 * 365 days); + + // Normalize the fuzzing parameters to a reasonable range. + apr = apr.normalizeToRange(0.001e18, 10e18); + positionDuration = positionDuration.normalizeToRange( + 1 days, + 10 * 365 days + ); + + // Deploy and initialize a pool with the target APR and a position + // duration of 1 year. + IHyperdrive.PoolConfig memory config = testConfig(apr, 365 days); + deploy(alice, config); + initialize(alice, apr, 100_000_000e18); + uint256 expectedShareReserves = hyperdrive.getPoolInfo().shareReserves; + uint256 expectedBondReserves = hyperdrive.getPoolInfo().bondReserves; + + // Deploy and initialize a pool with the target APR and the target + // position duration. + config = testConfig(apr, positionDuration); + config.checkpointDuration = positionDuration; + deploy(alice, config); + initialize(alice, apr, 100_000_000e18); + + // Ensure that the ratio of reserves is approximately equal across the + // two pools. + assertApproxEqAbs( + hyperdrive.getPoolInfo().shareReserves.divDown( + hyperdrive.getPoolInfo().bondReserves + ), + expectedShareReserves.divDown(expectedBondReserves), + 1e6 + ); + } +} diff --git a/test/units/libraries/LPMath.t.sol b/test/units/libraries/LPMath.t.sol index a4f8b21e1..879ba9890 100644 --- a/test/units/libraries/LPMath.t.sol +++ b/test/units/libraries/LPMath.t.sol @@ -22,7 +22,10 @@ contract LPMathTest is HyperdriveTest { uint256 apr = 0.02e18; uint256 initialSharePrice = 1e18; uint256 positionDuration = 365 days; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(apr); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + apr, + positionDuration + ); // no open positions. { @@ -632,7 +635,10 @@ contract LPMathTest is HyperdriveTest { uint256 apr = 0.02e18; uint256 initialSharePrice = 0.5e18; uint256 positionDuration = 365 days; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(apr); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + apr, + positionDuration + ); // The pool is net neutral with no open positions. { @@ -991,7 +997,10 @@ contract LPMathTest is HyperdriveTest { uint256 apr = 0.02e18; uint256 initialSharePrice = 1e18; uint256 positionDuration = 365 days; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(apr); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + apr, + positionDuration + ); // The pool is net neutral. { @@ -1285,7 +1294,10 @@ contract LPMathTest is HyperdriveTest { uint256 apr = 0.02e18; uint256 initialSharePrice = 1e18; uint256 positionDuration = 365 days; - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(apr); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + apr, + positionDuration + ); // The pool is net neutral. { diff --git a/test/units/libraries/YieldSpaceMath.t.sol b/test/units/libraries/YieldSpaceMath.t.sol index a15552af5..d0578f5d4 100644 --- a/test/units/libraries/YieldSpaceMath.t.sol +++ b/test/units/libraries/YieldSpaceMath.t.sol @@ -124,7 +124,8 @@ contract YieldSpaceMathTest is Test { for (uint256 j = i - (i / 2 + 1); j < i; j++) { // Calculate the bond reserves that give the pool the expected spot rate. uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( - fixedRate + fixedRate, + 365 days ); uint256 bondReserves = HyperdriveMath .calculateInitialBondReserves( @@ -190,7 +191,10 @@ contract YieldSpaceMathTest is Test { sharePrice = sharePrice.normalizeToRange(initialSharePrice, 5e18); // Calculate the bond reserves that give the pool the expected spot rate. - uint256 timeStretch = HyperdriveUtils.calculateTimeStretch(fixedRate); + uint256 timeStretch = HyperdriveUtils.calculateTimeStretch( + fixedRate, + 365 days + ); uint256 bondReserves = HyperdriveMath.calculateInitialBondReserves( shareReserves, initialSharePrice, diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index 9495eea67..060db74ec 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -40,7 +40,10 @@ contract HyperdriveTest is BaseTest { baseToken = new ERC20Mintable("Base", "BASE", 18, address(0), false); // Instantiate Hyperdrive. - IHyperdrive.PoolConfig memory config = testConfig(0.05e18); + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); deploy(alice, config); vm.stopPrank(); vm.startPrank(governance); @@ -92,7 +95,10 @@ contract HyperdriveTest is BaseTest { uint256 governanceLPFee, uint256 governanceZombieFee ) internal { - IHyperdrive.PoolConfig memory config = testConfig(apr); + IHyperdrive.PoolConfig memory config = testConfig( + apr, + POSITION_DURATION + ); config.initialSharePrice = initialSharePrice; config.fees.curve = curveFee; config.fees.flat = flatFee; @@ -102,10 +108,12 @@ contract HyperdriveTest is BaseTest { } function testConfig( - uint256 fixedRate + uint256 fixedRate, + uint256 positionDuration ) internal view returns (IHyperdrive.PoolConfig memory _config) { IHyperdrive.PoolDeployConfig memory _deployConfig = testDeployConfig( - fixedRate + fixedRate, + positionDuration ); _config.baseToken = _deployConfig.baseToken; @@ -125,7 +133,8 @@ contract HyperdriveTest is BaseTest { } function testDeployConfig( - uint256 fixedRate + uint256 fixedRate, + uint256 positionDuration ) internal view returns (IHyperdrive.PoolDeployConfig memory) { IHyperdrive.Fees memory fees = IHyperdrive.Fees({ curve: 0, @@ -140,9 +149,12 @@ contract HyperdriveTest is BaseTest { linkerCodeHash: bytes32(0), minimumShareReserves: MINIMUM_SHARE_RESERVES, minimumTransactionAmount: MINIMUM_TRANSACTION_AMOUNT, - positionDuration: POSITION_DURATION, + positionDuration: positionDuration, checkpointDuration: CHECKPOINT_DURATION, - timeStretch: HyperdriveUtils.calculateTimeStretch(fixedRate), + timeStretch: HyperdriveUtils.calculateTimeStretch( + fixedRate, + positionDuration + ), governance: governance, feeCollector: feeCollector, fees: fees diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index 82c31677d..906587ca8 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -159,11 +159,57 @@ library HyperdriveUtils { return (_principal, 0); } - function calculateTimeStretch(uint256 apr) internal pure returns (uint256) { + function calculateTimeStretch( + uint256 apr, + uint256 positionDuration + ) internal pure returns (uint256) { + // Calculate the benchmark time stretch. This time stretch is tuned for + // a position duration of 1 year. uint256 timeStretch = uint256(5.24592e18).divDown( uint256(0.04665e18).mulDown(apr * 100) ); - return ONE.divDown(timeStretch); + timeStretch = ONE.divDown(timeStretch); + + // If the position duration is 1 year, we can return the benchmark. + if (positionDuration == 365 days) { + return timeStretch; + } + + // Otherwise, we need to adjust the time stretch to account for the + // position duration. We do this by holding the reserve ratio constant + // and solving for the new time stretch directly. + // + // We can calculate the spot price at the target apr and position + // duration as: + // + // p = 1 / (1 + apr * (positionDuration / 365 days)) + // + // We then calculate the benchmark reserve ratio, `ratio`, implied by + // the benchmark time stretch using the `calculateInitialBondReserves` + // function. + // + // We can then derive the adjusted time stretch using the spot price + // calculation: + // + // p = ratio ** timeStretch + // => + // timeStretch = ln(p) / ln(ratio) + uint256 targetSpotPrice = ONE.divDown( + ONE + apr.mulDivDown(positionDuration, 365 days) + ); + uint256 benchmarkReserveRatio = ONE.divDown( + HyperdriveMath.calculateInitialBondReserves( + ONE, + ONE, + apr, + 365 days, + timeStretch + ) + ); + return + uint256(-int256(targetSpotPrice).ln()).divDown( + uint256(-int256(benchmarkReserveRatio).ln()) + ); } /// Trade Utils ///