diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index 63099def0..326c2aa93 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -239,6 +239,10 @@ interface IHyperdrive is /// price of the underlying yield source on deployment. error InvalidInitialVaultSharePrice(); + /// @notice Thrown when the LP share price couldn't be calculated in a + /// critical situation. + error InvalidLPSharePrice(); + /// @notice Thrown when the present value calculation fails. error InvalidPresentValue(); @@ -307,6 +311,10 @@ interface IHyperdrive is /// pool's depository assets changes. error SweepFailed(); + /// @notice Thrown when the distribute excess idle calculation fails due + /// to the starting present value calculation failing. + error DistributeExcessIdleFailed(); + /// @notice Thrown when an ether transfer fails. error TransferFailed(); diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 49a46c60e..3784a3d4a 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -180,18 +180,30 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { /// @dev Gets the distribute excess idle parameters from the current state. /// @param _vaultSharePrice The current vault share price. /// @return params The distribute excess idle parameters. - function _getDistributeExcessIdleParams( + /// @return success A failure flag indicating if the calculation succeeded. + function _getDistributeExcessIdleParamsSafe( uint256 _idle, uint256 _withdrawalSharesTotalSupply, uint256 _vaultSharePrice - ) internal view returns (LPMath.DistributeExcessIdleParams memory params) { + ) + internal + view + returns (LPMath.DistributeExcessIdleParams memory params, bool success) + { + // Calculate the starting present value. If this fails, we return a + // failure flag and proceed to avoid impacting checkpointing liveness. LPMath.PresentValueParams memory presentValueParams = _getPresentValueParams( _vaultSharePrice ); - uint256 startingPresentValue = LPMath.calculatePresentValue( + uint256 startingPresentValue; + (startingPresentValue, success) = LPMath.calculatePresentValueSafe( presentValueParams ); + if (!success) { + return (params, false); + } + // NOTE: For consistency with the present value calculation, we round // up the long side and round down the short side. int256 netCurveTrade = int256( @@ -215,6 +227,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { originalShareAdjustment: presentValueParams.shareAdjustment, originalBondReserves: presentValueParams.bondReserves }); + success = true; } /// @dev Gets the present value parameters from the current state. @@ -230,6 +243,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { vaultSharePrice: _vaultSharePrice, initialVaultSharePrice: _initialVaultSharePrice, minimumShareReserves: _minimumShareReserves, + minimumTransactionAmount: _minimumTransactionAmount, timeStretch: _timeStretch, longsOutstanding: _marketState.longsOutstanding, longAverageTimeRemaining: _calculateTimeRemainingScaled( @@ -431,25 +445,46 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { return idleShares; } - /// @dev Calculates the LP share price. + /// @dev Calculates the LP share price. If the LP share price can't be + /// calculated, this function returns a failure flag. /// @param _vaultSharePrice The current vault share price. - /// @return lpSharePrice The LP share price in units of (base / lp shares). - function _calculateLPSharePrice( + /// @return The LP share price in units of (base / lp shares). + /// @return A flag indicating if the calculation succeeded. + function _calculateLPSharePriceSafe( uint256 _vaultSharePrice - ) internal view returns (uint256 lpSharePrice) { + ) internal view returns (uint256, bool) { + // Calculate the present value safely to prevent liveness problems. If + // the calculation fails, we return 0. + (uint256 presentValueShares, bool success) = LPMath + .calculatePresentValueSafe( + _getPresentValueParams(_vaultSharePrice) + ); + if (!success) { + return (0, false); + } + // NOTE: Round down to underestimate the LP share price. + // + // Calculate the present value in base and the LP total supply. uint256 presentValue = _vaultSharePrice > 0 - ? LPMath - .calculatePresentValue(_getPresentValueParams(_vaultSharePrice)) - .mulDown(_vaultSharePrice) + ? presentValueShares.mulDown(_vaultSharePrice) : 0; uint256 lpTotalSupply = _totalSupply[AssetId._LP_ASSET_ID] + _totalSupply[AssetId._WITHDRAWAL_SHARE_ASSET_ID] - _withdrawPool.readyToWithdraw; - lpSharePrice = lpTotalSupply == 0 - ? 0 // NOTE: Round down to underestimate the LP share price. - : presentValue.divDown(lpTotalSupply); - return lpSharePrice; + + // If the LP total supply is zero, the LP share price can't be computed + // due to a divide-by-zero error. + if (lpTotalSupply == 0) { + return (0, false); + } + + // NOTE: Round down to underestimate the LP share price. + // + // Calculate the LP share price. + uint256 lpSharePrice = presentValue.divDown(lpTotalSupply); + + return (lpSharePrice, true); } /// @dev Calculates the fees that go to the LPs and governance. diff --git a/contracts/src/internal/HyperdriveCheckpoint.sol b/contracts/src/internal/HyperdriveCheckpoint.sol index 913c734c2..13805b5e9 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -223,18 +223,25 @@ abstract contract HyperdriveCheckpoint is 0 ); - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(_vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we proceed with the + // calculation since checkpoints should be minted regardless of + // whether idle could be distributed. + _distributeExcessIdleSafe(_vaultSharePrice); } // Emit an event about the checkpoint creation that includes the LP - // share price. + // share price. If the LP share price calculation fails, we proceed in + // minting the checkpoint and just emit the LP share price as zero. This + // ensures that the system's liveness isn't impacted by temporarily + // being unable to calculate the present value. + (uint256 lpSharePrice, ) = _calculateLPSharePriceSafe(_vaultSharePrice); emit CreateCheckpoint( _checkpointTime, _vaultSharePrice, maturedShortsAmount, maturedLongsAmount, - _calculateLPSharePrice(_vaultSharePrice) + lpSharePrice ); return _vaultSharePrice; diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index 7c525d073..ffd280b00 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -229,24 +229,32 @@ abstract contract HyperdriveLP is // Mint LP shares to the supplier. _mint(AssetId._LP_ASSET_ID, _options.destination, lpShares); - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the distribute + // excess idle calculation fails, we revert to avoid allowing the system + // to enter an unhealthy state. A failure indicates that the present + // value can't be calculated. + bool success = _distributeExcessIdleSafe(vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } // Emit an AddLiquidity event. uint256 lpSharePrice = lpTotalSupply == 0 ? 0 // NOTE: We always round the LP share price down for consistency. : startingPresentValue.divDown(lpTotalSupply); + uint256 contribution = _contribution; // avoid stack-too-deep uint256 baseContribution = _convertToBaseFromOption( - _contribution, + contribution, vaultSharePrice, _options ); + IHyperdrive.Options calldata options = _options; // avoid stack-too-deep emit AddLiquidity( - _options.destination, + options.destination, lpShares, baseContribution, // base contribution shareContribution, // vault shares contribution - _options.asBase, + options.asBase, lpSharePrice ); } @@ -293,9 +301,6 @@ abstract contract HyperdriveLP is _lpShares ); - // Distribute excess idle to the withdrawal pool. - _distributeExcessIdle(vaultSharePrice); - // Redeem as many of the withdrawal shares as possible. uint256 withdrawalSharesRedeemed; (proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal( @@ -307,7 +312,11 @@ abstract contract HyperdriveLP is ); withdrawalShares = _lpShares - withdrawalSharesRedeemed; - // Emit a RemoveLiquidity event. + // Emit a RemoveLiquidity event. If the LP share price calculation + // fails, we proceed in removing liquidity and just emit the LP share + // price as zero. This ensures that the system's liveness isn't impacted + // by temporarily being unable to calculate the present value. + (uint256 lpSharePrice, ) = _calculateLPSharePriceSafe(vaultSharePrice); emit RemoveLiquidity( _options.destination, _lpShares, @@ -319,7 +328,7 @@ abstract contract HyperdriveLP is ), // vault shares proceeds _options.asBase, uint256(withdrawalShares), - _calculateLPSharePrice(vaultSharePrice) + lpSharePrice ); return (proceeds, withdrawalShares); @@ -353,10 +362,6 @@ abstract contract HyperdriveLP is uint256 vaultSharePrice = _pricePerVaultShare(); _applyCheckpoint(_latestCheckpoint(), vaultSharePrice); - // Distribute the excess idle to the withdrawal pool prior to redeeming - // the withdrawal shares. - _distributeExcessIdle(vaultSharePrice); - // Redeem as many of the withdrawal shares as possible. (proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal( msg.sender, @@ -388,7 +393,7 @@ abstract contract HyperdriveLP is /// withdrawal shares ready to withdraw. /// @param _source The address that owns the withdrawal shares to redeem. /// @param _withdrawalShares The withdrawal shares to redeem. - /// @param _sharePrice The share price. + /// @param _vaultSharePrice The vault share price. /// @param _minOutputPerShare The minimum amount the LP expects to /// receive for each withdrawal share that is burned. The units of /// this quantity are either base or vault shares, depending on the @@ -402,10 +407,17 @@ abstract contract HyperdriveLP is function _redeemWithdrawalSharesInternal( address _source, uint256 _withdrawalShares, - uint256 _sharePrice, + uint256 _vaultSharePrice, uint256 _minOutputPerShare, IHyperdrive.Options calldata _options ) internal returns (uint256 proceeds, uint256 withdrawalSharesRedeemed) { + // Distribute the excess idle to the withdrawal pool. If the distribute + // excess idle calculation fails, we proceed with the calculation since + // LPs should be able to redeem their withdrawal shares for existing + // withdrawal proceeds regardless of whether or not idle could be + // distributed. + _distributeExcessIdleSafe(_vaultSharePrice); + // Clamp the shares to the total amount of shares ready for withdrawal // to avoid unnecessary reverts. We exit early if the user has no shares // available to redeem. @@ -438,7 +450,7 @@ abstract contract HyperdriveLP is _withdrawPool.proceeds -= shareProceeds.toUint128(); // Withdraw the share proceeds to the user. - proceeds = _withdraw(shareProceeds, _sharePrice, _options); + proceeds = _withdraw(shareProceeds, _vaultSharePrice, _options); // NOTE: Round up to make the check more conservative. // @@ -453,32 +465,43 @@ abstract contract HyperdriveLP is /// @dev Distribute as much of the excess idle as possible to the withdrawal /// pool while holding the LP share price constant. /// @param _vaultSharePrice The current vault share price. - function _distributeExcessIdle(uint256 _vaultSharePrice) internal { + /// @return A failure flag indicating if the calculation succeeded. + function _distributeExcessIdleSafe( + uint256 _vaultSharePrice + ) internal returns (bool) { // If there are no withdrawal shares, then there is nothing to // distribute. uint256 withdrawalSharesTotalSupply = _totalSupply[ AssetId._WITHDRAWAL_SHARE_ASSET_ID ] - _withdrawPool.readyToWithdraw; if (withdrawalSharesTotalSupply == 0) { - return; + return true; } // If there is no excess idle, then there is nothing to distribute. uint256 idle = _calculateIdleShareReserves(_vaultSharePrice); if (idle == 0) { - return; + return true; + } + + // Get the distribute excess idle parameters. If this fails for some + // we return a failure flag so that the caller can handle the failure. + ( + LPMath.DistributeExcessIdleParams memory params, + bool success + ) = _getDistributeExcessIdleParamsSafe( + idle, + withdrawalSharesTotalSupply, + _vaultSharePrice + ); + if (!success) { + return false; } // Calculate the amount of withdrawal shares that should be redeemed // and their share proceeds. (uint256 withdrawalSharesRedeemed, uint256 shareProceeds) = LPMath - .calculateDistributeExcessIdle( - _getDistributeExcessIdleParams( - idle, - withdrawalSharesTotalSupply, - _vaultSharePrice - ) - ); + .calculateDistributeExcessIdle(params); // Update the withdrawal pool's state. _withdrawPool.readyToWithdraw += withdrawalSharesRedeemed.toUint128(); @@ -486,6 +509,8 @@ abstract contract HyperdriveLP is // Remove the withdrawal pool proceeds from the reserves. _updateLiquidity(-int256(shareProceeds)); + + return true; } /// @dev Updates the pool's liquidity and holds the pool's spot price constant. diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 5704f33b8..d27993d02 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -195,15 +195,27 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP { nonNettedLongs + int256(_bondAmount), nonNettedLongs ); + + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we revert to avoid + // putting the system in an unhealthy state after the trade is + // processed. + bool success = _distributeExcessIdleSafe(vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } } else { // Apply the zombie close to the state and adjust the share proceeds // to account for negative interest that might have accrued to the // zombie share reserves. shareProceeds = _applyZombieClose(shareProceeds, vaultSharePrice); - } - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we proceed with the + // calculation since traders should be able to close their positions + // at maturity regardless of whether idle could be distributed. + _distributeExcessIdleSafe(vaultSharePrice); + } // Withdraw the profit to the trader. uint256 proceeds = _withdraw(shareProceeds, vaultSharePrice, _options); @@ -287,8 +299,13 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP { ); } - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(_vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the distribute + // excess idle calculation fails, we revert to avoid putting the system + // in an unhealthy state after the trade is processed. + bool success = _distributeExcessIdleSafe(_vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } } /// @dev Applies the trading deltas from a closed long to the reserves and diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 7aa98013e..8e5c4a068 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -214,15 +214,27 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP { IHyperdrive.InsufficientLiquidityReason.SolvencyViolated ); } + + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we revert to avoid + // putting the system in an unhealthy state after the trade is + // processed. + bool success = _distributeExcessIdleSafe(vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } } else { // Apply the zombie close to the state and adjust the share proceeds // to account for negative interest that might have accrued to the // zombie share reserves. shareProceeds = _applyZombieClose(shareProceeds, vaultSharePrice); - } - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we proceed with the + // calculation since traders should be able to close their positions + // at maturity regardless of whether idle could be distributed. + _distributeExcessIdleSafe(vaultSharePrice); + } // Withdraw the profit to the trader. This includes the proceeds from // the short sale as well as the variable interest that was collected @@ -341,8 +353,13 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP { ); } - // Distribute the excess idle to the withdrawal pool. - _distributeExcessIdle(_vaultSharePrice); + // Distribute the excess idle to the withdrawal pool. If the distribute + // excess idle calculation fails, we revert to avoid putting the system + // in an unhealthy state after the trade is processed. + bool success = _distributeExcessIdleSafe(_vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } } /// @dev Applies the trading deltas from a closed short to the reserves and diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index e3797c7b9..b893b179b 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -125,6 +125,7 @@ library LPMath { uint256 vaultSharePrice; uint256 initialVaultSharePrice; uint256 minimumShareReserves; + uint256 minimumTransactionAmount; uint256 timeStretch; uint256 longsOutstanding; uint256 longAverageTimeRemaining; @@ -183,8 +184,8 @@ library LPMath { int256(_params.minimumShareReserves); } - // If the present value is negative, return a status code indicating the - // failure. + // If the present value is negative, return a failure flag indicating + // the failure. if (presentValue < 0) { return (0, false); } @@ -256,8 +257,11 @@ library LPMath { // NOTE: We round in the same direction as when closing longs // to accurately estimate the impact of closing the net curve // position. - uint256 netCurveTrade = YieldSpaceMath - .calculateSharesOutGivenBondsInDown( + // + // Calculate the net curve trade. + uint256 netCurveTrade; + (netCurveTrade, success) = YieldSpaceMath + .calculateSharesOutGivenBondsInDownSafe( effectiveShareReserves, _params.bondReserves, netCurvePosition_, @@ -265,6 +269,22 @@ library LPMath { _params.vaultSharePrice, _params.initialVaultSharePrice ); + + // If the net curve position is smaller than the minimum + // transaction amount and the trade fails, we mark it to 0. This + // prevents liveness problems when the net curve position is + // very small. + if ( + !success && + netCurvePosition_ < _params.minimumTransactionAmount + ) { + return (0, true); + } + // Otherwise, we return a failure flag. + else if (!success) { + return (0, false); + } + return (-int256(netCurveTrade), true); } // Otherwise, we can only close part of the net curve position. @@ -305,13 +325,17 @@ library LPMath { // Calculate the maximum amount of bonds that can be bought on // YieldSpace. - uint256 maxCurveTrade = YieldSpaceMath.calculateMaxBuyBondsOut( - effectiveShareReserves, - _params.bondReserves, - ONE - _params.timeStretch, - _params.vaultSharePrice, - _params.initialVaultSharePrice - ); + (uint256 maxCurveTrade, bool success) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe( + effectiveShareReserves, + _params.bondReserves, + ONE - _params.timeStretch, + _params.vaultSharePrice, + _params.initialVaultSharePrice + ); + if (!success) { + return (0, false); + } // If the max curve trade is greater than the net curve position, // then we can close the entire net curve position. @@ -319,8 +343,11 @@ library LPMath { // NOTE: We round in the same direction as when closing shorts // to accurately estimate the impact of closing the net curve // position. - uint256 netCurveTrade = YieldSpaceMath - .calculateSharesInGivenBondsOutUp( + // + // Calculate the net curve trade. + uint256 netCurveTrade; + (netCurveTrade, success) = YieldSpaceMath + .calculateSharesInGivenBondsOutUpSafe( effectiveShareReserves, _params.bondReserves, netCurvePosition_, @@ -328,20 +355,42 @@ library LPMath { _params.vaultSharePrice, _params.initialVaultSharePrice ); + + // If the net curve position is smaller than the minimum + // transaction amount and the trade fails, we mark it to 0. This + // prevents liveness problems when the net curve position is + // very small. + if ( + !success && + netCurvePosition_ < _params.minimumTransactionAmount + ) { + return (0, true); + } + // Otherwise, we return a failure flag. + else if (!success) { + return (0, false); + } + return (int256(netCurveTrade), true); } // Otherwise, we can only close part of the net curve position. // Since the spot price is equal to one after closing the entire net // curve position, we mark any remaining bonds to one. else { - uint256 maxSharePayment = YieldSpaceMath - .calculateMaxBuySharesIn( + // Calculate the max share payment. + uint256 maxSharePayment; + (maxSharePayment, success) = YieldSpaceMath + .calculateMaxBuySharesInSafe( effectiveShareReserves, _params.bondReserves, ONE - _params.timeStretch, _params.vaultSharePrice, _params.initialVaultSharePrice ); + if (!success) { + return (0, false); + } + return ( // NOTE: We round the difference down to underestimate the // impact of closing the net curve position. @@ -428,16 +477,23 @@ library LPMath { DistributeExcessIdleParams memory _params ) internal pure returns (uint256, uint256) { // Steps 1 and 2: Calculate the maximum amount the share reserves can be - // debited. + // debited. If the maximum share reserves delta can't be calculated, + // idle can't be distributed. uint256 originalEffectiveShareReserves = HyperdriveMath .calculateEffectiveShareReserves( _params.originalShareReserves, _params.originalShareAdjustment ); - uint256 maxShareReservesDelta = calculateMaxShareReservesDelta( - _params, - originalEffectiveShareReserves - ); + ( + uint256 maxShareReservesDelta, + bool success + ) = calculateMaxShareReservesDeltaSafe( + _params, + originalEffectiveShareReserves + ); + if (!success) { + return (0, 0); + } // Step 3: Calculate the amount of withdrawal shares that can be // redeemed given the maximum share reserves delta. Otherwise, we @@ -1014,17 +1070,32 @@ library LPMath { /// @param _originalEffectiveShareReserves The original effective share /// reserves. /// @return maxShareReservesDelta The upper bound on the share proceeds. - function calculateMaxShareReservesDelta( + /// @return success A flag indicating if the calculation succeeded. + function calculateMaxShareReservesDeltaSafe( DistributeExcessIdleParams memory _params, uint256 _originalEffectiveShareReserves - ) internal pure returns (uint256 maxShareReservesDelta) { + ) internal pure returns (uint256 maxShareReservesDelta, bool success) { // If the net curve position is zero or net long, then the maximum // share reserves delta is equal to the pool's idle. if (_params.netCurveTrade >= 0) { - return _params.idle; + return (_params.idle, true); } uint256 netCurveTrade = uint256(-_params.netCurveTrade); + // Calculate the max bond amount. If the calculation fails, we return a + // failure flag. + uint256 maxBondAmount; + (maxBondAmount, success) = YieldSpaceMath.calculateMaxBuyBondsOutSafe( + _originalEffectiveShareReserves, + _params.originalBondReserves, + ONE - _params.presentValueParams.timeStretch, + _params.presentValueParams.vaultSharePrice, + _params.presentValueParams.initialVaultSharePrice + ); + if (!success) { + return (0, false); + } + // We can solve for the maximum share reserves delta in one shot using // the fact that the maximum amount of bonds that can be purchased is // linear with respect to the scaling factor applied to the reserves. @@ -1043,15 +1114,7 @@ library LPMath { // s * y_out^max(z, y, zeta) - netCurveTrade = 0 // => // s = netCurveTrade / y_out^max(z, y, zeta) - uint256 maxScalingFactor = netCurveTrade.divUp( - YieldSpaceMath.calculateMaxBuyBondsOut( - _originalEffectiveShareReserves, - _params.originalBondReserves, - ONE - _params.presentValueParams.timeStretch, - _params.presentValueParams.vaultSharePrice, - _params.presentValueParams.initialVaultSharePrice - ) - ); + uint256 maxScalingFactor = netCurveTrade.divUp(maxBondAmount); // Using the maximum scaling factor, we can calculate the maximum share // reserves delta as: @@ -1062,15 +1125,17 @@ library LPMath { ONE - maxScalingFactor ); } else { - return 0; + // NOTE: If the max scaling factor is greater than one, the + // calculation fails and we return a failure flag. + return (0, false); } // If the maximum share reserves delta is greater than the idle, then // the maximum share reserves delta is equal to the idle. if (maxShareReservesDelta > _params.idle) { - return _params.idle; + return (_params.idle, true); } - return maxShareReservesDelta; + return (maxShareReservesDelta, true); } /// @dev Calculates the derivative of `calculateSharesOutGivenBondsIn`. This diff --git a/contracts/src/libraries/YieldSpaceMath.sol b/contracts/src/libraries/YieldSpaceMath.sol index 5f2bf5a41..ecc1ebabf 100644 --- a/contracts/src/libraries/YieldSpaceMath.sol +++ b/contracts/src/libraries/YieldSpaceMath.sol @@ -101,7 +101,7 @@ library YieldSpaceMath { /// @param t The time elapsed since the term's start. /// @param c The vault share price. /// @param mu The initial vault share price. - /// @return The amount of shares the trader pays. + /// @return result The amount of shares the trader pays. function calculateSharesInGivenBondsOutUp( uint256 ze, uint256 y, @@ -109,27 +109,61 @@ library YieldSpaceMath { uint256 t, uint256 c, uint256 mu - ) internal pure returns (uint256) { + ) internal pure returns (uint256 result) { + bool success; + (result, success) = calculateSharesInGivenBondsOutUpSafe( + ze, + y, + dy, + t, + c, + mu + ); + if (!success) { + Errors.throwInsufficientLiquidityError( + IHyperdrive.InsufficientLiquidityReason.ArithmeticUnderflow + ); + } + } + + /// @dev Calculates the amount of shares a user must provide the pool to + /// receive a specified amount of bonds. This function returns a + /// success flag instead of reverting. We overestimate the amount of + /// shares in. + /// @param ze The effective share reserves. + /// @param y The bond reserves. + /// @param dy The amount of bonds paid to the trader. + /// @param t The time elapsed since the term's start. + /// @param c The vault share price. + /// @param mu The initial vault share price. + /// @return The amount of shares the trader pays. + /// @return A flag indicating if the calculation succeeded. + function calculateSharesInGivenBondsOutUpSafe( + uint256 ze, + uint256 y, + uint256 dy, + uint256 t, + uint256 c, + uint256 mu + ) internal pure returns (uint256, bool) { // NOTE: We round k up to make the lhs of the equation larger. // // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) uint256 k = kUp(ze, y, t, c, mu); - // If y < dy, we have no choice but to revert. + // If y < dy, we return a failure flag since the calculation would have + // underflowed. if (y < dy) { - Errors.throwInsufficientLiquidityError( - IHyperdrive.InsufficientLiquidityReason.ArithmeticUnderflow - ); + return (0, false); } // (y - dy)^(1 - t) y = (y - dy).pow(t); - // If k < y, we have no choice but to revert. + // If k < y, we return a failure flag since the calculation would have + // underflowed. if (k < y) { - Errors.throwInsufficientLiquidityError( - IHyperdrive.InsufficientLiquidityReason.ArithmeticUnderflow - ); + return (0, false); } // NOTE: We round _z up to make the lhs of the equation larger. @@ -146,15 +180,14 @@ library YieldSpaceMath { // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ _z = _z.divUp(mu); - // If _z < ze, we have no choice but to revert. + // If _z < ze, we return a failure flag since the calculation would have + // underflowed. if (_z < ze) { - Errors.throwInsufficientLiquidityError( - IHyperdrive.InsufficientLiquidityReason.ArithmeticUnderflow - ); + return (0, false); } // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - return _z - ze; + return (_z - ze, true); } /// @dev Calculates the amount of shares a user must provide the pool to @@ -267,8 +300,8 @@ library YieldSpaceMath { /// @param t The time elapsed since the term's start. /// @param c The vault share price. /// @param mu The initial vault share price. - /// @return result The amount of shares the user receives - /// @return success A flag indicating if the calculation succeeded. + /// @return The amount of shares the user receives + /// @return A flag indicating if the calculation succeeded. function calculateSharesOutGivenBondsInDownSafe( uint256 ze, uint256 y, @@ -276,7 +309,7 @@ library YieldSpaceMath { uint256 t, uint256 c, uint256 mu - ) internal pure returns (uint256 result, bool success) { + ) internal pure returns (uint256, bool) { // NOTE: We round k up to make the rhs of the equation larger. // // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) @@ -304,28 +337,34 @@ library YieldSpaceMath { // ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ _z = _z.divUp(mu); - // Δz = ze - ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) / µ - if (ze > _z) { - result = ze - _z; + // If ze is less than _z, we return a failure flag since the calculation + // underflowed. + if (ze < _z) { + return (0, false); } - success = true; + + // Δz = ze - ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) / µ + return (ze - _z, true); } /// @dev Calculates the share payment required to purchase the maximum - /// amount of bonds from the pool. + /// amount of bonds from the pool. This function returns a success flag + /// instead of reverting. We round so that the max buy amount is + /// underestimated. /// @param ze The effective share reserves. /// @param y The bond reserves. /// @param t The time elapsed since the term's start. /// @param c The vault share price. /// @param mu The initial vault share price. /// @return The share payment to purchase the maximum amount of bonds. - function calculateMaxBuySharesIn( + /// @return A flag indicating if the calculation succeeded. + function calculateMaxBuySharesInSafe( uint256 ze, uint256 y, uint256 t, uint256 c, uint256 mu - ) internal pure returns (uint256) { + ) internal pure returns (uint256, bool) { // We solve for the maximum buy using the constraint that the pool's // spot price can never exceed 1. We do this by noting that a spot price // of 1, ((mu * ze) / y) ** tau = 1, implies that mu * ze = y. This @@ -347,12 +386,17 @@ library YieldSpaceMath { } optimalZe = optimalZe.divDown(mu); - // The optimal trade size is given by dz = ze' - ze. - return optimalZe - ze; + // The optimal trade size is given by dz = ze' - ze. If the calculation + // underflows, we return a failure flag. + if (optimalZe < ze) { + return (0, false); + } + return (optimalZe - ze, true); } /// @dev Calculates the maximum amount of bonds that can be purchased with - /// the specified reserves. We round so that the max buy amount is + /// the specified reserves. This function returns a success flag + /// instead of reverting. We round so that the max buy amount is /// underestimated. /// @param ze The effective share reserves. /// @param y The bond reserves. @@ -360,13 +404,14 @@ library YieldSpaceMath { /// @param c The vault share price. /// @param mu The initial vault share price. /// @return The maximum amount of bonds that can be purchased. - function calculateMaxBuyBondsOut( + /// @return A flag indicating if the calculation succeeded. + function calculateMaxBuyBondsOutSafe( uint256 ze, uint256 y, uint256 t, uint256 c, uint256 mu - ) internal pure returns (uint256) { + ) internal pure returns (uint256, bool) { // We can use the same derivation as in `calculateMaxBuySharesIn` to // calculate the minimum bond reserves as: // @@ -381,8 +426,12 @@ library YieldSpaceMath { optimalY = optimalY.pow(ONE.divDown(t)); } - // The optimal trade size is given by dy = y - y'. - return y - optimalY; + // The optimal trade size is given by dy = y - y'. If the calculation + // underflows, we return a failure flag. + if (y < optimalY) { + return (0, false); + } + return (y - optimalY, true); } /// @dev Calculates the maximum amount of bonds that can be sold with the @@ -425,7 +474,14 @@ library YieldSpaceMath { // y' = (k - (c / mu) * (mu * zMin) ** (1 - tau)) ** (1 / (1 - tau)). uint256 ze = HyperdriveMath.calculateEffectiveShareReserves(z, zeta); uint256 k = kDown(ze, y, t, c, mu); - uint256 optimalY = k - c.mulDivUp(mu.mulUp(zMin).pow(t), mu); + uint256 rhs = c.mulDivUp(mu.mulUp(zMin).pow(t), mu); + if (k < rhs) { + return (0, false); + } + uint256 optimalY; + unchecked { + optimalY = k - rhs; + } if (optimalY >= ONE) { // Rounding the exponent down results in a smaller outcome. optimalY = optimalY.pow(ONE.divDown(t)); diff --git a/contracts/src/token/ERC20ForwarderFactory.sol b/contracts/src/token/ERC20ForwarderFactory.sol index 42e4c9bf4..7b8ea39c0 100644 --- a/contracts/src/token/ERC20ForwarderFactory.sol +++ b/contracts/src/token/ERC20ForwarderFactory.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.20; import { IERC20Forwarder } from "../interfaces/IERC20Forwarder.sol"; import { IERC20ForwarderFactory } from "../interfaces/IERC20ForwarderFactory.sol"; -import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IMultiToken } from "../interfaces/IMultiToken.sol"; import { ERC20Forwarder } from "./ERC20Forwarder.sol"; diff --git a/contracts/test/MockLPMath.sol b/contracts/test/MockLPMath.sol index 586ad872f..d02d50801 100644 --- a/contracts/test/MockLPMath.sol +++ b/contracts/test/MockLPMath.sol @@ -74,12 +74,12 @@ contract MockLPMath { ); } - function calculateMaxShareReservesDelta( + function calculateMaxShareReservesDeltaSafe( LPMath.DistributeExcessIdleParams memory _params, uint256 _originalEffectiveShareReserves - ) external pure returns (uint256) { + ) external pure returns (uint256, bool) { return - LPMath.calculateMaxShareReservesDelta( + LPMath.calculateMaxShareReservesDeltaSafe( _params, _originalEffectiveShareReserves ); diff --git a/contracts/test/MockYieldSpaceMath.sol b/contracts/test/MockYieldSpaceMath.sol index 6ee71b8a1..a9347ab34 100644 --- a/contracts/test/MockYieldSpaceMath.sol +++ b/contracts/test/MockYieldSpaceMath.sol @@ -93,41 +93,31 @@ contract MockYieldSpaceMath { return (result1, result2); } - function calculateMaxBuySharesIn( + function calculateMaxBuySharesInSafe( uint256 ze, uint256 y, uint256 t, uint256 c, uint256 mu - ) external pure returns (uint256) { - uint256 result1 = YieldSpaceMath.calculateMaxBuySharesIn( - ze, - y, - t, - c, - mu - ); - return result1; + ) external pure returns (uint256, bool) { + (uint256 result1, bool result2) = YieldSpaceMath + .calculateMaxBuySharesInSafe(ze, y, t, c, mu); + return (result1, result2); } - function calculateMaxBuyBondsOut( + function calculateMaxBuyBondsOutSafe( uint256 ze, uint256 y, uint256 t, uint256 c, uint256 mu - ) external pure returns (uint256) { - uint256 result1 = YieldSpaceMath.calculateMaxBuyBondsOut( - ze, - y, - t, - c, - mu - ); - return result1; + ) external pure returns (uint256, bool) { + (uint256 result1, bool result2) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe(ze, y, t, c, mu); + return (result1, result2); } - function calculateMaxSellBondsIn( + function calculateMaxSellBondsInSafe( uint256 z, int256 zeta, uint256 y, @@ -135,11 +125,10 @@ contract MockYieldSpaceMath { uint256 t, uint256 c, uint256 mu - ) external pure returns (uint256) { - (uint256 result1, bool success) = YieldSpaceMath + ) external pure returns (uint256, bool) { + (uint256 result1, bool result2) = YieldSpaceMath .calculateMaxSellBondsInSafe(z, zeta, y, zMin, t, c, mu); - require(success, "MockYieldSpaceMath: calculateMaxSellBondsInSafe"); - return result1; + return (result1, result2); } function kUp( diff --git a/crates/hyperdrive-math/src/lib.rs b/crates/hyperdrive-math/src/lib.rs index 6ceef331e..dc0d5f080 100644 --- a/crates/hyperdrive-math/src/lib.rs +++ b/crates/hyperdrive-math/src/lib.rs @@ -161,6 +161,10 @@ impl State { self.config.minimum_share_reserves.into() } + fn minimum_transaction_amount(&self) -> FixedPoint { + self.config.minimum_transaction_amount.into() + } + fn curve_fee(&self) -> FixedPoint { self.config.fees.curve.into() } diff --git a/crates/hyperdrive-math/src/lp.rs b/crates/hyperdrive-math/src/lp.rs index ba045e193..625160dbf 100644 --- a/crates/hyperdrive-math/src/lp.rs +++ b/crates/hyperdrive-math/src/lp.rs @@ -66,29 +66,59 @@ impl State { // from the share reserves, so we negate the result. match net_curve_position.cmp(&int256!(0)) { Ordering::Greater => { - let max_curve_trade = - self.calculate_max_sell_bonds_in(self.minimum_share_reserves()); + let net_curve_position: FixedPoint = FixedPoint::from(net_curve_position); + let max_curve_trade = self + .calculate_max_sell_bonds_in_safe(self.minimum_share_reserves()) + .unwrap(); if max_curve_trade >= net_curve_position.into() { - -I256::from( - self.calculate_shares_out_given_bonds_in_down(net_curve_position.into()), - ) + match self + .calculate_shares_out_given_bonds_in_down_safe(net_curve_position.into()) + { + Ok(net_curve_trade) => -I256::from(net_curve_trade), + Err(err) => { + // If the net curve position is smaller than the + // minimum transaction amount and the trade fails, + // we mark it to 0. This prevents liveness problems + // when the net curve position is very small. + if net_curve_position < self.minimum_transaction_amount() { + I256::zero() + } else { + panic!("net_curve_trade failure: {}", err); + } + } + } } else { -I256::from(self.effective_share_reserves() - self.minimum_share_reserves()) } } Ordering::Less => { - let _net_curve_position: FixedPoint = FixedPoint::from(-net_curve_position); - let max_curve_trade = self.calculate_max_buy_bonds_out(); - if max_curve_trade >= _net_curve_position { - I256::from(self.calculate_shares_in_given_bonds_out_up(_net_curve_position)) + let net_curve_position: FixedPoint = FixedPoint::from(-net_curve_position); + let max_curve_trade = self.calculate_max_buy_bonds_out_safe().unwrap(); + if max_curve_trade >= net_curve_position { + match self + .calculate_shares_in_given_bonds_out_up_safe(net_curve_position.into()) + { + Ok(net_curve_trade) => I256::from(net_curve_trade), + Err(err) => { + // If the net curve position is smaller than the + // minimum transaction amount and the trade fails, + // we mark it to 0. This prevents liveness problems + // when the net curve position is very small. + if net_curve_position < self.minimum_transaction_amount() { + I256::zero() + } else { + panic!("net_curve_trade failure: {}", err); + } + } + } } else { - let max_share_payment = self.calculate_max_buy_shares_in(); + let max_share_payment = self.calculate_max_buy_shares_in_safe().unwrap(); // NOTE: We round the difference down to underestimate the // impact of closing the net curve position. I256::from( max_share_payment - + (_net_curve_position - max_curve_trade) + + (net_curve_position - max_curve_trade) .div_down(self.vault_share_price()), ) } @@ -152,6 +182,7 @@ mod tests { vault_share_price: state.info.vault_share_price, initial_vault_share_price: state.config.initial_vault_share_price, minimum_share_reserves: state.config.minimum_share_reserves, + minimum_transaction_amount: state.config.minimum_transaction_amount, long_average_time_remaining: state .time_remaining_scaled( current_block_timestamp.into(), @@ -213,6 +244,7 @@ mod tests { vault_share_price: state.info.vault_share_price, initial_vault_share_price: state.config.initial_vault_share_price, minimum_share_reserves: state.config.minimum_share_reserves, + minimum_transaction_amount: state.config.minimum_transaction_amount, long_average_time_remaining: long_average_time_remaining.into(), short_average_time_remaining: short_average_time_remaining.into(), shorts_outstanding: state.shorts_outstanding().into(), @@ -264,6 +296,7 @@ mod tests { vault_share_price: state.info.vault_share_price, initial_vault_share_price: state.config.initial_vault_share_price, minimum_share_reserves: state.config.minimum_share_reserves, + minimum_transaction_amount: state.config.minimum_transaction_amount, long_average_time_remaining: long_average_time_remaining.into(), short_average_time_remaining: short_average_time_remaining.into(), shorts_outstanding: state.shorts_outstanding().into(), diff --git a/crates/hyperdrive-math/src/short/close.rs b/crates/hyperdrive-math/src/short/close.rs index 707738c69..4d9b48497 100644 --- a/crates/hyperdrive-math/src/short/close.rs +++ b/crates/hyperdrive-math/src/short/close.rs @@ -26,7 +26,8 @@ impl State { // payment. // let curve_bonds_in = bond_amount * normalized_time_remaining; - self.calculate_shares_in_given_bonds_out_up(curve_bonds_in) + self.calculate_shares_in_given_bonds_out_up_safe(curve_bonds_in) + .unwrap() } else { fixed!(0) }; diff --git a/crates/hyperdrive-math/src/yield_space.rs b/crates/hyperdrive-math/src/yield_space.rs index 72089e3fd..1a1fa8533 100644 --- a/crates/hyperdrive-math/src/yield_space.rs +++ b/crates/hyperdrive-math/src/yield_space.rs @@ -71,31 +71,52 @@ pub trait YieldSpace { /// Calculates the amount of shares a user must provide the pool to receive /// a specified amount of bonds. We overestimate the amount of shares in. - fn calculate_shares_in_given_bonds_out_up(&self, dy: FixedPoint) -> FixedPoint { + fn calculate_shares_in_given_bonds_out_up_safe(&self, dy: FixedPoint) -> Result { // NOTE: We round k up to make the lhs of the equation larger. // // k = (c / µ) * (µ * z)^(1 - t) + y^(1 - t) let k = self.k_up(); // (y - dy)^(1 - t) + if self.y() < dy { + return Err(eyre!( + "calculate_shares_in_given_bonds_out_up_safe: y = {} < {} = dy", + self.y(), + dy, + )); + } let y = (self.y() - dy).pow(fixed!(1e18) - self.t()); // NOTE: We round _z up to make the lhs of the equation larger. // // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) - let mut ze = (k - y).mul_div_up(self.mu(), self.c()); - if ze >= fixed!(1e18) { + if k < y { + return Err(eyre!( + "calculate_shares_in_given_bonds_out_up_safe: k = {} < {} = y", + k, + y, + )); + } + let mut _z = (k - y).mul_div_up(self.mu(), self.c()); + if _z >= fixed!(1e18) { // Rounding up the exponent results in a larger result. - ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); + _z = _z.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); } else { // Rounding down the exponent results in a larger result. - ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); + _z = _z.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); } // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze = ze.div_up(self.mu()); + _z = _z.div_up(self.mu()); // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - ze - self.ze() + if _z < self.ze() { + return Err(eyre!( + "calculate_shares_in_given_bonds_out_up_safe: _z = {} < {} = ze", + _z, + self.ze(), + )); + } + Ok(_z - self.ze()) } /// Calculates the amount of shares a user must provide the pool to receive @@ -179,9 +200,9 @@ pub trait YieldSpace { } } - // Calculates the share payment required to purchase the maximum - // amount of bonds from the pool. - fn calculate_max_buy_shares_in(&self) -> FixedPoint { + /// Calculates the share payment required to purchase the maximum + /// amount of bonds from the pool. + fn calculate_max_buy_shares_in_safe(&self) -> Result { // We solve for the maximum buy using the constraint that the pool's // spot price can never exceed 1. We do this by noting that a spot price // of 1, ((mu * ze) / y) ** tau = 1, implies that mu * ze = y. This @@ -202,13 +223,24 @@ pub trait YieldSpace { optimal_ze = optimal_ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); } optimal_ze = optimal_ze.div_down(self.mu()); - optimal_ze - self.ze() + + // The optimal trade size is given by dz = ze' - ze. If the calculation + // underflows, we return a failure flag. + if optimal_ze >= self.ze() { + Ok(optimal_ze - self.ze()) + } else { + Err(eyre!( + "calculate_max_buy_shares_in_safe: optimal_ze = {} < {} = ze", + optimal_ze, + self.ze(), + )) + } } /// Calculates the maximum amount of bonds that can be purchased with the /// specified reserves. We round so that the max buy amount is /// underestimated. - fn calculate_max_buy_bonds_out(&self) -> FixedPoint { + fn calculate_max_buy_bonds_out_safe(&self) -> Result { // We solve for the maximum buy using the constraint that the pool's // spot price can never exceed 1. We do this by noting that a spot price // of 1, (mu * ze) / y ** tau = 1, implies that mu * ze = y. This @@ -226,14 +258,23 @@ pub trait YieldSpace { optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t())); } - // The optimal trade size is given by dy = y - y'. - self.y() - optimal_y + // The optimal trade size is given by dy = y - y'. If the calculation + // underflows, we return a failure flag. + if self.y() >= optimal_y { + Ok(self.y() - optimal_y) + } else { + Err(eyre!( + "calculate_max_buy_bonds_out_safe: y = {} < {} = optimal_y", + self.y(), + optimal_y, + )) + } } /// Calculates the maximum amount of bonds that can be sold with the /// specified reserves. We round so that the max sell amount is /// underestimated. - fn calculate_max_sell_bonds_in(&self, mut z_min: FixedPoint) -> FixedPoint { + fn calculate_max_sell_bonds_in_safe(&self, mut z_min: FixedPoint) -> Result { // If the share adjustment is negative, the minimum share reserves is // given by `z_min - zeta`, which ensures that the share reserves never // fall below the minimum share reserves. Otherwise, the minimum share @@ -261,8 +302,17 @@ pub trait YieldSpace { optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t())); } - // The optimal trade size is given by dy = y' - y. - optimal_y - self.y() + // The optimal trade size is given by dy = y' - y. If this subtraction + // will underflow, we return a failure flag. + if optimal_y >= self.y() { + Ok(optimal_y - self.y()) + } else { + Err(eyre!( + "calculate_max_sell_bonds_in_safe: optimal_y = {} < {} = y", + optimal_y, + self.y(), + )) + } } /// Calculates the YieldSpace invariant k. This invariant is given by: @@ -343,7 +393,7 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let in_ = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_shares_in_given_bonds_out_up(in_)); + let actual = state.calculate_shares_in_given_bonds_out_up_safe(in_); match mock .calculate_shares_in_given_bonds_out_up( state.ze().into(), @@ -471,7 +521,7 @@ mod tests { } #[tokio::test] - async fn fuzz_calculate_max_buy_shares_in() -> Result<()> { + async fn fuzz_calculate_max_buy_shares_in_safe() -> Result<()> { let chain = TestChainWithMocks::new(1).await?; let mock = chain.mock_yield_space_math(); @@ -479,9 +529,9 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_max_buy_shares_in()); + let actual = panic::catch_unwind(|| state.calculate_max_buy_shares_in_safe()); match mock - .calculate_max_buy_shares_in( + .calculate_max_buy_shares_in_safe( state.ze().into(), state.y().into(), (fixed!(1e18) - state.t()).into(), @@ -491,8 +541,10 @@ mod tests { .call() .await { - Ok(expected) => { - assert_eq!(actual.unwrap(), FixedPoint::from(expected)); + Ok((expected_out, expected_status)) => { + let actual = actual.unwrap(); + assert_eq!(actual.is_ok(), expected_status); + assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } Err(_) => assert!(actual.is_err()), } @@ -502,7 +554,7 @@ mod tests { } #[tokio::test] - async fn fuzz_calculate_max_buy_bounds_out() -> Result<()> { + async fn fuzz_calculate_max_buy_bounds_out_safe() -> Result<()> { let chain = TestChainWithMocks::new(1).await?; let mock = chain.mock_yield_space_math(); @@ -510,9 +562,9 @@ mod tests { let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_max_buy_bonds_out()); + let actual = panic::catch_unwind(|| state.calculate_max_buy_bonds_out_safe()); match mock - .calculate_max_buy_bonds_out( + .calculate_max_buy_bonds_out_safe( state.ze().into(), state.y().into(), (fixed!(1e18) - state.t()).into(), @@ -522,8 +574,10 @@ mod tests { .call() .await { - Ok(expected) => { - assert_eq!(actual.unwrap(), FixedPoint::from(expected)); + Ok((expected_out, expected_status)) => { + let actual = actual.unwrap(); + assert_eq!(actual.is_ok(), expected_status); + assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } Err(_) => assert!(actual.is_err()), } @@ -533,7 +587,7 @@ mod tests { } #[tokio::test] - async fn fuzz_calculate_max_sell_bonds_in() -> Result<()> { + async fn fuzz_calculate_max_sell_bonds_in_safe() -> Result<()> { let chain = TestChainWithMocks::new(1).await?; let mock = chain.mock_yield_space_math(); @@ -542,9 +596,9 @@ mod tests { for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); let z_min = rng.gen::(); - let actual = panic::catch_unwind(|| state.calculate_max_sell_bonds_in(z_min)); + let actual = panic::catch_unwind(|| state.calculate_max_sell_bonds_in_safe(z_min)); match mock - .calculate_max_sell_bonds_in( + .calculate_max_sell_bonds_in_safe( state.z().into(), state.zeta().into(), state.y().into(), @@ -556,8 +610,10 @@ mod tests { .call() .await { - Ok(expected) => { - assert_eq!(actual.unwrap(), FixedPoint::from(expected)); + Ok((expected_out, expected_status)) => { + let actual = actual.unwrap(); + assert_eq!(actual.is_ok(), expected_status); + assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out)); } Err(_) => assert!(actual.is_err()), } diff --git a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol index 048c44457..dc39529a4 100644 --- a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol +++ b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol @@ -286,7 +286,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest { function test_netting_extreme_positive_interest_time_elapsed() external { uint256 initialVaultSharePrice = 0.5e18; int256 variableInterest = 0.5e18; - uint256 timeElapsed = 15275477; //176 days bewteen each trade + uint256 timeElapsed = 15275477; // 176 days between each trade uint256 tradeSize = 504168.031667365798150347e18; uint256 numTrades = 100; @@ -842,8 +842,64 @@ contract IntraCheckpointNettingTest is HyperdriveTest { // fast forward time, create checkpoints and accrue interest advanceTimeWithCheckpoints(timeElapsed, variableInterest); + // Bob attempts to add liquidity. If this fails, we record a flag + // and ensure that he can add liquidity after the short is opened. + bool addLiquiditySuccess; + vm.stopPrank(); + vm.startPrank(celine); + uint256 contribution = 500_000_000e18; + baseToken.mint(contribution); + baseToken.approve(address(hyperdrive), contribution); + try + hyperdrive.addLiquidity( + contribution, + 0, // min lp share price of 0 + 0, // min spot rate of 0 + type(uint256).max, // max spot rate of uint256 max + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) // unused + }) + ) + returns (uint256 lpShares) { + // Adding liquidity succeeded, so we don't need to check again. + addLiquiditySuccess = true; + + // Immediately remove the liquidity to avoid interfering with + // the remaining test. + removeLiquidity(bob, lpShares); + } catch (bytes memory reason) { + // Adding liquidity failed, so we need to try again after + // opening a short. + addLiquiditySuccess = true; + + // Ensure that the failure was caused by the present value + // calculation failing. + assertEq( + keccak256(reason), + keccak256( + abi.encodeWithSelector( + IHyperdrive.InvalidPresentValue.selector + ) + ) + ); + } + + // Open a short position. (uint256 maturityTimeShort, ) = openShort(bob, bondAmount); shortMaturityTimes[i] = maturityTimeShort; + + // If adding liquidity failed, we try again to ensure that the LP + // can add liquidity when the pool is net neutral. + if (!addLiquiditySuccess) { + // Attempt to add liquidity. This should succeed. + uint256 lpShares = addLiquidity(bob, contribution); + + // Immediately remove the liquidity to avoid interfering with + // the remaining test. + removeLiquidity(bob, lpShares); + } } removeLiquidity(alice, aliceLpShares); diff --git a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol index b19132e67..036d8e91e 100644 --- a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol +++ b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol @@ -1971,15 +1971,17 @@ contract LPWithdrawalTest is HyperdriveTest { // Get the maximum share reserves delta prior to removing liquidity. LPMath.DistributeExcessIdleParams memory params = hyperdrive .getDistributeExcessIdleParams(); - uint256 maxBaseReservesDelta = LPMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, ) = LPMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( params.originalShareReserves, params.originalShareAdjustment ) - ) - .mulDown(hyperdrive.getPoolInfo().vaultSharePrice); + ); + uint256 maxBaseReservesDelta = maxShareReservesDelta.mulDown( + hyperdrive.getPoolInfo().vaultSharePrice + ); // Remove the liquidity. uint256 idleBefore = hyperdrive.idle(); diff --git a/test/units/libraries/HyperdriveMath.t.sol b/test/units/libraries/HyperdriveMath.t.sol index 6b118f081..7e88dfdcf 100644 --- a/test/units/libraries/HyperdriveMath.t.sol +++ b/test/units/libraries/HyperdriveMath.t.sol @@ -904,6 +904,16 @@ contract HyperdriveMathTest is HyperdriveTest { 115763819684266577237839082600338781403556286119250692248603493285535482011337, 0 ); + + // This is an edge case where the present value couldn't be calculated + // due to a tiny net curve trade. + _test__calculateMaxLong( + 3988, + 370950184595018764582435593, + 10660, + 999000409571, + 1000000000012659 + ); } function test__calculateMaxLong__fuzz( diff --git a/test/units/libraries/LPMath.t.sol b/test/units/libraries/LPMath.t.sol index 6d7b54542..625db2bc8 100644 --- a/test/units/libraries/LPMath.t.sol +++ b/test/units/libraries/LPMath.t.sol @@ -41,6 +41,7 @@ contract LPMathTest is HyperdriveTest { timeStretch ), minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, timeStretch: timeStretch, @@ -72,6 +73,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 1e18, @@ -110,6 +112,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0, @@ -142,6 +145,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -180,6 +184,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -212,6 +217,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0.3e18, @@ -241,6 +247,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0, @@ -284,6 +291,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 1e18, @@ -327,6 +335,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 100_000e18, longAverageTimeRemaining: 0.75e18, @@ -383,6 +392,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0.75e18, @@ -442,6 +452,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 100_000e18, longAverageTimeRemaining: 0.75e18, @@ -458,20 +469,29 @@ contract LPMathTest is HyperdriveTest { params.longsOutstanding.mulDown( params.longAverageTimeRemaining ); - uint256 maxCurveTrade = YieldSpaceMath.calculateMaxBuyBondsOut( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); - uint256 maxShareProceeds = YieldSpaceMath.calculateMaxBuySharesIn( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); + (uint256 maxCurveTrade, bool success) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); + uint256 maxShareProceeds; + (maxShareProceeds, success) = YieldSpaceMath + .calculateMaxBuySharesInSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); params.shareReserves += maxShareProceeds; params.shareReserves += (netCurveTrade - maxCurveTrade).divDown( params.vaultSharePrice @@ -509,6 +529,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e18, + minimumTransactionAmount: 1e18, timeStretch: timeStretch, longsOutstanding: 100_000e18, longAverageTimeRemaining: 0.75e18, @@ -525,20 +546,29 @@ contract LPMathTest is HyperdriveTest { params.longsOutstanding.mulDown( params.longAverageTimeRemaining ); - uint256 maxCurveTrade = YieldSpaceMath.calculateMaxBuyBondsOut( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); - uint256 maxShareProceeds = YieldSpaceMath.calculateMaxBuySharesIn( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); + (uint256 maxCurveTrade, bool success) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); + uint256 maxShareProceeds; + (maxShareProceeds, success) = YieldSpaceMath + .calculateMaxBuySharesInSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); params.shareReserves += maxShareProceeds; params.shareReserves += (netCurveTrade - maxCurveTrade).divDown( params.vaultSharePrice @@ -576,6 +606,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: 1e18, minimumShareReserves: 1e18, + minimumTransactionAmount: 1e18, timeStretch: timeStretch, longsOutstanding: 100_000e18, longAverageTimeRemaining: 0.75e18, @@ -592,20 +623,29 @@ contract LPMathTest is HyperdriveTest { params.longsOutstanding.mulDown( params.longAverageTimeRemaining ); - uint256 maxCurveTrade = YieldSpaceMath.calculateMaxBuyBondsOut( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); - uint256 maxShareProceeds = YieldSpaceMath.calculateMaxBuySharesIn( - uint256(int256(params.shareReserves) - params.shareAdjustment), - params.bondReserves, - ONE - params.timeStretch, - params.vaultSharePrice, - params.initialVaultSharePrice - ); + (uint256 maxCurveTrade, bool success) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); + uint256 maxShareProceeds; + (maxShareProceeds, success) = YieldSpaceMath + .calculateMaxBuySharesInSafe( + uint256( + int256(params.shareReserves) - params.shareAdjustment + ), + params.bondReserves, + ONE - params.timeStretch, + params.vaultSharePrice, + params.initialVaultSharePrice + ); + assertEq(success, true); params.shareReserves += maxShareProceeds; params.shareReserves += (netCurveTrade - maxCurveTrade).divDown( params.vaultSharePrice @@ -662,6 +702,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -691,14 +732,15 @@ contract LPMathTest is HyperdriveTest { originalShareAdjustment: shareAdjustment, originalBondReserves: bondReserves }); - uint256 maxShareReservesDelta = lpMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, bool success) = lpMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( shareReserves, shareAdjustment ) ); + assertEq(success, true); // The max share reserves delta is just the idle. assertEq(maxShareReservesDelta, params.idle); @@ -726,6 +768,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0.5e18, @@ -755,14 +798,15 @@ contract LPMathTest is HyperdriveTest { originalShareAdjustment: shareAdjustment, originalBondReserves: bondReserves }); - uint256 maxShareReservesDelta = lpMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, bool success) = lpMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( shareReserves, shareAdjustment ) ); + assertEq(success, true); // The max share reserves delta is just the idle. assertEq(maxShareReservesDelta, params.idle); @@ -790,6 +834,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 10_000_000e18, longAverageTimeRemaining: 0.5e18, @@ -819,14 +864,15 @@ contract LPMathTest is HyperdriveTest { originalShareAdjustment: shareAdjustment, originalBondReserves: bondReserves }); - uint256 maxShareReservesDelta = lpMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, bool success) = lpMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( shareReserves, shareAdjustment ) ); + assertEq(success, true); // The max share reserves delta is just the idle. assertEq(maxShareReservesDelta, params.idle); @@ -855,6 +901,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 1_000_000e18, longAverageTimeRemaining: 0.5e18, @@ -884,14 +931,15 @@ contract LPMathTest is HyperdriveTest { originalShareAdjustment: shareAdjustment, originalBondReserves: bondReserves }); - uint256 maxShareReservesDelta = lpMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, bool success) = lpMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( shareReserves, shareAdjustment ) ); + assertEq(success, true); // The max share reserves delta is just the idle. assertEq(maxShareReservesDelta, params.idle); @@ -920,6 +968,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -949,14 +998,15 @@ contract LPMathTest is HyperdriveTest { originalShareAdjustment: shareAdjustment, originalBondReserves: bondReserves }); - uint256 maxShareReservesDelta = lpMath - .calculateMaxShareReservesDelta( + (uint256 maxShareReservesDelta, bool success) = lpMath + .calculateMaxShareReservesDeltaSafe( params, HyperdriveMath.calculateEffectiveShareReserves( shareReserves, shareAdjustment ) ); + assertEq(success, true); // The max share reserves delta should have been calculated so that // the maximum amount of bonds can be purchased is close to the net @@ -972,16 +1022,19 @@ contract LPMathTest is HyperdriveTest { params.presentValueParams.minimumShareReserves, -int256(maxShareReservesDelta) ); - uint256 maxBondAmount = YieldSpaceMath.calculateMaxBuyBondsOut( - HyperdriveMath.calculateEffectiveShareReserves( - params.presentValueParams.shareReserves, - params.presentValueParams.shareAdjustment - ), - params.presentValueParams.bondReserves, - ONE - params.presentValueParams.timeStretch, - params.presentValueParams.vaultSharePrice, - params.presentValueParams.initialVaultSharePrice - ); + uint256 maxBondAmount; + (maxBondAmount, success) = YieldSpaceMath + .calculateMaxBuyBondsOutSafe( + HyperdriveMath.calculateEffectiveShareReserves( + params.presentValueParams.shareReserves, + params.presentValueParams.shareAdjustment + ), + params.presentValueParams.bondReserves, + ONE - params.presentValueParams.timeStretch, + params.presentValueParams.vaultSharePrice, + params.presentValueParams.initialVaultSharePrice + ); + assertEq(success, true); assertApproxEqAbs( maxBondAmount, params.presentValueParams.shortsOutstanding, @@ -1024,6 +1077,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -1071,14 +1125,17 @@ contract LPMathTest is HyperdriveTest { // Calculate the share proceeds. MockLPMath lpMath_ = lpMath; // avoid stack-too-deep + (uint256 maxShareReservesDelta, bool success) = lpMath_ + .calculateMaxShareReservesDeltaSafe( + params, + originalEffectiveShareReserves + ); + assertEq(success, true); uint256 shareProceeds = lpMath_ .calculateDistributeExcessIdleShareProceeds( params, originalEffectiveShareReserves, - lpMath_.calculateMaxShareReservesDelta( - params, - originalEffectiveShareReserves - ) + maxShareReservesDelta ); // Calculate the ending LP share price. @@ -1127,6 +1184,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 50_000_000e18, longAverageTimeRemaining: 1e18, @@ -1174,14 +1232,17 @@ contract LPMathTest is HyperdriveTest { // Calculate the share proceeds. MockLPMath lpMath_ = lpMath; // avoid stack-too-deep + (uint256 maxShareReservesDelta, bool success) = lpMath_ + .calculateMaxShareReservesDeltaSafe( + params, + originalEffectiveShareReserves + ); + assertEq(success, true); uint256 shareProceeds = lpMath_ .calculateDistributeExcessIdleShareProceeds( params, originalEffectiveShareReserves, - lpMath_.calculateMaxShareReservesDelta( - params, - originalEffectiveShareReserves - ) + maxShareReservesDelta ); // Calculate the ending LP share price. @@ -1230,6 +1291,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -1277,14 +1339,17 @@ contract LPMathTest is HyperdriveTest { // Calculate the share proceeds. MockLPMath lpMath_ = lpMath; // avoid stack-too-deep + (uint256 maxShareReservesDelta, bool success) = lpMath_ + .calculateMaxShareReservesDeltaSafe( + params, + originalEffectiveShareReserves + ); + assertEq(success, true); uint256 shareProceeds = lpMath_ .calculateDistributeExcessIdleShareProceeds( params, originalEffectiveShareReserves, - lpMath_.calculateMaxShareReservesDelta( - params, - originalEffectiveShareReserves - ) + maxShareReservesDelta ); // Calculate the ending LP share price. @@ -1348,6 +1413,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -1443,6 +1509,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 50_000_000e18, longAverageTimeRemaining: 1e18, @@ -1538,6 +1605,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, diff --git a/test/units/libraries/YieldSpaceMath.t.sol b/test/units/libraries/YieldSpaceMath.t.sol index 80bc67a75..6596e0df7 100644 --- a/test/units/libraries/YieldSpaceMath.t.sol +++ b/test/units/libraries/YieldSpaceMath.t.sol @@ -236,20 +236,24 @@ contract YieldSpaceMathTest is Test { ); // Calculatethe share payment and bonds proceeds of the max buy. - uint256 maxDy = yieldSpaceMath.calculateMaxBuyBondsOut( - shareReserves, - bondReserves, - 1e18 - ONE.mulDown(timeStretch), - vaultSharePrice, - initialVaultSharePrice - ); - uint256 maxDz = yieldSpaceMath.calculateMaxBuySharesIn( + (uint256 maxDy, bool success) = yieldSpaceMath + .calculateMaxBuyBondsOutSafe( + shareReserves, + bondReserves, + 1e18 - ONE.mulDown(timeStretch), + vaultSharePrice, + initialVaultSharePrice + ); + assertEq(success, true); + uint256 maxDz; + (maxDz, success) = yieldSpaceMath.calculateMaxBuySharesInSafe( shareReserves, bondReserves, 1e18 - ONE.mulDown(timeStretch), vaultSharePrice, initialVaultSharePrice ); + assertEq(success, true); // Ensure that the maximum buy is a valid trade on this invariant and // that the ending spot price is close to 1. diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index 323c8b7ae..f1725644e 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -1393,6 +1393,7 @@ library HyperdriveUtils { vaultSharePrice: poolInfo.vaultSharePrice, initialVaultSharePrice: poolConfig.initialVaultSharePrice, minimumShareReserves: poolConfig.minimumShareReserves, + minimumTransactionAmount: poolConfig.minimumTransactionAmount, timeStretch: poolConfig.timeStretch, longsOutstanding: poolInfo.longsOutstanding, longAverageTimeRemaining: calculateTimeRemaining(