From 844fc0e497bedcc3519c7d602d994f1128416109 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 11 Dec 2023 19:58:49 -0600 Subject: [PATCH] Netting fix and simplification (#692) * Simplified the netting flow * Updated the math behind `calculateMaxLong` and `calculateMaxShort` * Replaced `checkpoint.exposure` with `getNonNettedLongs()` * Fixed the remaining tests * Polished and fixed the rust tests * `BaseBufferExceedsShareReserves` => `Insolvency` * Cleaned up some stack-cycling in `closeShort` * Removed the `precisionThreshold` parameter * Addressed review feedback from @jrhea --- contracts/src/external/HyperdriveTarget0.sol | 12 +- contracts/src/interfaces/IHyperdrive.sol | 10 +- contracts/src/interfaces/IHyperdriveRead.sol | 4 + contracts/src/internal/HyperdriveBase.sol | 112 +++++-------- .../src/internal/HyperdriveCheckpoint.sol | 16 +- contracts/src/internal/HyperdriveLong.sol | 51 ++---- contracts/src/internal/HyperdriveShort.sol | 75 +++------ contracts/src/internal/HyperdriveStorage.sol | 9 +- contracts/test/MockMultiToken.sol | 2 - crates/hyperdrive-math/src/lib.rs | 1 - crates/hyperdrive-math/src/long/max.rs | 24 +-- crates/hyperdrive-math/src/short/max.rs | 5 +- .../tests/integration_tests.rs | 5 +- crates/test-utils/src/agent.rs | 19 ++- crates/test-utils/src/chain/test_chain.rs | 1 - crates/test-utils/src/crash_reports.rs | 158 +----------------- script/DevnetMigration.s.sol | 6 - test/integrations/ERC4626Hyperdrive.t.sol | 3 - test/integrations/HyperdriveFactory.t.sol | 1 - .../IntraCheckpointNettingTest.t.sol | 14 +- .../hyperdrive/NonstandardDecimals.sol | 2 +- test/units/ForceRevertDelegatecall.t.sol | 1 - test/units/hyperdrive/ExtremeInputs.t.sol | 9 +- test/units/hyperdrive/OpenLongTest.t.sol | 2 +- .../hyperdrive/RemoveLiquidityTest.t.sol | 15 +- test/units/libraries/HyperdriveMath.t.sol | 13 +- test/utils/HyperdriveTest.sol | 2 - test/utils/HyperdriveUtils.sol | 69 ++++---- 28 files changed, 176 insertions(+), 465 deletions(-) diff --git a/contracts/src/external/HyperdriveTarget0.sol b/contracts/src/external/HyperdriveTarget0.sol index 687baa41c..3489998b5 100644 --- a/contracts/src/external/HyperdriveTarget0.sol +++ b/contracts/src/external/HyperdriveTarget0.sol @@ -218,6 +218,17 @@ abstract contract HyperdriveTarget0 is _revert(abi.encode(_checkpoints[_checkpointId])); } + /// @notice Gets the checkpoint exposure at a specified time. + /// @param _checkpointTime The checkpoint time. + /// @return The checkpoint exposure. + function getCheckpointExposure( + uint256 _checkpointTime + ) external view returns (int256) { + _revert( + abi.encode(_nonNettedLongs(_checkpointTime + _positionDuration)) + ); + } + /// @notice Gets the pool's configuration parameters. /// @dev These parameters are immutable, so this should only need to be /// called once. @@ -236,7 +247,6 @@ abstract contract HyperdriveTarget0 is initialSharePrice: _initialSharePrice, minimumShareReserves: _minimumShareReserves, minimumTransactionAmount: _minimumTransactionAmount, - precisionThreshold: _precisionThreshold, positionDuration: _positionDuration, checkpointDuration: _checkpointDuration, timeStretch: _timeStretch, diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index c68adf9a9..95fa484af 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -123,9 +123,6 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { /// as well as the share price at closing of matured longs and /// shorts. uint128 sharePrice; - /// @dev If exposure is positive, then we have net long exposure, otherwise - /// we have net short exposure in the checkpoint. - int128 exposure; } struct WithdrawPool { @@ -159,9 +156,6 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { /// @dev The minimum amount of tokens that a position can be opened or /// closed with. uint256 minimumTransactionAmount; - /// @dev The amount of precision expected to lose due to exponentiation - /// implementation. - uint256 precisionThreshold; /// @dev The duration of a position prior to maturity. uint256 positionDuration; /// @dev The duration of a checkpoint. @@ -225,9 +219,6 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { /// ### Hyperdrive ### /// ################## error ApprovalFailed(); - // TODO: We should rename this so that it's clear that it pertains to - // solvency. - error BaseBufferExceedsShareReserves(); error BelowMinimumContribution(); error BelowMinimumShareReserves(); error InvalidApr(); @@ -241,6 +232,7 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken { error InvalidShareReserves(); error InvalidFeeAmounts(); error InvalidFeeDestination(); + error InsufficientLiquidity(); error NegativeInterest(); error NegativePresentValue(); error NoAssetsToWithdraw(); diff --git a/contracts/src/interfaces/IHyperdriveRead.sol b/contracts/src/interfaces/IHyperdriveRead.sol index 9884ac518..074a51ff1 100644 --- a/contracts/src/interfaces/IHyperdriveRead.sol +++ b/contracts/src/interfaces/IHyperdriveRead.sol @@ -11,6 +11,10 @@ interface IHyperdriveRead is IMultiTokenRead { uint256 _checkpointId ) external view returns (IHyperdrive.Checkpoint memory); + function getCheckpointExposure( + uint256 _checkpointTime + ) external view returns (int256); + function getWithdrawPool() external view diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index bd6941558..ab67555b2 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -228,6 +228,36 @@ abstract contract HyperdriveBase is HyperdriveStorage { ); } + /// @dev Gets the amount of non-netted longs with a given maturity. + /// @param _maturityTime The maturity time of the longs. + /// @return The amount of non-netted longs. This is a signed value that + /// can be negative. This is convenient for updating the long + /// exposure when closing positions. + function _nonNettedLongs( + uint256 _maturityTime + ) internal view returns (int256) { + // The amount of non-netted longs is the difference between the amount + // of longs and the amount of shorts with a given maturity time. If the + // difference is negative, the amount of non-netted longs is zero. + return + int256( + _totalSupply[ + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _maturityTime + ) + ] + ) - + int256( + _totalSupply[ + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _maturityTime + ) + ] + ); + } + /// @dev Gets the present value parameters from the current state. /// @param _sharePrice The current share price. /// @return presentValue The present value parameters. @@ -293,83 +323,19 @@ abstract contract HyperdriveBase is HyperdriveStorage { int256(_minimumShareReserves.mulDown(_sharePrice)); } - /// @dev Calculates the checkpoint exposure when a position is closed - /// @param _bondAmount The amount of bonds that the user is closing. - /// @param _shareCurveDelta The amount of shares the trader pays the curve. - /// @param _bondReservesDelta The amount of bonds that the reserves will - /// change by. - /// @param _shareReservesDelta The amount of shares that the reserves will - /// change by. - /// @param _maturityTime The maturity time of the position being closed. - /// @param _sharePrice The current share price. - /// @param _isLong True if the position being closed is long. - function _updateCheckpointExposureOnClose( - uint256 _bondAmount, - uint256 _shareCurveDelta, - uint256 _bondReservesDelta, - uint256 _shareReservesDelta, - uint256 _maturityTime, - uint256 _sharePrice, - bool _isLong - ) internal { - uint256 checkpointTime = _maturityTime - _positionDuration; - uint256 checkpointLongs = _totalSupply[ - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _maturityTime) - ]; - uint256 checkpointShorts = _totalSupply[ - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _maturityTime) - ]; - - // We can zero out exposure when there are no more open positions - if (checkpointLongs == 0 && checkpointShorts == 0) { - _checkpoints[checkpointTime].exposure = 0; - } else { - // The exposure delta is flat + curve amount + the bonds the - // user is closing: - // - // (dz_user*c - dz*c) + (dy - dz*c) + dy_user - // = dz_user*c + dy - 2*dz*c + dy_user - int128 delta = int128( - (_shareReservesDelta.mulDown(_sharePrice) + - _bondReservesDelta - - 2 * - _shareCurveDelta.mulDown(_sharePrice) + - _bondAmount).toUint128() - ); - - // If the position being closed is long, then the exposure - // decreases by the delta. If it's short, then the exposure - // increases by the delta. - if (_isLong) { - _checkpoints[checkpointTime].exposure -= delta; - } else { - _checkpoints[checkpointTime].exposure += delta; - } - } - } - /// @dev Updates the global long exposure. /// @param _before The long exposure before the update. /// @param _after The long exposure after the update. function _updateLongExposure(int256 _before, int256 _after) internal { - // LongExposure is decreasing (OpenShort/CloseLong) - if (_before > _after && _before >= 0) { - int256 delta = int256(_before - _after.max(0)); - // Since the longExposure can't be negative, we need to make sure we - // don't underflow. - _marketState.longExposure -= uint128( - delta.min(int128(_marketState.longExposure)).toInt128() - ); - } - // LongExposure is increasing (OpenLong/CloseShort) - else if (_after > _before) { - if (_before >= 0) { - _marketState.longExposure += uint128( - _after.toInt128() - _before.toInt128() - ); - } else { - _marketState.longExposure += uint128(_after.max(0).toInt128()); - } + // The global long exposure is the sum of the non-netted longs in each + // checkpoint. To update this value, we subtract the current value + // (`_before.max(0)`) and add the new value (`_after.max(0)`). + int128 delta = (int256(_after.max(0)) - int256(_before.max(0))) + .toInt128(); + if (delta > 0) { + _marketState.longExposure += uint128(delta); + } else if (delta < 0) { + _marketState.longExposure -= uint128(-delta); } } diff --git a/contracts/src/internal/HyperdriveCheckpoint.sol b/contracts/src/internal/HyperdriveCheckpoint.sol index bdbd9dcea..9ff663fdf 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -21,6 +21,7 @@ abstract contract HyperdriveCheckpoint is HyperdriveShort { using FixedPointMath for uint256; + using FixedPointMath for int256; using SafeCast for uint256; /// @dev Attempts to mint a checkpoint with the specified checkpoint time. @@ -149,16 +150,15 @@ abstract contract HyperdriveCheckpoint is positionsClosed = true; } - // Update the checkpoint exposure and global long exposure. + // If we closed any positions, update the global long exposure and + // distribute any excess idle to the withdrawal pool. if (positionsClosed) { - uint256 maturityTime = _checkpointTime - _positionDuration; - int128 checkpointExposureBefore = int128( - _checkpoints[maturityTime].exposure - ); - _checkpoints[maturityTime].exposure = 0; + // Update the global long exposure. Since we've closed some matured + // positions, we can reduce the long exposure for the matured + // checkpoint to zero. _updateLongExposure( - checkpointExposureBefore, - _checkpoints[maturityTime].exposure + int256(maturedLongsAmount) - int256(maturedShortsAmount), + 0 ); // Distribute the excess idle to the withdrawal pool. diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 3226001a6..ea31183d7 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -96,7 +96,6 @@ abstract contract HyperdriveLong is HyperdriveLP { bondProceeds, bondReservesDelta, sharePrice, - latestCheckpoint, maturityTime ); @@ -158,7 +157,6 @@ abstract contract HyperdriveLong is HyperdriveLP { uint256 bondReservesDelta, uint256 shareProceeds, uint256 shareReservesDelta, - uint256 shareCurveDelta, int256 shareAdjustmentDelta, uint256 totalGovernanceFee ) = _calculateCloseLong(_bondAmount, sharePrice, _maturityTime); @@ -178,23 +176,12 @@ abstract contract HyperdriveLong is HyperdriveLP { maturityTime ); - // Update the checkpoint exposure and global long exposure. - uint256 checkpointTime = maturityTime - _positionDuration; - int128 checkpointExposureBefore = int128( - _checkpoints[checkpointTime].exposure - ); - _updateCheckpointExposureOnClose( - _bondAmount, - shareCurveDelta, - bondReservesDelta, - shareReservesDelta, - maturityTime, - sharePrice, - true - ); + // Update the global long exposure. Since we're closing a long, the + // number of non-netted longs decreases by the bond amount. + int256 nonNettedLongs = _nonNettedLongs(maturityTime); _updateLongExposure( - checkpointExposureBefore, - _checkpoints[checkpointTime].exposure + nonNettedLongs + int256(_bondAmount), + nonNettedLongs ); // Distribute the excess idle to the withdrawal pool. @@ -238,14 +225,12 @@ abstract contract HyperdriveLong is HyperdriveLP { /// @param _bondProceeds The amount of bonds purchased by the trader. /// @param _bondReservesDelta The amount of bonds sold by the curve. /// @param _sharePrice The share price. - /// @param _checkpointTime The time of the latest checkpoint. /// @param _maturityTime The maturity time of the long. function _applyOpenLong( uint256 _shareReservesDelta, uint256 _bondProceeds, uint256 _bondReservesDelta, uint256 _sharePrice, - uint256 _checkpointTime, uint256 _maturityTime ) internal { // Update the average maturity time of long positions. @@ -268,23 +253,17 @@ abstract contract HyperdriveLong is HyperdriveLP { longsOutstanding_ += _bondProceeds.toUint128(); _marketState.longsOutstanding = longsOutstanding_; - // Increase the exposure by the amount the LPs must reserve to cover the - // long. We are overly conservative, so this is equal to the amount of - // fixed interest the long is owed at maturity plus the face value of - // the long. - IHyperdrive.Checkpoint storage checkpoint = _checkpoints[ - _checkpointTime - ]; - int128 checkpointExposureBefore = int128(checkpoint.exposure); - uint128 exposureDelta = (2 * - _bondProceeds - - _shareReservesDelta.mulDown(_sharePrice)).toUint128(); - checkpoint.exposure += int128(exposureDelta); - _updateLongExposure(checkpointExposureBefore, checkpoint.exposure); + // Update the global long exposure. Since we're opening a long, the + // number of non-netted longs increases by the bond amount. + int256 nonNettedLongs = _nonNettedLongs(_maturityTime); + _updateLongExposure( + nonNettedLongs, + nonNettedLongs + int256(_bondProceeds) + ); // We need to check solvency because longs increase the system's exposure. if (!_isSolvent(_sharePrice)) { - revert IHyperdrive.BaseBufferExceedsShareReserves(); + revert IHyperdrive.InsufficientLiquidity(); } // Distribute the excess idle to the withdrawal pool. @@ -467,8 +446,6 @@ abstract contract HyperdriveLong is HyperdriveLP { /// @return bondReservesDelta The bonds added to the reserves. /// @return shareProceeds The proceeds in shares of selling the bonds. /// @return shareReservesDelta The shares removed from the reserves. - /// @return shareCurveDelta The curve portion of the payment that LPs need - /// to make to the trader in shares. /// @return shareAdjustmentDelta The change in the share adjustment. /// @return totalGovernanceFee The governance fee in shares. function _calculateCloseLong( @@ -482,7 +459,6 @@ abstract contract HyperdriveLong is HyperdriveLP { uint256 bondReservesDelta, uint256 shareProceeds, uint256 shareReservesDelta, - uint256 shareCurveDelta, int256 shareAdjustmentDelta, uint256 totalGovernanceFee ) @@ -490,6 +466,7 @@ abstract contract HyperdriveLong is HyperdriveLP { // Calculate the effect that closing the long should have on the pool's // reserves as well as the amount of shares the trader receives for // selling their bonds. + uint256 shareCurveDelta; { // Calculate the effect that closing the long should have on the // pool's reserves as well as the amount of shares the trader diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 0ee65f47f..78cba51d9 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -94,7 +94,6 @@ abstract contract HyperdriveShort is HyperdriveLP { // openLong. _applyOpenShort( _bondAmount, - baseDeposit, shareReservesDelta, sharePrice, maturityTime @@ -157,7 +156,6 @@ abstract contract HyperdriveShort is HyperdriveLP { uint256 bondReservesDelta, uint256 shareProceeds, uint256 shareReservesDelta, - uint256 shareCurveDelta, int256 shareAdjustmentDelta, uint256 totalGovernanceFee ) = _calculateCloseShort(_bondAmount, sharePrice, _maturityTime); @@ -165,43 +163,29 @@ abstract contract HyperdriveShort is HyperdriveLP { // If the position hasn't matured, apply the accounting updates that // result from closing the short to the reserves and pay out the // withdrawal pool if necessary. - uint256 bondAmount = _bondAmount; // Avoid stack too deep error. - uint256 maturityTime = _maturityTime; // Avoid stack too deep error. - uint256 sharePrice_ = sharePrice; // Avoid stack too deep error. - if (block.timestamp < maturityTime) { + if (block.timestamp < _maturityTime) { // Attribute the governance fees. _governanceFeesAccrued += totalGovernanceFee; // Update the pool's state to account for the short being closed. _applyCloseShort( - bondAmount, + _bondAmount, bondReservesDelta, shareReservesDelta, shareAdjustmentDelta, - maturityTime + _maturityTime ); - // Update the checkpoint exposure and global long exposure. - uint256 checkpointTime = maturityTime - _positionDuration; - int128 checkpointExposureBefore = int128( - _checkpoints[checkpointTime].exposure - ); - _updateCheckpointExposureOnClose( - bondAmount, - shareCurveDelta, - bondReservesDelta, - shareReservesDelta, - maturityTime, - sharePrice_, - false - ); + // Update the global long exposure. Since we're closing a short, the + // number of non-netted longs increases by the bond amount. + int256 nonNettedLongs = _nonNettedLongs(_maturityTime); _updateLongExposure( - checkpointExposureBefore, - _checkpoints[checkpointTime].exposure + nonNettedLongs - int256(_bondAmount), + nonNettedLongs ); // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(sharePrice_); + _distributeExcessIdle(sharePrice); } // Withdraw the profit to the trader. This includes the proceeds from @@ -214,23 +198,24 @@ abstract contract HyperdriveShort is HyperdriveLP { // withdraw to check against the minOutput because // in the event of slippage on the withdraw, we want // it to be caught be the minOutput check. - IHyperdrive.Options calldata options = _options; // Avoid stack too deep error. uint256 baseProceeds = _convertToBaseFromOption( proceeds, sharePrice, - options + _options ); if (baseProceeds < _minOutput) { revert IHyperdrive.OutputLimit(); } // Emit a CloseShort event. + uint256 bondAmount = _bondAmount; // Avoid stack too deep error. + uint256 maturityTime = _maturityTime; // Avoid stack too deep error. emit CloseShort( _options.destination, AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime), maturityTime, baseProceeds, - sharePrice_, + sharePrice, bondAmount ); @@ -240,13 +225,11 @@ abstract contract HyperdriveShort is HyperdriveLP { /// @dev Applies an open short to the state. This includes updating the /// reserves and maintaining the reserve invariants. /// @param _bondAmount The amount of bonds shorted. - /// @param _baseDeposit The deposit, in base, required to open the short. /// @param _shareReservesDelta The amount of shares paid to the curve. /// @param _sharePrice The share price. /// @param _maturityTime The maturity time of the long. function _applyOpenShort( uint256 _bondAmount, - uint256 _baseDeposit, uint256 _shareReservesDelta, uint256 _sharePrice, uint256 _maturityTime @@ -289,28 +272,12 @@ abstract contract HyperdriveShort is HyperdriveLP { revert IHyperdrive.InvalidShareReserves(); } - // Update the checkpoint's short deposits and decrease the exposure. - uint256 _latestCheckpoint = _latestCheckpoint(); - int128 checkpointExposureBefore = int128( - _checkpoints[_latestCheckpoint].exposure - ); - - // Round the base deposit down to the nearest multiple of the precision - // threshold. We specifically round down because there are cases where - // a smaller short deposit is larger than a larger long's fixed interest. - // This happens because exponentiation in FixedPointMath is only accurate - // to 1e14. The result of this innacuracy is that it causes a solvency issue - // once all LPs have withdrawn, all positions mature, and the shorts have - // been closed. When applyCheckpoint is closing the longs, it results in an - // underflow bc there aren't enough shareReserves available. - _baseDeposit = _baseDeposit - (_baseDeposit % _precisionThreshold); - uint256 exposureDelta = _baseDeposit + _bondAmount; - _checkpoints[_latestCheckpoint].exposure -= int128( - exposureDelta.toUint128() - ); + // Update the global long exposure. Since we're opening a short, the + // number of non-netted longs decreases by the bond amount. + int256 nonNettedLongs = _nonNettedLongs(_maturityTime); _updateLongExposure( - checkpointExposureBefore, - _checkpoints[_latestCheckpoint].exposure + nonNettedLongs, + nonNettedLongs - int256(_bondAmount) ); // Opening a short decreases the system's exposure because the short's @@ -319,7 +286,7 @@ abstract contract HyperdriveShort is HyperdriveLP { // of capital available to back non-netted long exposure. Since both // quantities decrease, we need to check that the system is still solvent. if (!_isSolvent(_sharePrice)) { - revert IHyperdrive.BaseBufferExceedsShareReserves(); + revert IHyperdrive.InsufficientLiquidity(); } // Distribute the excess idle to the withdrawal pool. @@ -470,8 +437,6 @@ abstract contract HyperdriveShort is HyperdriveLP { /// @return bondReservesDelta The change in the bond reserves. /// @return shareProceeds The proceeds in shares of closing the short. /// @return shareReservesDelta The shares added to the reserves. - /// @return shareCurveDelta The curve portion of the proceeds that LPs - /// receive from the trader in shares. /// @return shareAdjustmentDelta The change in the share adjustment. /// @return totalGovernanceFee The governance fee in shares. function _calculateCloseShort( @@ -485,7 +450,6 @@ abstract contract HyperdriveShort is HyperdriveLP { uint256 bondReservesDelta, uint256 shareProceeds, uint256 shareReservesDelta, - uint256 shareCurveDelta, int256 shareAdjustmentDelta, uint256 totalGovernanceFee ) @@ -493,6 +457,7 @@ abstract contract HyperdriveShort is HyperdriveLP { // Calculate the effect that closing the short should have on the pool's // reserves as well as the amount of shares the trader pays to buy the // bonds that they shorted back at the market price. + uint256 shareCurveDelta; { // Calculate the effect that closing the short should have on the // pool's reserves as well as the amount of shares the trader needs diff --git a/contracts/src/internal/HyperdriveStorage.sol b/contracts/src/internal/HyperdriveStorage.sol index 8837e8fd1..c777910d2 100644 --- a/contracts/src/internal/HyperdriveStorage.sol +++ b/contracts/src/internal/HyperdriveStorage.sol @@ -58,10 +58,6 @@ abstract contract HyperdriveStorage is ReentrancyGuard { /// closed with. uint256 internal immutable _minimumTransactionAmount; - /// @dev The amount of precision expected to lose due to exponentiation - /// implementation. - uint256 internal immutable _precisionThreshold; - /// @dev The state of the market. This includes the reserves, buffers, and /// other data used to price trades and maintain solvency. IHyperdrive.MarketState internal _marketState; @@ -140,8 +136,11 @@ abstract contract HyperdriveStorage is ReentrancyGuard { } _minimumShareReserves = _config.minimumShareReserves; + // Initialize the minimum transaction amount. The minimum transaction + // amount defines the minimum input that the system will allow, which + // prevents weird rounding issues that can occur with very small + // amounts. _minimumTransactionAmount = _config.minimumTransactionAmount; - _precisionThreshold = _config.precisionThreshold; // Initialize the time configurations. There must be at least one // checkpoint per term to avoid having a position duration of zero. diff --git a/contracts/test/MockMultiToken.sol b/contracts/test/MockMultiToken.sol index abb2b390d..5480e7607 100644 --- a/contracts/test/MockMultiToken.sol +++ b/contracts/test/MockMultiToken.sol @@ -55,7 +55,6 @@ contract MockMultiToken is HyperdriveMultiToken, MockHyperdriveBase { initialSharePrice: 1e18, minimumShareReserves: 1e18, minimumTransactionAmount: 1e15, - precisionThreshold: 1e14, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), @@ -75,7 +74,6 @@ contract MockMultiToken is HyperdriveMultiToken, MockHyperdriveBase { initialSharePrice: 1e18, minimumShareReserves: 1e18, minimumTransactionAmount: 1e15, - precisionThreshold: 1e14, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), diff --git a/crates/hyperdrive-math/src/lib.rs b/crates/hyperdrive-math/src/lib.rs index 7c8b53903..ba083b90a 100644 --- a/crates/hyperdrive-math/src/lib.rs +++ b/crates/hyperdrive-math/src/lib.rs @@ -40,7 +40,6 @@ impl Distribution for Standard { initial_share_price: rng.gen_range(fixed!(0.5e18)..=fixed!(2.5e18)).into(), minimum_share_reserves: rng.gen_range(fixed!(0.1e18)..=fixed!(1e18)).into(), minimum_transaction_amount: rng.gen_range(fixed!(0.1e18)..=fixed!(1e18)).into(), - precision_threshold: rng.gen_range(fixed!(0.1e18)..=fixed!(1e18)).into(), time_stretch: rng.gen_range(fixed!(0.005e18)..=fixed!(0.5e18)).into(), position_duration: rng .gen_range( diff --git a/crates/hyperdrive-math/src/long/max.rs b/crates/hyperdrive-math/src/long/max.rs index 1f4151f12..94638415a 100644 --- a/crates/hyperdrive-math/src/long/max.rs +++ b/crates/hyperdrive-math/src/long/max.rs @@ -313,20 +313,11 @@ impl State { /// \Delta z = \tfrac{x - g(x)}{c} /// $$ /// - /// In the solidity implementation, we calculate the delta in the exposure - /// as: - /// - /// ```solidity - /// shareReservesDelta = _shareAmount - governanceCurveFee.divDown(_sharePrice); - /// uint128 exposureDelta = (2 * - /// _bondProceeds - - /// _shareReservesDelta.mulDown(_sharePrice)).toUint128(); - /// ``` - /// - /// From this, we can calculate our exposure as: + /// Opening the long increases the non-netted longs by the bond amount. From + /// this, the change in the exposure is given by: /// /// $$ - /// \Delta exposure = 2 \cdot y(x) - x + g(x) + /// \Delta exposure = y(x) /// $$ /// /// From this, we can calculate $S(x)$ as: @@ -349,8 +340,7 @@ impl State { let governance_fee = self.open_long_governance_fee(base_amount); let share_reserves = self.share_reserves() + base_amount / self.share_price() - governance_fee / self.share_price(); - let exposure = - self.long_exposure() + fixed!(2e18) * bond_amount - base_amount + governance_fee; + let exposure = self.long_exposure() + bond_amount; let checkpoint_exposure = FixedPoint::from(-checkpoint_exposure.min(int256!(0))); if share_reserves + checkpoint_exposure / self.share_price() >= exposure / self.share_price() + self.minimum_share_reserves() @@ -372,8 +362,8 @@ impl State { /// amount that the long pays is given by: /// /// $$ - /// S'(x) = \tfrac{2}{c} \cdot \left( 1 - y'(x) - \phi_{g} \cdot p \cdot c'(x) \right) \\ - /// = \tfrac{2}{c} \cdot \left( + /// S'(x) = \tfrac{1}{c} \cdot \left( 1 - y'(x) - \phi_{g} \cdot p \cdot c'(x) \right) \\ + /// = \tfrac{1}{c} \cdot \left( /// 1 - y'(x) - \phi_{g} \cdot \phi_{c} \cdot \left( 1 - p \right) /// \right) /// $$ @@ -387,7 +377,7 @@ impl State { (derivative + self.governance_fee() * self.curve_fee() * (fixed!(1e18) - self.get_spot_price()) - fixed!(1e18)) - .mul_div_down(fixed!(2e18), self.share_price()) + .mul_div_down(fixed!(1e18), self.share_price()) }) } diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index 6c8613ab6..1aaa08a02 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -633,11 +633,12 @@ mod tests { let state = alice.get_state().await?; let Checkpoint { share_price: open_share_price, - exposure: checkpoint_exposure, - .. } = alice .get_checkpoint(state.to_checkpoint(alice.now().await?)) .await?; + let checkpoint_exposure = alice + .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) + .await?; let global_max_short = state.get_max_short(U256::MAX, open_share_price, checkpoint_exposure, None, None); diff --git a/crates/hyperdrive-math/tests/integration_tests.rs b/crates/hyperdrive-math/tests/integration_tests.rs index 96de0707a..4e70c5c3a 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -118,11 +118,12 @@ pub async fn test_integration_get_max_short() -> Result<()> { let state = alice.get_state().await?; let Checkpoint { share_price: open_share_price, - exposure: checkpoint_exposure, - .. } = alice .get_checkpoint(state.to_checkpoint(alice.now().await?)) .await?; + let checkpoint_exposure = alice + .get_checkpoint_exposure(state.to_checkpoint(alice.now().await?)) + .await?; let global_max_short = state.get_max_short(U256::MAX, open_share_price, checkpoint_exposure, None, None); let budget = bob.base(); diff --git a/crates/test-utils/src/agent.rs b/crates/test-utils/src/agent.rs index 3d283cd28..8e78205cb 100644 --- a/crates/test-utils/src/agent.rs +++ b/crates/test-utils/src/agent.rs @@ -5,7 +5,7 @@ use ethers::{ contract::ContractCall, prelude::EthLogDecode, providers::{Http, Middleware, Provider, RetryClient}, - types::{Address, BlockId, U256}, + types::{Address, BlockId, I256, U256}, }; use eyre::Result; use fixed_point::FixedPoint; @@ -921,6 +921,11 @@ impl Agent { Ok(self.hyperdrive.get_checkpoint(id).await?) } + /// Gets the checkpoint exposure. + pub async fn get_checkpoint_exposure(&self, id: U256) -> Result { + Ok(self.hyperdrive.get_checkpoint_exposure(id).await?) + } + /// Gets the spot price. pub async fn get_spot_price(&self) -> Result { Ok(self.get_state().await?.get_spot_price()) @@ -954,11 +959,11 @@ impl Agent { /// Gets the max long that can be opened in the current checkpoint. pub async fn get_max_long(&self, maybe_max_iterations: Option) -> Result { let state = self.get_state().await?; - let Checkpoint { exposure, .. } = self + let checkpoint_exposure = self .hyperdrive - .get_checkpoint(state.to_checkpoint(self.now().await?)) + .get_checkpoint_exposure(state.to_checkpoint(self.now().await?)) .await?; - Ok(state.get_max_long(self.wallet.base, exposure, maybe_max_iterations)) + Ok(state.get_max_long(self.wallet.base, checkpoint_exposure, maybe_max_iterations)) } /// Gets the max short that can be opened in the current checkpoint. @@ -976,12 +981,14 @@ impl Agent { let state = self.get_state().await?; let Checkpoint { share_price: open_share_price, - exposure: checkpoint_exposure, - .. } = self .hyperdrive .get_checkpoint(state.to_checkpoint(self.now().await?)) .await?; + let checkpoint_exposure = self + .hyperdrive + .get_checkpoint_exposure(state.to_checkpoint(self.now().await?)) + .await?; // We linearly interpolate between the current spot price and the minimum // price that the pool can support. This is a conservative estimate of diff --git a/crates/test-utils/src/chain/test_chain.rs b/crates/test-utils/src/chain/test_chain.rs index a7ca43cf7..d8b2f2bbd 100644 --- a/crates/test-utils/src/chain/test_chain.rs +++ b/crates/test-utils/src/chain/test_chain.rs @@ -266,7 +266,6 @@ impl TestChain { initial_share_price: uint256!(1e18), minimum_share_reserves: uint256!(10e18), minimum_transaction_amount: uint256!(0.001e18), - precision_threshold: uint256!(1e14), position_duration: U256::from(60 * 60 * 24 * 365), // 1 year checkpoint_duration: U256::from(60 * 60 * 24), // 1 day time_stretch: get_time_stretch(fixed!(0.05e18)).into(), // time stretch for 5% rate diff --git a/crates/test-utils/src/crash_reports.rs b/crates/test-utils/src/crash_reports.rs index bd07e1fec..53809796e 100644 --- a/crates/test-utils/src/crash_reports.rs +++ b/crates/test-utils/src/crash_reports.rs @@ -1,8 +1,7 @@ /// This module provides the `CrashReport` struct which implents `Deserialize`. /// It is intended to be used to deserialize crash reports from JSON. -use ethers::types::{Address, Bytes, H256, U256}; +use ethers::types::{Address, Bytes, U256}; use hyperdrive_addresses::Addresses; -use hyperdrive_wrappers::wrappers::i_hyperdrive::{Checkpoint, Fees, PoolConfig, PoolInfo}; use serde::{Deserialize, Deserializer}; #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] @@ -58,105 +57,6 @@ impl From for Trade { } } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawPoolConfig { - base_token: Address, - linker_factory: Address, - linker_code_hash: H256, - initial_share_price: u128, - minimum_share_reserves: u128, - minimum_transaction_amount: u128, - precision_threshold: u128, - position_duration: u64, - checkpoint_duration: u64, - time_stretch: u128, - governance: Address, - fee_collector: Address, - fees: Vec, -} - -impl From for PoolConfig { - fn from(r: RawPoolConfig) -> Self { - if r.fees.len() != 3 { - panic!("Expected 3 fees, got {}", r.fees.len()); - } - Self { - base_token: r.base_token, - linker_factory: r.linker_factory, - linker_code_hash: r.linker_code_hash.into(), - initial_share_price: r.initial_share_price.into(), - minimum_share_reserves: r.minimum_share_reserves.into(), - minimum_transaction_amount: r.minimum_transaction_amount.into(), - precision_threshold: r.precision_threshold.into(), - position_duration: r.position_duration.into(), - checkpoint_duration: r.checkpoint_duration.into(), - time_stretch: r.time_stretch.into(), - governance: r.governance, - fee_collector: r.fee_collector, - fees: Fees { - curve: r.fees[0].into(), - flat: r.fees[1].into(), - governance: r.fees[2].into(), - }, - } - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawPoolInfo { - share_reserves: u128, - share_adjustment: i128, - bond_reserves: u128, - lp_total_supply: u128, - share_price: u128, - longs_outstanding: u128, - long_average_maturity_time: u128, - shorts_outstanding: u128, - short_average_maturity_time: u128, - withdrawal_shares_ready_to_withdraw: u128, - withdrawal_shares_proceeds: u128, - lp_share_price: u128, - long_exposure: u128, -} - -impl From for PoolInfo { - fn from(r: RawPoolInfo) -> Self { - Self { - share_reserves: r.share_reserves.into(), - share_adjustment: r.share_adjustment.into(), - bond_reserves: r.bond_reserves.into(), - lp_total_supply: r.lp_total_supply.into(), - share_price: r.share_price.into(), - longs_outstanding: r.longs_outstanding.into(), - long_average_maturity_time: r.long_average_maturity_time.into(), - shorts_outstanding: r.shorts_outstanding.into(), - short_average_maturity_time: r.short_average_maturity_time.into(), - withdrawal_shares_ready_to_withdraw: r.withdrawal_shares_ready_to_withdraw.into(), - withdrawal_shares_proceeds: r.withdrawal_shares_proceeds.into(), - lp_share_price: r.lp_share_price.into(), - long_exposure: r.long_exposure.into(), - } - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RawCheckpoint { - share_price: u128, - exposure: i128, -} - -impl From for Checkpoint { - fn from(r: RawCheckpoint) -> Self { - Self { - share_price: r.share_price, - exposure: r.exposure, - } - } -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct CrashReport { /// Crash Metadata @@ -171,10 +71,6 @@ pub struct CrashReport { pub addresses: Addresses, pub agent_info: AgentInfo, pub trade: Trade, - /// Pool Context - pub pool_config: PoolConfig, - pub pool_info: PoolInfo, - pub checkpoint: Checkpoint, /// State Dump pub state_dump: Bytes, } @@ -195,13 +91,6 @@ struct RawCrashReport { agent_info: AgentInfo, #[serde(rename = "raw_trade_object")] trade: RawTrade, - // Pool Context - #[serde(rename = "raw_pool_config")] - pool_config: RawPoolConfig, - #[serde(rename = "raw_pool_info")] - pool_info: RawPoolInfo, - #[serde(rename = "raw_checkpoint")] - checkpoint: RawCheckpoint, // State Dump #[serde(rename = "anvil_dump_state")] state_dump: Bytes, @@ -222,10 +111,6 @@ impl From for CrashReport { addresses: r.addresses, agent_info: r.agent_info, trade: r.trade.into(), - // Pool Context - pool_config: r.pool_config.into(), - pool_info: r.pool_info.into(), - checkpoint: r.checkpoint.into(), // State Dump state_dump: r.state_dump, } @@ -285,7 +170,6 @@ mod tests { "initialSharePrice": 1000000000000000000, "minimumShareReserves": 10000000000000000000, "minimumTransactionAmount": 1000000000000000, - "precisionThreshold" : 10000000000000, "positionDuration": 604800, "checkpointDuration": 3600, "timeStretch": 44463125629060298, @@ -354,7 +238,6 @@ mod tests { "initialSharePrice": "1.0", "minimumShareReserves": "10.0", "minimumTransactionAmount": "0.001", - "precisionThreshold": "0.0001", "positionDuration": 604800, "checkpointDuration": 3600, "timeStretch": "0.044463125629060298", @@ -428,45 +311,6 @@ mod tests { slippage_tolerance: None, maturity_time: 604800 }, - // Pool Context - pool_config: PoolConfig { - base_token: "0x5FbDB2315678afecb367f032d93F642f64180aa3".parse()?, - linker_factory: "0x33027547537D35728a741470dF1CCf65dE10b454".parse()?, - linker_code_hash: "0x33027547537d35728a741470df1ccf65de10b454ca0def7c5c20b257b7b8d161".parse::()?.into(), - initial_share_price: uint256!(1000000000000000000), - minimum_share_reserves: uint256!(10000000000000000000), - minimum_transaction_amount: uint256!(1000000000000000), - precision_threshold: uint256!(10000000000000), - position_duration: uint256!(604800), - checkpoint_duration: uint256!(3600), - time_stretch: uint256!(44463125629060298), - governance: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse()?, - fee_collector: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse()?, - fees: Fees { - curve: uint256!(100000000000000000), - flat: uint256!(500000000000000), - governance: uint256!(150000000000000000), - }, - }, - pool_info: PoolInfo { - share_reserves: uint256!(100000000000000000000000000), - share_adjustment: int256!(0), - bond_reserves: uint256!(102178995195337961200000000), - lp_total_supply: uint256!(99999990000000000000000000), - share_price: uint256!(1000000006341958396), - longs_outstanding: uint256!(0), - long_average_maturity_time: uint256!(0), - shorts_outstanding: uint256!(0), - short_average_maturity_time: uint256!(0), - withdrawal_shares_ready_to_withdraw: uint256!(0), - withdrawal_shares_proceeds: uint256!(0), - lp_share_price: uint256!(1000000006341958396), - long_exposure: uint256!(0) - }, - checkpoint: Checkpoint { - share_price: 1000000000000000000, - exposure: 0, - }, // State Dump state_dump: "0x7b22".parse()?, }, diff --git a/script/DevnetMigration.s.sol b/script/DevnetMigration.s.sol index d6139f610..6977b3444 100644 --- a/script/DevnetMigration.s.sol +++ b/script/DevnetMigration.s.sol @@ -57,7 +57,6 @@ contract DevnetMigration is Script { uint256 hyperdriveInitialSharePrice; uint256 hyperdriveMinimumShareReserves; uint256 hyperdriveMinimumTransactionAmount; - uint256 hyperdrivePrecisionThreshold; uint256 hyperdrivePositionDuration; uint256 hyperdriveCheckpointDuration; uint256 hyperdriveTimeStretchApr; @@ -126,10 +125,6 @@ contract DevnetMigration is Script { "HYPERDRIVE_MINIMUM_TRANSACTION_AMOUNT", uint256(0.001e18) ), - hyperdrivePrecisionThreshold: vm.envOr( - "HYPERDRIVE_PRECISION_THRESHOLD", - uint256(1e14) - ), hyperdrivePositionDuration: vm.envOr( "HYPERDRIVE_POSITION_DURATION", uint256(1 weeks) @@ -225,7 +220,6 @@ contract DevnetMigration is Script { minimumShareReserves: config.hyperdriveMinimumShareReserves, minimumTransactionAmount: config .hyperdriveMinimumTransactionAmount, - precisionThreshold: config.hyperdrivePrecisionThreshold, positionDuration: config.hyperdrivePositionDuration, checkpointDuration: config.hyperdriveCheckpointDuration, timeStretch: config diff --git a/test/integrations/ERC4626Hyperdrive.t.sol b/test/integrations/ERC4626Hyperdrive.t.sol index 723ed3c3d..26421ad25 100644 --- a/test/integrations/ERC4626Hyperdrive.t.sol +++ b/test/integrations/ERC4626Hyperdrive.t.sol @@ -94,7 +94,6 @@ contract ERC4626HyperdriveTest is HyperdriveTest { initialSharePrice: ONE, minimumShareReserves: ONE, minimumTransactionAmount: 0.001e18, - precisionThreshold: PRECISION_THRESHOLD, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: ONE.divDown(22.186877016851916266e18), @@ -212,7 +211,6 @@ contract ERC4626HyperdriveTest is HyperdriveTest { initialSharePrice: ONE, minimumShareReserves: ONE, minimumTransactionAmount: 0.001e18, - precisionThreshold: PRECISION_THRESHOLD, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(apr), @@ -264,7 +262,6 @@ contract ERC4626HyperdriveTest is HyperdriveTest { initialSharePrice: ONE, minimumShareReserves: ONE, minimumTransactionAmount: 0.001e18, - precisionThreshold: PRECISION_THRESHOLD, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(apr), diff --git a/test/integrations/HyperdriveFactory.t.sol b/test/integrations/HyperdriveFactory.t.sol index 40e44b6d0..4ac8737fb 100644 --- a/test/integrations/HyperdriveFactory.t.sol +++ b/test/integrations/HyperdriveFactory.t.sol @@ -150,7 +150,6 @@ contract HyperdriveFactoryBaseTest is HyperdriveTest { initialSharePrice: 1e18, minimumShareReserves: 1e18, minimumTransactionAmount: 1e15, - precisionThreshold: 1e14, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(APR), diff --git a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol index 44b5cbe10..a0bf771a4 100644 --- a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol +++ b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol @@ -749,16 +749,12 @@ contract IntraCheckpointNettingTest is HyperdriveTest { shortMaturityTimes[i] = maturityTimeShort; } - // Checkpoint Exposure should be small even if there are many trades - int256 checkpointExposure = int256( - hyperdrive - .getCheckpoint(HyperdriveUtils.latestCheckpoint(hyperdrive)) - .exposure + // The amount of non-netted longs should be equal to zero since the + // bond amounts cancel out. + assertEq( + hyperdrive.getCheckpointExposure(hyperdrive.latestCheckpoint()), + 0 ); - checkpointExposure = checkpointExposure < 0 - ? -checkpointExposure - : checkpointExposure; - assertLe(uint256(checkpointExposure), PRECISION_THRESHOLD * numTrades); // fast forward time, create checkpoints and accrue interest advanceTimeWithCheckpoints(timeElapsed, variableInterest); diff --git a/test/integrations/hyperdrive/NonstandardDecimals.sol b/test/integrations/hyperdrive/NonstandardDecimals.sol index 6764282fb..d941f91e9 100644 --- a/test/integrations/hyperdrive/NonstandardDecimals.sol +++ b/test/integrations/hyperdrive/NonstandardDecimals.sol @@ -525,7 +525,7 @@ contract NonstandardDecimalsTest is HyperdriveTest { aliceLpShares, aliceLpShares + bobLpShares ); - assertGe(aliceRedeemProceeds, estimatedRedeemProceeds); + assertGe(aliceRedeemProceeds + 10, estimatedRedeemProceeds); } // Bob and Celine remove their liquidity. Bob should receive more base diff --git a/test/units/ForceRevertDelegatecall.t.sol b/test/units/ForceRevertDelegatecall.t.sol index 40eefd0ca..5a33e1360 100644 --- a/test/units/ForceRevertDelegatecall.t.sol +++ b/test/units/ForceRevertDelegatecall.t.sol @@ -34,7 +34,6 @@ contract DummyHyperdrive is Hyperdrive, MockHyperdriveBase { initialSharePrice: 1e18, minimumShareReserves: 1e18, minimumTransactionAmount: 1e15, - precisionThreshold: 1e14, positionDuration: 365 days, checkpointDuration: 1 days, timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18), diff --git a/test/units/hyperdrive/ExtremeInputs.t.sol b/test/units/hyperdrive/ExtremeInputs.t.sol index f4cf4ab5d..d44a06036 100644 --- a/test/units/hyperdrive/ExtremeInputs.t.sol +++ b/test/units/hyperdrive/ExtremeInputs.t.sol @@ -185,11 +185,8 @@ contract ExtremeInputs is HyperdriveTest { // Bob attempts to short exactly the maximum amount of bonds needed for // the share reserves to be equal to zero. This should fail because the // share reserves fall below the minimum share reserves. - IHyperdrive.Checkpoint memory checkpoint = hyperdrive.getCheckpoint( - hyperdrive.latestCheckpoint() - ); - IHyperdrive.PoolInfo memory poolInfo = hyperdrive.getPoolInfo(); IHyperdrive.PoolConfig memory poolConfig = hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory poolInfo = hyperdrive.getPoolInfo(); targetReserves = targetReserves.normalizeToRange( config.minimumTransactionAmount, poolConfig.minimumShareReserves - 1 @@ -209,12 +206,12 @@ contract ExtremeInputs is HyperdriveTest { flatFee: poolConfig.fees.flat, governanceFee: poolConfig.fees.governance }), - checkpoint.exposure, + hyperdrive.getCheckpointExposure(hyperdrive.latestCheckpoint()), 7 ); baseToken.mint(shortAmount); baseToken.approve(address(hyperdrive), shortAmount); - vm.expectRevert(IHyperdrive.BaseBufferExceedsShareReserves.selector); + vm.expectRevert(IHyperdrive.InsufficientLiquidity.selector); hyperdrive.openShort( shortAmount, type(uint256).max, diff --git a/test/units/hyperdrive/OpenLongTest.t.sol b/test/units/hyperdrive/OpenLongTest.t.sol index 783f8140e..3cd94e30e 100644 --- a/test/units/hyperdrive/OpenLongTest.t.sol +++ b/test/units/hyperdrive/OpenLongTest.t.sol @@ -268,7 +268,7 @@ contract OpenLongTest is HyperdriveTest { baseToken.mint(longAmount); baseToken.approve(address(hyperdrive), longAmount); - vm.expectRevert(IHyperdrive.BaseBufferExceedsShareReserves.selector); + vm.expectRevert(IHyperdrive.InsufficientLiquidity.selector); hyperdrive.openLong( longAmount, 0, diff --git a/test/units/hyperdrive/RemoveLiquidityTest.t.sol b/test/units/hyperdrive/RemoveLiquidityTest.t.sol index 03d51da49..4910731ed 100644 --- a/test/units/hyperdrive/RemoveLiquidityTest.t.sol +++ b/test/units/hyperdrive/RemoveLiquidityTest.t.sol @@ -100,7 +100,7 @@ contract RemoveLiquidityTest is HyperdriveTest { // Remove the intializer's liquidity and verify that the state was // updated correctly. - _test_remove_liquidity(testCase, false, 0); + _test_remove_liquidity(testCase, 0); } function test_remove_liquidity_long_trade() external { @@ -137,7 +137,7 @@ contract RemoveLiquidityTest is HyperdriveTest { // Remove the intializer's liquidity and verify that the state was // updated correctly. - _test_remove_liquidity(testCase, true, 2); + _test_remove_liquidity(testCase, 2); } function test_remove_liquidity_short_trade() external { @@ -174,7 +174,7 @@ contract RemoveLiquidityTest is HyperdriveTest { // Remove the intializer's liquidity and verify that the state was // updated correctly. - _test_remove_liquidity(testCase, false, 3e7); // TODO: Reduce this bound. + _test_remove_liquidity(testCase, 3e7); // TODO: Reduce this bound. } /// Helpers /// @@ -200,15 +200,9 @@ contract RemoveLiquidityTest is HyperdriveTest { /// we ensure that they receive the correct amount of base and /// withdrawal shares. /// @param testCase The test case. - /// @param isLong True if the trade that was opened is a long and false - /// otherwise. The current netting implementation is overly - /// conservative, which results in double counting the margin that - /// LPs must reserve for LPs. This double counting isn't done in the - /// case of shorts. /// @param tolerance The error tolerance for imprecise assertions. function _test_remove_liquidity( TestCase memory testCase, - bool isLong, uint256 tolerance ) internal { // The LPs provided margins for all of the open trades. We can calculate @@ -225,9 +219,6 @@ contract RemoveLiquidityTest is HyperdriveTest { // delta idle = dy - dz *c = - dz * c + dy // new idle = old idle + delta idle (since dy > dz*c idle goes up) uint256 marginFactor = 1; - if (isLong) { - marginFactor = 2; - } uint256 margin = (testCase.longAmount - testCase.longBasePaid) + (testCase.shortAmount - testCase.shortBasePaid); uint256 remainingMargin = uint256(marginFactor * margin).mulDivDown( diff --git a/test/units/libraries/HyperdriveMath.t.sol b/test/units/libraries/HyperdriveMath.t.sol index 740151258..9cd31fb32 100644 --- a/test/units/libraries/HyperdriveMath.t.sol +++ b/test/units/libraries/HyperdriveMath.t.sol @@ -17,6 +17,8 @@ contract HyperdriveMathTest is HyperdriveTest { using HyperdriveUtils for IHyperdrive; using Lib for *; + uint256 internal constant PRECISION_THRESHOLD = 1e14; + function test__calcSpotPrice() external { // NOTE: Coverage only works if I initialize the fixture in the test function MockHyperdriveMath hyperdriveMath = new MockHyperdriveMath(); @@ -961,8 +963,8 @@ contract HyperdriveMathTest is HyperdriveTest { // indicate a bug in the max long function. // // Open the maximum long on Hyperdrive. - IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); IHyperdrive.PoolConfig memory config = hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); uint256 maxIterations = 10; if (fixedRate > 0.15e18) { maxIterations += 5; @@ -985,7 +987,7 @@ contract HyperdriveMathTest is HyperdriveTest { flatFee: config.fees.flat, governanceFee: config.fees.governance }), - hyperdrive.getCheckpoint(hyperdrive.latestCheckpoint()).exposure, + hyperdrive.getCheckpointExposure(hyperdrive.latestCheckpoint()), maxIterations ); (uint256 maturityTime, uint256 longAmount) = openLong(bob, maxLong); @@ -1125,11 +1127,8 @@ contract HyperdriveMathTest is HyperdriveTest { openShort(bob, initialShortAmount); // Open the maximum short on Hyperdrive. - IHyperdrive.Checkpoint memory checkpoint = hyperdrive.getCheckpoint( - hyperdrive.latestCheckpoint() - ); - IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); IHyperdrive.PoolConfig memory config = hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo(); uint256 maxShort = HyperdriveUtils.calculateMaxShort( HyperdriveUtils.MaxTradeParams({ shareReserves: info.shareReserves, @@ -1145,7 +1144,7 @@ contract HyperdriveMathTest is HyperdriveTest { flatFee: config.fees.flat, governanceFee: config.fees.governance }), - checkpoint.exposure, + hyperdrive.getCheckpointExposure(hyperdrive.latestCheckpoint()), 7 ); (uint256 maturityTime, ) = openShort(bob, maxShort); diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index 7d93e39b7..5a5f46e43 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -28,7 +28,6 @@ contract HyperdriveTest is BaseTest { uint256 internal constant INITIAL_SHARE_PRICE = ONE; uint256 internal constant MINIMUM_SHARE_RESERVES = ONE; uint256 internal constant MINIMUM_TRANSACTION_AMOUNT = 0.001e18; - uint256 internal constant PRECISION_THRESHOLD = 1e14; uint256 internal constant CHECKPOINT_DURATION = 1 days; uint256 internal constant POSITION_DURATION = 365 days; @@ -113,7 +112,6 @@ contract HyperdriveTest is BaseTest { initialSharePrice: ONE, minimumShareReserves: MINIMUM_SHARE_RESERVES, minimumTransactionAmount: MINIMUM_TRANSACTION_AMOUNT, - precisionThreshold: PRECISION_THRESHOLD, positionDuration: POSITION_DURATION, checkpointDuration: CHECKPOINT_DURATION, timeStretch: HyperdriveUtils.calculateTimeStretch(fixedRate), diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index 38077622e..a08bf4f51 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -173,11 +173,8 @@ library HyperdriveUtils { IHyperdrive _hyperdrive, uint256 _maxIterations ) internal view returns (uint256 baseAmount) { - IHyperdrive.Checkpoint memory checkpoint = _hyperdrive.getCheckpoint( - _hyperdrive.latestCheckpoint() - ); - IHyperdrive.PoolInfo memory poolInfo = _hyperdrive.getPoolInfo(); IHyperdrive.PoolConfig memory poolConfig = _hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory poolInfo = _hyperdrive.getPoolInfo(); (baseAmount, ) = calculateMaxLong( MaxTradeParams({ shareReserves: poolInfo.shareReserves, @@ -193,7 +190,7 @@ library HyperdriveUtils { flatFee: poolConfig.fees.flat, governanceFee: poolConfig.fees.governance }), - checkpoint.exposure, + _hyperdrive.getCheckpointExposure(_hyperdrive.latestCheckpoint()), _maxIterations ); return baseAmount; @@ -216,11 +213,8 @@ library HyperdriveUtils { IHyperdrive _hyperdrive, uint256 _maxIterations ) internal view returns (uint256) { - IHyperdrive.Checkpoint memory checkpoint = _hyperdrive.getCheckpoint( - _hyperdrive.latestCheckpoint() - ); - IHyperdrive.PoolInfo memory poolInfo = _hyperdrive.getPoolInfo(); IHyperdrive.PoolConfig memory poolConfig = _hyperdrive.getPoolConfig(); + IHyperdrive.PoolInfo memory poolInfo = _hyperdrive.getPoolInfo(); return calculateMaxShort( MaxTradeParams({ @@ -237,7 +231,9 @@ library HyperdriveUtils { flatFee: poolConfig.fees.flat, governanceFee: poolConfig.fees.governance }), - checkpoint.exposure, + _hyperdrive.getCheckpointExposure( + _hyperdrive.latestCheckpoint() + ), _maxIterations ); } @@ -632,7 +628,7 @@ library HyperdriveUtils { /// in the checkpoint. The pool's solvency is calculated as: /// /// $$ - /// s = z - \tfrac{exposure + min(exposure_{c}, 0)}{c} - z_{min} + /// s = z - \tfrac{exposure + min(exposure_{checkpoint}, 0)}{c} - z_{min} /// $$ /// /// When a long is opened, the share reserves $z$ increase by: @@ -641,32 +637,27 @@ library HyperdriveUtils { /// \Delta z = \tfrac{x - g(x)}{c} /// $$ /// - /// In the solidity implementation, we calculate the delta in the - /// exposure as: - /// - /// ``` - /// shareReservesDelta = _shareAmount - governanceCurveFee.divDown(_sharePrice) - /// uint128 exposureDelta = (2 * - /// _bondProceeds - - /// _shareReservesDelta.mulDown(_sharePrice)).toUint128(); - /// ``` - /// - /// From this, we can calculate our exposure as: + /// Opening the long increases the non-netted longs by the bond amount. + /// From this, the change in the exposure is given by: /// /// $$ - /// \Delta exposure = 2 \cdot y(x) - x + g(x) + /// \Delta exposure = y(x) /// $$ /// /// From this, we can calculate $S(x)$ as: /// /// $$ /// S(x) = \left( z + \Delta z \right) - \left( - /// \tfrac{exposure + min(exposure_{c}, 0) + \Delta exposure}{c} + /// \tfrac{ + /// exposure + + /// min(exposure_{checkpoint}, 0) + + /// \Delta exposure + /// }{c} /// \right) - z_{min} /// $$ /// /// It's possible that the pool is insolvent after opening a long. In - /// this case, we return `false` since the fixed point library can't + /// this case, we return `None` since the fixed point library can't /// represent negative numbers. /// @param _params The max long calculation parameters. /// @param _checkpointExposure The exposure in the checkpoint. @@ -692,11 +683,7 @@ library HyperdriveUtils { uint256 shareReserves = _params.shareReserves + _baseAmount.divDown(_params.sharePrice) - governanceFee.divDown(_params.sharePrice); - uint256 exposure = _params.longExposure + - 2 * - _bondAmount - - _baseAmount + - governanceFee; + uint256 exposure = _params.longExposure + _bondAmount; uint256 checkpointExposure = uint256(-_checkpointExposure.min(0)); if ( shareReserves + checkpointExposure.divDown(_params.sharePrice) >= @@ -721,11 +708,13 @@ library HyperdriveUtils { /// base amount that the long pays is given by: /// /// $$ - /// S'(x) = \tfrac{2}{c} \cdot \left( + /// S'(x) = \tfrac{1}{c} \cdot \left( /// 1 - y'(x) - \phi_{g} \cdot p \cdot c'(x) /// \right) \\ - /// = \tfrac{2}{c} \cdot \left( - /// 1 - y'(x) - \phi_{g} \cdot \phi_{c} \cdot \left( 1 - p \right) + /// = \tfrac{1}{c} \cdot \left( + /// 1 - y'(x) - \phi_{g} \cdot \phi_{c} \cdot \left( + /// 1 - p + /// \right) /// \right) /// $$ /// @@ -764,7 +753,7 @@ library HyperdriveUtils { ); derivative -= ONE; - return (derivative.mulDivDown(2e18, _params.sharePrice), success); + return (derivative.mulDivDown(1e18, _params.sharePrice), success); } /// @dev Gets the long amount that will be opened for a given base amount. @@ -1187,14 +1176,14 @@ library HyperdriveUtils { /// and $e(x)$ represents the pool's exposure after opening the short: /// /// $$ - /// e(x) = e_0 - min(x + D(x), max(e_{c}, 0)) + /// e(x) = e_0 - min(x, max(e_{c}, 0)) /// $$ /// /// We simplify our $e(x)$ formula by noting that the max short is only - /// constrained by solvency when $x + D(x) > max(e_{c}, 0)$ since - /// $x + D(x)$ grows faster than + /// constrained by solvency when $x > max(e_{c}, 0)$ since $x$ grows + /// faster than /// $P(x) - \tfrac{\phi_{c}}{c} \cdot \left( 1 - p \right) \cdot x$. - /// With this in mind, $min(x + D(x), max(e_{c}, 0)) = max(e_{c}, 0)$ + /// With this in mind, $min(x, max(e_{c}, 0)) = max(e_{c}, 0)$ /// whenever solvency is actually a constraint, so we can write: /// /// $$ @@ -1476,8 +1465,8 @@ library HyperdriveUtils { bytes4 _selector ) internal pure returns (string memory) { // Convert the selector to the correct error message. - if (_selector == IHyperdrive.BaseBufferExceedsShareReserves.selector) { - return "BaseBufferExceedsShareReserves"; + if (_selector == IHyperdrive.InsufficientLiquidity.selector) { + return "InsufficientLiquidity"; } if (_selector == IHyperdrive.BelowMinimumContribution.selector) { return "BelowMinimumContribution";