diff --git a/contracts/src/HyperdriveLong.sol b/contracts/src/HyperdriveLong.sol index 290813ffe..33d371d5c 100644 --- a/contracts/src/HyperdriveLong.sol +++ b/contracts/src/HyperdriveLong.sol @@ -452,10 +452,6 @@ abstract contract HyperdriveLong is HyperdriveLP { ); } - // FIXME: We should calculate the share adjustment here. There is a - // component of the share adjustment needed for negative interest on the - // curve and another for flat updates. - // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a long. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the short. diff --git a/contracts/src/HyperdriveShort.sol b/contracts/src/HyperdriveShort.sol index 244c71ab2..d789e5d0b 100644 --- a/contracts/src/HyperdriveShort.sol +++ b/contracts/src/HyperdriveShort.sol @@ -67,12 +67,6 @@ abstract contract HyperdriveShort is HyperdriveLP { maturityTime = latestCheckpoint + _positionDuration; uint256 shareReservesDelta; { - // FIXME: After thinking about this a bit, I think that negative - // interest makes it possible to open shorts for free. We should - // test this edge case as it will cause issues. - // - // TODO: What happens to the short deposit if the open share price - // is greater than the current share price? uint256 totalGovernanceFee; ( traderDeposit, @@ -436,7 +430,10 @@ abstract contract HyperdriveShort is HyperdriveLP { // The trader will need to deposit capital to pay for the fixed rate, // the curve fee, the flat fee, and any back-paid interest that will be - // received back upon closing the trade. + // received back upon closing the trade. If negative interest has + // accrued during the current checkpoint, we set close share price to + // equal the open share price. This ensures that shorts don't benefit + // from negative interest that accrued during the current checkpoint. traderDeposit = HyperdriveMath .calculateShortProceeds( _bondAmount, @@ -445,7 +442,7 @@ abstract contract HyperdriveShort is HyperdriveLP { // their deposit. shareReservesDelta - totalGovernanceFee, _openSharePrice, - _sharePrice, + _sharePrice.max(_openSharePrice), _sharePrice, _flatFee ) @@ -454,10 +451,6 @@ abstract contract HyperdriveShort is HyperdriveLP { return (traderDeposit, shareReservesDelta, totalGovernanceFee); } - // FIXME: We should calculate the share adjustment here. There is a - // component of the share adjustment needed for negative interest on the - // curve and another for flat updates. - // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a short. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 360cf8efb..03ea5e2bc 100644 --- a/contracts/src/libraries/HyperdriveMath.sol +++ b/contracts/src/libraries/HyperdriveMath.sol @@ -335,10 +335,6 @@ library HyperdriveMath { // shareCurveDelta int256 shareAdjustmentDelta; if (_closeSharePrice < _openSharePrice) { - // TODO: It may be better to scale here considering that the current - // negative interest logic creates dangerous situations in - // `openShort`. - // // We only need to scale the proceeds in the case that we're closing // a long since `calculateShortProceeds` accounts for negative // interest. diff --git a/crates/hyperdrive-math/tests/integration_tests.rs b/crates/hyperdrive-math/tests/integration_tests.rs index cab9ac33a..6aa341754 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -68,6 +68,7 @@ async fn preamble( // TODO: Unignore after we add the logic to apply checkpoints prior to computing // the max long. +#[ignore] #[tokio::test] pub async fn test_integration_get_max_short() -> Result<()> { // Set up a random number generator. We use ChaCha8Rng with a randomly diff --git a/test/integrations/hyperdrive/PresentValueTest.t.sol b/test/integrations/hyperdrive/PresentValueTest.t.sol index 6e9f6e5ad..9836159a5 100644 --- a/test/integrations/hyperdrive/PresentValueTest.t.sol +++ b/test/integrations/hyperdrive/PresentValueTest.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.19; -import { console } from "forge-std/console.sol"; +import { console2 as console } from "forge-std/console2.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"; @@ -525,6 +525,10 @@ contract PresentValueTest is HyperdriveTest { } function test_present_value(bytes32 __seed) external { + // TODO: It would be better to bound all of the intermediate present values + // to the starting present value instead of bounding to the previous present + // value. + // // TODO: This tolerance is WAY too large. uint256 tolerance = 1_000_000e18; @@ -649,7 +653,7 @@ contract PresentValueTest is HyperdriveTest { POSITION_DURATION.mulDown(0.99e18) ); int256 variableRate = int256(uint256(seed())).normalizeToRange( - 0, + -0.5e18, 1e18 ); advanceTime(timeDelta, variableRate); @@ -742,11 +746,118 @@ contract PresentValueTest is HyperdriveTest { info1 = hyperdrive.getPoolInfo(); } - // Ensure that the reserves are approximately the same across the two - // rounds of trading. - assertApproxEqAbs(info0.shareReserves, info1.shareReserves, 1e12); + // Ensure that the ending YieldSpace coordinates are approximately + // equal. The ending share reserves and share adjustment may not match + // because the negative interest component of the share adjustment is + // path dependent. + assertApproxEqAbs( + HyperdriveMath.calculateEffectiveShareReserves( + info0.shareReserves, + info0.shareAdjustment + ), + HyperdriveMath.calculateEffectiveShareReserves( + info1.shareReserves, + info1.shareAdjustment + ), + 1e12 + ); assertApproxEqAbs(info0.bondReserves, info1.bondReserves, 1e12); - assertApproxEqAbs(info0.shareAdjustment, info1.shareAdjustment, 1e12); + } + + function test_k_invariance(bytes32 __seed) external { + uint256 tolerance = 1e12; + + // Set the seed. + _seed = __seed; + + // Initialize the pool. + initialize(alice, 0.02e18, 500_000_000e18); + + // Accrues positive interest for a period. + advanceTime(hyperdrive.getPoolConfig().positionDuration, 1e18); + + // Execute a series of random open trades. We ensure that k remains + // invariant throughout the trading. + uint256 k = hyperdrive.k(); + uint256 maturityTime0 = hyperdrive.maturityTimeFromLatestCheckpoint(); + Trade[] memory trades0 = randomOpenTrades(); + for (uint256 i = 0; i < trades0.length; i++) { + executeTrade(trades0[i]); + assertApproxEqAbs(hyperdrive.k(), k, tolerance); + k = hyperdrive.k(); + } + + // Time passes and interest accrues. + { + uint256 timeDelta = uint256(seed()).normalizeToRange( + CHECKPOINT_DURATION, + POSITION_DURATION.mulDown(0.99e18) + ); + int256 variableRate = int256(uint256(seed())).normalizeToRange( + -0.2e18, + 1e18 + ); + advanceTime(timeDelta, variableRate); + } + + // Execute a series of random open trades. We ensure that k remains + // invariant throughout the trading. + k = hyperdrive.k(); + uint256 maturityTime1 = hyperdrive.maturityTimeFromLatestCheckpoint(); + Trade[] memory trades1 = randomOpenTrades(); + for (uint256 i = 0; i < trades1.length; i++) { + executeTrade(trades1[i]); + assertApproxEqAbs(hyperdrive.k(), k, tolerance); + k = hyperdrive.k(); + } + + // Close all of the positions in a random order and verify that k is + // invariant throughout the trading. + Trade[] memory closeTrades; + { + Trade[] memory closeTrades0 = randomCloseTrades( + maturityTime0, + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + maturityTime0 + ), + alice + ), + maturityTime0, + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime0 + ), + alice + ) + ); + Trade[] memory closeTrades1 = randomCloseTrades( + maturityTime1, + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + maturityTime1 + ), + alice + ), + maturityTime1, + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime1 + ), + alice + ) + ); + closeTrades = combineTrades(closeTrades0, closeTrades1); + } + for (uint256 i = 0; i < closeTrades.length; i++) { + executeTrade(closeTrades[i]); + assertApproxEqAbs(hyperdrive.k(), k, tolerance); + k = hyperdrive.k(); + } } /// Random Trading /// diff --git a/test/units/hyperdrive/OpenShortTest.t.sol b/test/units/hyperdrive/OpenShortTest.t.sol index 6272ffec8..78bf8f984 100644 --- a/test/units/hyperdrive/OpenShortTest.t.sol +++ b/test/units/hyperdrive/OpenShortTest.t.sol @@ -159,29 +159,6 @@ contract OpenShortTest is HyperdriveTest { ); } - function test_RevertsWithNegativeInterestRate() public { - uint256 apr = 0.05e18; - - // Initialize the pool with a large amount of capital. - uint256 contribution = 500_000_000e18; - initialize(alice, apr, contribution); - - vm.stopPrank(); - vm.startPrank(bob); - - uint256 bondAmount = (hyperdrive.calculateMaxShort() * 90) / 100; - openShort(bob, bondAmount); - - uint256 longAmount = (hyperdrive.calculateMaxLong() * 50) / 100; - openLong(bob, longAmount); - - //vm.expectRevert(IHyperdrive.NegativeInterest.selector); - - uint256 baseAmount = (hyperdrive.calculateMaxShort() * 100) / 100; - openShort(bob, baseAmount); - //I think we could trigger this with big short, open long, and short? - } - function test_governance_fees_excluded_share_reserves() public { uint256 apr = 0.05e18; uint256 contribution = 500_000_000e18; @@ -289,6 +266,47 @@ contract OpenShortTest is HyperdriveTest { assertEq(basePaid, basePaid2); } + function test_open_short_after_negative_interest( + int256 variableRate + ) external { + // Alice initializes the pool. + uint256 fixedRate = 0.05e18; + uint256 contribution = 500_000_000e18; + initialize(alice, fixedRate, contribution); + + // Get the deposit amount for a short opened with no negative interest. + uint256 expectedBasePaid; + uint256 snapshotId = vm.snapshot(); + uint256 shortAmount = 100_000e18; + { + hyperdrive.checkpoint(hyperdrive.latestCheckpoint()); + advanceTime( + hyperdrive.getPoolConfig().checkpointDuration.mulDown(0.5e18), + 0 + ); + (, expectedBasePaid) = openShort(bob, shortAmount); + } + vm.revertTo(snapshotId); + + // Get the deposit amount for a short opened with negative interest. + variableRate = variableRate.normalizeToRange(-100e18, 0); + uint256 basePaid; + { + hyperdrive.checkpoint(hyperdrive.latestCheckpoint()); + advanceTime( + hyperdrive.getPoolConfig().checkpointDuration.mulDown(0.5e18), + variableRate + ); + (, basePaid) = openShort(bob, shortAmount); + } + + // The base paid should be greater than or equal (with a fudge factor) + // to the base paid with no negative interest. In theory, we'd like the + // deposits to be equal, but the trading calculation changes slightly + // with negative interest due to rounding. + assertGe(basePaid + 1e9, expectedBasePaid); + } + function verifyOpenShort( IHyperdrive.PoolInfo memory poolInfoBefore, uint256 contribution, diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index b1f6717c6..4414f5937 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; 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 { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol"; import { YieldSpaceMath } from "contracts/src/libraries/YieldSpaceMath.sol"; @@ -368,6 +368,22 @@ library HyperdriveUtils { int256(config.minimumShareReserves); } + function k(IHyperdrive hyperdrive) internal view returns (uint256) { + IHyperdrive.PoolConfig memory config = hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); + return + YieldSpaceMath.modifiedYieldSpaceConstant( + info.sharePrice.divDown(config.initialSharePrice), + config.initialSharePrice, + HyperdriveMath.calculateEffectiveShareReserves( + info.shareReserves, + info.shareAdjustment + ), + ONE - config.timeStretch, + info.bondReserves + ); + } + function decodeError( bytes memory _error ) internal pure returns (string memory) {