From 36797c7bdd3014cb18c2d58f84898606525041c8 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 29 Feb 2024 17:39:21 -0600 Subject: [PATCH 01/15] Use safe functions more consistently in `calculateNetCurveTradeSafe` --- contracts/src/internal/HyperdriveBase.sol | 1 + contracts/src/libraries/LPMath.sol | 126 ++++++++---- contracts/src/libraries/YieldSpaceMath.sol | 96 ++++++--- contracts/test/MockLPMath.sol | 6 +- contracts/test/MockYieldSpaceMath.sol | 30 +-- .../hyperdrive/LPWithdrawalTest.t.sol | 10 +- test/units/libraries/HyperdriveMath.t.sol | 10 + test/units/libraries/LPMath.t.sol | 183 ++++++++++++------ test/units/libraries/YieldSpaceMath.t.sol | 20 +- test/utils/HyperdriveUtils.sol | 1 + 10 files changed, 328 insertions(+), 155 deletions(-) diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index d90bf33a5..c36f0c0ce 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -226,6 +226,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { vaultSharePrice: _vaultSharePrice, initialVaultSharePrice: _initialVaultSharePrice, minimumShareReserves: _minimumShareReserves, + minimumTransactionAmount: _minimumTransactionAmount, timeStretch: _timeStretch, longsOutstanding: _marketState.longsOutstanding, longAverageTimeRemaining: _calculateTimeRemainingScaled( diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index b26c28e51..7dd598fa1 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; @@ -250,8 +251,10 @@ library LPMath { // If the max curve trade is greater than the net curve position, // then we can close the entire net curve position. if (maxCurveTrade >= netCurvePosition_) { - uint256 netCurveTrade = YieldSpaceMath - .calculateSharesOutGivenBondsInDown( + // Calculate the net curve trade. + uint256 netCurveTrade; + (netCurveTrade, success) = YieldSpaceMath + .calculateSharesOutGivenBondsInDownSafe( effectiveShareReserves, _params.bondReserves, netCurvePosition_, @@ -259,6 +262,21 @@ 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. @@ -299,19 +317,25 @@ 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. if (maxCurveTrade >= netCurvePosition_) { - uint256 netCurveTrade = YieldSpaceMath - .calculateSharesInGivenBondsOutUp( + // Calculate the net curve trade. + uint256 netCurveTrade; + (netCurveTrade, success) = YieldSpaceMath + .calculateSharesInGivenBondsOutUpSafe( effectiveShareReserves, _params.bondReserves, netCurvePosition_, @@ -319,20 +343,41 @@ 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. @@ -419,16 +464,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 @@ -902,17 +954,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. @@ -931,15 +998,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: @@ -950,15 +1009,16 @@ library LPMath { ONE - maxScalingFactor ); } else { - return 0; + // NOTE: If the max scaling factor is greater than one, the calculation + 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..415fdf83d 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 @@ -312,20 +345,23 @@ library YieldSpaceMath { } /// @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 +383,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 +401,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 +423,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 diff --git a/contracts/test/MockLPMath.sol b/contracts/test/MockLPMath.sol index f5e249e6c..3837521f7 100644 --- a/contracts/test/MockLPMath.sol +++ b/contracts/test/MockLPMath.sol @@ -72,12 +72,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..64a4bf1e8 100644 --- a/contracts/test/MockYieldSpaceMath.sol +++ b/contracts/test/MockYieldSpaceMath.sol @@ -93,38 +93,28 @@ 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( 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 099895660..b379f7dd8 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, @@ -1118,6 +1172,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 50_000_000e18, longAverageTimeRemaining: 1e18, @@ -1212,6 +1267,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -1321,6 +1377,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 0, longAverageTimeRemaining: 0, @@ -1416,6 +1473,7 @@ contract LPMathTest is HyperdriveTest { vaultSharePrice: 2e18, initialVaultSharePrice: initialVaultSharePrice, minimumShareReserves: 1e5, + minimumTransactionAmount: 1e5, timeStretch: timeStretch, longsOutstanding: 50_000_000e18, longAverageTimeRemaining: 1e18, @@ -1511,6 +1569,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( From bf485231d920eee032041f2c3a8a8be9b965d3ee Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 29 Feb 2024 21:10:59 -0600 Subject: [PATCH 02/15] Fail if `calculateSharesOutGivenBondsInDownSafe` underflows --- contracts/src/libraries/YieldSpaceMath.sol | 17 ++++++++++------- .../hyperdrive/IntraCheckpointNettingTest.t.sol | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/src/libraries/YieldSpaceMath.sol b/contracts/src/libraries/YieldSpaceMath.sol index 415fdf83d..85298c3c9 100644 --- a/contracts/src/libraries/YieldSpaceMath.sol +++ b/contracts/src/libraries/YieldSpaceMath.sol @@ -300,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, @@ -309,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) @@ -337,11 +337,14 @@ 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 diff --git a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol index 048c44457..6058feaa2 100644 --- a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol +++ b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol @@ -285,8 +285,8 @@ contract IntraCheckpointNettingTest is HyperdriveTest { // This test shows that you can open/close long/shorts with extreme positive interest 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 + int256 variableInterest = 0.3e18; + uint256 timeElapsed = 15275477; // 176 days between each trade uint256 tradeSize = 504168.031667365798150347e18; uint256 numTrades = 100; From 9abab7a03b5bd811d56d6a289c24ededd6e62b1d Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 12:33:51 -0600 Subject: [PATCH 03/15] Fixed the rust tests --- contracts/test/MockYieldSpaceMath.sol | 9 +- crates/hyperdrive-math/src/lib.rs | 4 + crates/hyperdrive-math/src/lp.rs | 55 ++++++++-- crates/hyperdrive-math/src/short/close.rs | 3 +- crates/hyperdrive-math/src/yield_space.rs | 122 ++++++++++++++++------ 5 files changed, 143 insertions(+), 50 deletions(-) diff --git a/contracts/test/MockYieldSpaceMath.sol b/contracts/test/MockYieldSpaceMath.sol index 64a4bf1e8..a9347ab34 100644 --- a/contracts/test/MockYieldSpaceMath.sol +++ b/contracts/test/MockYieldSpaceMath.sol @@ -117,7 +117,7 @@ contract MockYieldSpaceMath { return (result1, result2); } - function calculateMaxSellBondsIn( + function calculateMaxSellBondsInSafe( uint256 z, int256 zeta, uint256 y, @@ -125,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 918274cfd..f925803f1 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()), } From 5102086e6c677ea973341c946ab7cc720dda934d Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 13:24:12 -0600 Subject: [PATCH 04/15] Improved the system's liveness when the present value can't be computed --- contracts/src/interfaces/IHyperdrive.sol | 8 ++ contracts/src/internal/HyperdriveBase.sol | 62 ++++++++++---- .../src/internal/HyperdriveCheckpoint.sol | 15 +++- contracts/src/internal/HyperdriveLP.sol | 82 ++++++++++++++----- contracts/src/internal/HyperdriveLong.sol | 27 ++++-- contracts/src/internal/HyperdriveShort.sol | 27 ++++-- .../IntraCheckpointNettingTest.t.sol | 2 +- 7 files changed, 172 insertions(+), 51 deletions(-) diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index cf9d1be1a..83518c279 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 update liquidity brings the share reserves below /// the minimum share reserves. error InvalidShareReserves(); @@ -308,6 +312,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 c36f0c0ce..59c4fbb2f 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -176,18 +176,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( @@ -211,6 +223,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. @@ -428,25 +441,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 status) = LPMath + .calculatePresentValueSafe( + _getPresentValueParams(_vaultSharePrice) + ); + if (!status) { + 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..630565d31 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 not + // being able 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 61fc0a25d..fe88cbd5a 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -224,24 +224,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, vaultShares, - _options.asBase, + options.asBase, lpSharePrice ); } @@ -285,8 +293,14 @@ abstract contract HyperdriveLP is _lpShares ); - // Distribute 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(); + } // Redeem as many of the withdrawal shares as possible. uint256 withdrawalSharesRedeemed; @@ -299,7 +313,15 @@ abstract contract HyperdriveLP is ); withdrawalShares = _lpShares - withdrawalSharesRedeemed; - // Emit a RemoveLiquidity event. + // Emit a RemoveLiquidity event. If the LP share price calculation + // fails, we revert since this indicates that the ending present value + // can't be calculated after removing liquidity. + (uint256 lpSharePrice, bool status) = _calculateLPSharePriceSafe( + vaultSharePrice + ); + if (!status) { + revert IHyperdrive.InvalidLPSharePrice(); + } emit RemoveLiquidity( _options.destination, _lpShares, @@ -311,7 +333,7 @@ abstract contract HyperdriveLP is ), _options.asBase, uint256(withdrawalShares), - _calculateLPSharePrice(vaultSharePrice) + lpSharePrice ); return (proceeds, withdrawalShares); @@ -341,9 +363,12 @@ 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); + // 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); // Redeem as many of the withdrawal shares as possible. (proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal( @@ -437,32 +462,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(); @@ -470,6 +506,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 a0e522976..87b54f65b 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -189,15 +189,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); @@ -284,8 +296,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 22e16e4d8..3d1bc0db5 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -205,15 +205,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 @@ -335,8 +347,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/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol index 6058feaa2..d60256017 100644 --- a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol +++ b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol @@ -285,7 +285,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest { // This test shows that you can open/close long/shorts with extreme positive interest function test_netting_extreme_positive_interest_time_elapsed() external { uint256 initialVaultSharePrice = 0.5e18; - int256 variableInterest = 0.3e18; + int256 variableInterest = 0.5e18; uint256 timeElapsed = 15275477; // 176 days between each trade uint256 tradeSize = 504168.031667365798150347e18; uint256 numTrades = 100; From 93c3fda9825a57621f3dd5dd9435fd691229a928 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 13:33:03 -0600 Subject: [PATCH 05/15] Improved the liveness properties of `removeLiquidity` --- contracts/src/internal/HyperdriveLP.sol | 29 +++++++++---------------- contracts/src/libraries/LPMath.sol | 14 +++++++----- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index fe88cbd5a..59271a025 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -293,15 +293,6 @@ abstract contract HyperdriveLP is _lpShares ); - // 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(); - } - // Redeem as many of the withdrawal shares as possible. uint256 withdrawalSharesRedeemed; (proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal( @@ -363,13 +354,6 @@ abstract contract HyperdriveLP is uint256 vaultSharePrice = _pricePerVaultShare(); _applyCheckpoint(_latestCheckpoint(), vaultSharePrice); - // 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); - // Redeem as many of the withdrawal shares as possible. (proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal( msg.sender, @@ -401,7 +385,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 of base the LP expects to /// receive for each withdrawal share that is burned. /// @param _options The options that configure how the operation is settled. @@ -411,10 +395,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. @@ -447,7 +438,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. // diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 7dd598fa1..af0365b2d 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -263,9 +263,10 @@ library LPMath { _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 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 @@ -344,9 +345,10 @@ library LPMath { _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 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 From 682a92dc6abecc07db391312b368d7fd3b35920c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 13:41:49 -0600 Subject: [PATCH 06/15] Ignore `calculateLPSharePrice` failures in `removeLiquidity` --- contracts/src/internal/HyperdriveCheckpoint.sol | 4 ++-- contracts/src/internal/HyperdriveLP.sol | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/contracts/src/internal/HyperdriveCheckpoint.sol b/contracts/src/internal/HyperdriveCheckpoint.sol index 630565d31..b8100403f 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -233,8 +233,8 @@ abstract contract HyperdriveCheckpoint is // Emit an event about the checkpoint creation that includes the LP // 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 not - // being able to calculate the present value. + // ensures that the system's liveness isn't impacted by temporarilj + // being unable to calculate the present value. (uint256 lpSharePrice, ) = _calculateLPSharePriceSafe(_vaultSharePrice); emit CreateCheckpoint( _checkpointTime, diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index 59271a025..03fd5ad48 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -305,14 +305,10 @@ abstract contract HyperdriveLP is withdrawalShares = _lpShares - withdrawalSharesRedeemed; // Emit a RemoveLiquidity event. If the LP share price calculation - // fails, we revert since this indicates that the ending present value - // can't be calculated after removing liquidity. - (uint256 lpSharePrice, bool status) = _calculateLPSharePriceSafe( - vaultSharePrice - ); - if (!status) { - revert IHyperdrive.InvalidLPSharePrice(); - } + // 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, From 722d3e3990f0060975cbb752d24363fd5c0e92e9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 26 Feb 2024 19:15:32 -0600 Subject: [PATCH 07/15] Ensure that the ending indexes are valid in `HyperdriveFactory` getters --- contracts/src/factory/HyperdriveFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/factory/HyperdriveFactory.sol b/contracts/src/factory/HyperdriveFactory.sol index 977dc7fe5..867e50629 100644 --- a/contracts/src/factory/HyperdriveFactory.sol +++ b/contracts/src/factory/HyperdriveFactory.sol @@ -789,7 +789,7 @@ contract HyperdriveFactory is IHyperdriveFactory { if (startIndex > endIndex) { revert IHyperdriveFactory.InvalidIndexes(); } - if (endIndex > _instances.length) { + if (endIndex >= _instances.length) { revert IHyperdriveFactory.EndIndexTooLarge(); } @@ -829,7 +829,7 @@ contract HyperdriveFactory is IHyperdriveFactory { if (startIndex > endIndex) { revert IHyperdriveFactory.InvalidIndexes(); } - if (endIndex > _deployerCoordinators.length) { + if (endIndex >= _deployerCoordinators.length) { revert IHyperdriveFactory.EndIndexTooLarge(); } From 4c3aebae406fbadb7ff1d8ff51c65f7cc72ba30c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 17:32:23 -0600 Subject: [PATCH 08/15] Used unchecked arithmetic in several places --- contracts/src/factory/HyperdriveFactory.sol | 12 +++- contracts/src/internal/HyperdriveBase.sol | 8 ++- contracts/src/internal/HyperdriveLP.sol | 4 +- contracts/src/internal/HyperdriveLong.sol | 4 +- .../src/internal/HyperdriveMultiToken.sol | 4 +- contracts/src/libraries/YieldSpaceMath.sol | 70 +++++++++++++++---- 6 files changed, 80 insertions(+), 22 deletions(-) diff --git a/contracts/src/factory/HyperdriveFactory.sol b/contracts/src/factory/HyperdriveFactory.sol index 867e50629..87b9bc11f 100644 --- a/contracts/src/factory/HyperdriveFactory.sol +++ b/contracts/src/factory/HyperdriveFactory.sol @@ -639,7 +639,9 @@ contract HyperdriveFactory is IHyperdriveFactory { // Only the contribution amount of ether will be passed to // Hyperdrive. - refund = msg.value - _contribution; + unchecked { + refund = msg.value - _contribution; + } // Initialize the Hyperdrive instance. hyperdrive.initialize{ value: _contribution }( @@ -796,7 +798,9 @@ contract HyperdriveFactory is IHyperdriveFactory { // Return the range of instances. range = new address[](endIndex - startIndex + 1); for (uint256 i = startIndex; i <= endIndex; i++) { - range[i - startIndex] = _instances[i]; + unchecked { + range[i - startIndex] = _instances[i]; + } } } @@ -836,7 +840,9 @@ contract HyperdriveFactory is IHyperdriveFactory { // Return the range of instances. range = new address[](endIndex - startIndex + 1); for (uint256 i = startIndex; i <= endIndex; i++) { - range[i - startIndex] = _deployerCoordinators[i]; + unchecked { + range[i - startIndex] = _deployerCoordinators[i]; + } } } diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 59c4fbb2f..c9ae05903 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -338,14 +338,18 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { // Apply the updates to the zombie base proceeds and share reserves. if (baseProceeds < zombieBaseProceeds) { - zombieBaseProceeds -= baseProceeds; + unchecked { + zombieBaseProceeds -= baseProceeds; + } } else { zombieBaseProceeds = 0; } _marketState.zombieBaseProceeds = zombieBaseProceeds.toUint112(); uint256 zombieShareReserves = _marketState.zombieShareReserves; if (_shareProceeds < zombieShareReserves) { - zombieShareReserves -= _shareProceeds; + unchecked { + zombieShareReserves -= _shareProceeds; + } } else { zombieShareReserves = 0; } diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index 03fd5ad48..efe408541 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -66,7 +66,9 @@ abstract contract HyperdriveLP is if (vaultShares < 2 * _minimumShareReserves) { revert IHyperdrive.BelowMinimumContribution(); } - lpShares = vaultShares - 2 * _minimumShareReserves; + unchecked { + lpShares = vaultShares - 2 * _minimumShareReserves; + } // Set the initialized state to true. _marketState.isInitialized = true; diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 87b54f65b..714ff52d9 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -330,7 +330,9 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP { IHyperdrive.InsufficientLiquidityReason.SolvencyViolated ); } - shareReserves -= _shareReservesDelta; + unchecked { + shareReserves -= _shareReservesDelta; + } // If the effective share reserves are decreasing, then we need to // verify that z - zeta >= z_min is satisfied. diff --git a/contracts/src/internal/HyperdriveMultiToken.sol b/contracts/src/internal/HyperdriveMultiToken.sol index 07fc4f53f..5c02a1f9c 100644 --- a/contracts/src/internal/HyperdriveMultiToken.sol +++ b/contracts/src/internal/HyperdriveMultiToken.sol @@ -158,7 +158,9 @@ abstract contract HyperdriveMultiToken is IHyperdriveEvents, HyperdriveBase { } // Decrement from the source and supply. - _balanceOf[tokenID][from] -= amount; + unchecked { + _balanceOf[tokenID][from] -= amount; + } _totalSupply[tokenID] -= amount; // Emit an event to track burning. diff --git a/contracts/src/libraries/YieldSpaceMath.sol b/contracts/src/libraries/YieldSpaceMath.sol index 85298c3c9..666337fab 100644 --- a/contracts/src/libraries/YieldSpaceMath.sol +++ b/contracts/src/libraries/YieldSpaceMath.sol @@ -72,7 +72,10 @@ library YieldSpaceMath { // NOTE: We round _y up to make the rhs of the equation larger. // // (k - (c / µ) * (µ * (ze + dz))^(1 - t))^(1 / (1 - t)) - uint256 _y = k - ze; + uint256 _y; + unchecked { + _y = k - ze; + } if (_y >= ONE) { // Rounding up the exponent results in a larger result. _y = _y.pow(ONE.divUp(t)); @@ -89,7 +92,9 @@ library YieldSpaceMath { } // Δy = y - (k - (c / µ) * (µ * (ze + dz))^(1 - t))^(1 / (1 - t)) - return y - _y; + unchecked { + return y - _y; + } } /// @dev Calculates the amount of shares a user must provide the pool to @@ -158,7 +163,10 @@ library YieldSpaceMath { } // (y - dy)^(1 - t) - y = (y - dy).pow(t); + unchecked { + y -= dy; + } + y = y.pow(t); // If k < y, we return a failure flag since the calculation would have // underflowed. @@ -169,7 +177,11 @@ library YieldSpaceMath { // NOTE: We round _z up to make the lhs of the equation larger. // // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) - uint256 _z = (k - y).mulDivUp(mu, c); + uint256 _z; + unchecked { + _z = k - y; + } + _z = _z.mulDivUp(mu, c); if (_z >= ONE) { // Rounding up the exponent results in a larger result. _z = _z.pow(ONE.divUp(t)); @@ -187,7 +199,9 @@ library YieldSpaceMath { } // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - return (_z - ze, true); + unchecked { + return (_z - ze, true); + } } /// @dev Calculates the amount of shares a user must provide the pool to @@ -221,7 +235,10 @@ library YieldSpaceMath { } // (y - dy)^(1 - t) - y = (y - dy).pow(t); + unchecked { + y -= dy; + } + y = y.pow(t); // If k < y, we have no choice but to revert. if (k < y) { @@ -233,7 +250,11 @@ library YieldSpaceMath { // NOTE: We round _z down to make the lhs of the equation smaller. // // _z = ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) - uint256 _z = (k - y).mulDivDown(mu, c); + uint256 _z; + unchecked { + _z = k - y; + } + _z = _z.mulDivDown(mu, c); if (_z >= ONE) { // Rounding down the exponent results in a smaller result. _z = _z.pow(ONE.divDown(t)); @@ -252,7 +273,9 @@ library YieldSpaceMath { } // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze - return _z - ze; + unchecked { + return _z - ze; + } } /// @dev Calculates the amount of shares a user will receive from the pool @@ -326,7 +349,11 @@ library YieldSpaceMath { // NOTE: We round _z up to make the rhs of the equation larger. // // ((k - (y + dy)^(1 - t)) / (c / µ))^(1 / (1 - t))) - uint256 _z = (k - y).mulDivUp(mu, c); + uint256 _z; + unchecked { + _z = k - y; + } + _z = _z.mulDivUp(mu, c); if (_z >= ONE) { // Rounding the exponent up results in a larger outcome. _z = _z.pow(ONE.divUp(t)); @@ -344,7 +371,9 @@ library YieldSpaceMath { } // Δz = ze - ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) / µ - return (ze - _z, true); + unchecked { + return (ze - _z, true); + } } /// @dev Calculates the share payment required to purchase the maximum @@ -391,7 +420,9 @@ library YieldSpaceMath { if (optimalZe < ze) { return (0, false); } - return (optimalZe - ze, true); + unchecked { + return (optimalZe - ze, true); + } } /// @dev Calculates the maximum amount of bonds that can be purchased with @@ -431,7 +462,9 @@ library YieldSpaceMath { if (y < optimalY) { return (0, false); } - return (y - optimalY, true); + unchecked { + return (y - optimalY, true); + } } /// @dev Calculates the maximum amount of bonds that can be sold with the @@ -474,7 +507,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)); @@ -488,7 +528,9 @@ library YieldSpaceMath { if (optimalY < y) { return (0, false); } - return (optimalY - y, true); + unchecked { + return (optimalY - y, true); + } } /// @dev Calculates the YieldSpace invariant k. This invariant is given by: From 4da6f25187676c16f10e7bcba49a1bf5f748d123 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 18:00:19 -0600 Subject: [PATCH 09/15] Made the `LPMath` safer and used unchecked arithmetic --- contracts/src/libraries/LPMath.sol | 139 +++++++++++++++++++---------- 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index af0365b2d..7573ac853 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -693,7 +693,9 @@ library LPMath { // couldn't be calculated. return 0; } - derivative = ONE - derivative; + unchecked { + derivative = ONE - derivative; + } } // Otherwise, we can solve directly for the share proceeds. else { @@ -738,7 +740,9 @@ library LPMath { derivative.mulUp(lpTotalSupply) ); if (delta_ < shareProceeds) { - shareProceeds = shareProceeds - delta_; + unchecked { + shareProceeds = shareProceeds - delta_; + } } else { // NOTE: Returning 0 to indicate that the share proceeds // couldn't be calculated. @@ -802,7 +806,9 @@ library LPMath { // couldn't be calculated. return 0; } - derivative = ONE - derivative; + unchecked { + derivative = ONE - derivative; + } // NOTE: Round the delta down to avoid overshooting. // @@ -833,7 +839,9 @@ library LPMath { .divDown(derivative) .divDown(lpTotalSupply); if (delta_ < shareProceeds) { - shareProceeds = shareProceeds - delta_; + unchecked { + shareProceeds = shareProceeds - delta_; + } } else { // NOTE: Returning 0 to indicate that the share proceeds // couldn't be calculated. @@ -886,23 +894,22 @@ library LPMath { // NOTE: Round up since this is the rhs of the final subtraction. // // rhs = (PV(0) / l) * (l - w) - net_f - uint256 rhs; + uint256 rhs = _params.startingPresentValue.mulDivUp( + _params.activeLpTotalSupply, + _params.activeLpTotalSupply + _params.withdrawalSharesTotalSupply + ); if (netFlatTrade >= 0) { - rhs = - _params.startingPresentValue.mulDivUp( - _params.activeLpTotalSupply, - _params.activeLpTotalSupply + - _params.withdrawalSharesTotalSupply - ) - - uint256(netFlatTrade); + if (uint256(netFlatTrade) < rhs) { + unchecked { + rhs -= uint256(netFlatTrade); + } + } else { + // NOTE: Return a failure flag if computing the rhs would + // underflow. + return (0, false); + } } else { - rhs = - _params.startingPresentValue.mulDivUp( - _params.activeLpTotalSupply, - _params.activeLpTotalSupply + - _params.withdrawalSharesTotalSupply - ) + - uint256(-netFlatTrade); + rhs += uint256(-netFlatTrade); } // NOTE: Round up since this is the rhs of the final subtraction. @@ -914,7 +921,12 @@ library LPMath { ); // share proceeds = z - rhs - return (_params.originalShareReserves - rhs, true); + if (_params.originalShareReserves < rhs) { + return (0, false); + } + unchecked { + return (_params.originalShareReserves - rhs, true); + } } /// @dev Checks to see if we should short-circuit the iterative calculation @@ -1007,8 +1019,11 @@ library LPMath { // // maxShareReservesDelta = z * (1 - s) if (maxScalingFactor <= ONE) { - maxShareReservesDelta = _params.originalShareReserves.mulDown( - ONE - maxScalingFactor + unchecked { + maxShareReservesDelta = ONE - maxScalingFactor; + } + maxShareReservesDelta = maxShareReservesDelta.mulDown( + _params.originalShareReserves ); } else { // NOTE: If the max scaling factor is greater than one, the calculation @@ -1102,8 +1117,11 @@ library LPMath { // idle since the derivative couldn't be computed. return (0, false); } - inner = _params.presentValueParams.initialVaultSharePrice.mulDivUp( - k - inner, + unchecked { + inner = k - inner; + } + inner = inner.mulDivUp( + _params.presentValueParams.initialVaultSharePrice, _params.presentValueParams.vaultSharePrice ); if (inner >= ONE) { @@ -1130,7 +1148,9 @@ library LPMath { // derivative = 1 - derivative if (ONE >= derivative) { - derivative = ONE - derivative; + unchecked { + derivative = ONE - derivative; + } } else { // NOTE: Small rounding errors can result in the derivative being // slightly (on the order of a few wei) greater than 1. In this case, @@ -1142,12 +1162,18 @@ library LPMath { // // derivative = derivative * (1 - (zeta / z)) if (_params.originalShareAdjustment >= 0) { - derivative = derivative.mulDown( - ONE - - uint256(_params.originalShareAdjustment).divUp( - _params.originalShareReserves - ) + uint256 rhs = uint256(_params.originalShareAdjustment).divUp( + _params.originalShareReserves ); + if (rhs >= ONE) { + // NOTE: Return a failure flag if the calculation would + // underflow. + return (0, false); + } + unchecked { + rhs = ONE - rhs; + } + derivative = derivative.mulDown(rhs); } else { derivative = derivative.mulDown( ONE + @@ -1187,6 +1213,12 @@ library LPMath { uint256 _originalEffectiveShareReserves, uint256 _bondAmount ) internal pure returns (uint256, bool) { + // If the bond amount exceeds the bond reserves, we must return a + // failure flag. + if (_params.presentValueParams.bondReserves < _bondAmount) { + return (0, false); + } + // NOTE: Round up since this is on the rhs of the final subtraction. // // derivative = c * (mu * z_e(x)) ** -t_s + @@ -1210,15 +1242,21 @@ library LPMath { _params.presentValueParams.timeStretch ) ) - ) - - // NOTE: Round down this rounds the subtraction up. - _params.originalBondReserves.divDown( - _originalEffectiveShareReserves.mulUp( - (_params.presentValueParams.bondReserves - _bondAmount).pow( - _params.presentValueParams.timeStretch - ) - ) ); + // NOTE: Round down this rounds the subtraction up. + uint256 rhs = _params.originalBondReserves.divDown( + _originalEffectiveShareReserves.mulUp( + (_params.presentValueParams.bondReserves - _bondAmount).pow( + _params.presentValueParams.timeStretch + ) + ) + ); + if (derivative < rhs) { + return (0, false); + } + unchecked { + derivative -= rhs; + } // NOTE: Round up since this is on the rhs of the final subtraction. // @@ -1239,8 +1277,11 @@ library LPMath { // idle since the derivative couldn't be computed. return (0, false); } - inner = _params.presentValueParams.initialVaultSharePrice.mulDivUp( - k - inner, + unchecked { + inner = k - inner; + } + inner = inner.mulDivUp( + _params.presentValueParams.initialVaultSharePrice, _params.presentValueParams.vaultSharePrice ); if (inner >= ONE) { @@ -1275,7 +1316,9 @@ library LPMath { // derivative = 1 - derivative if (ONE >= derivative) { - derivative = ONE - derivative; + unchecked { + derivative = ONE - derivative; + } } else { // NOTE: Small rounding errors can result in the derivative being // slightly (on the order of a few wei) greater than 1. In this case, @@ -1287,12 +1330,16 @@ library LPMath { // // derivative = derivative * (1 - (zeta / z)) if (_params.originalShareAdjustment >= 0) { - derivative = derivative.mulDown( - ONE - - uint256(_params.originalShareAdjustment).divUp( - _params.originalShareReserves - ) + rhs = uint256(_params.originalShareAdjustment).divUp( + _params.originalShareReserves ); + if (rhs >= ONE) { + return (0, false); + } + unchecked { + rhs = ONE - rhs; + } + derivative = derivative.mulDown(rhs); } else { derivative = derivative.mulDown( ONE + From f0b00237502b1f5b3078e0d76e4ad22cc4732769 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 18:03:52 -0600 Subject: [PATCH 10/15] Use unchecked arithmetic in the rest of the contracts --- contracts/src/instances/steth/StETHBase.sol | 4 +++- contracts/src/internal/HyperdriveMultiToken.sol | 4 +++- contracts/src/internal/HyperdriveShort.sol | 4 +++- contracts/src/libraries/HyperdriveMath.sol | 8 ++++++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/contracts/src/instances/steth/StETHBase.sol b/contracts/src/instances/steth/StETHBase.sol index 98c2d1708..52ba2c6d3 100644 --- a/contracts/src/instances/steth/StETHBase.sol +++ b/contracts/src/instances/steth/StETHBase.sol @@ -51,7 +51,9 @@ abstract contract StETHBase is HyperdriveBase { // If the user sent more ether than the amount specified, refund the // excess ether. - refund = msg.value - _amount; + unchecked { + refund = msg.value - _amount; + } // Submit the provided ether to Lido to be deposited. The fee // collector address is passed as the referral address; however, diff --git a/contracts/src/internal/HyperdriveMultiToken.sol b/contracts/src/internal/HyperdriveMultiToken.sol index 5c02a1f9c..f4ee00897 100644 --- a/contracts/src/internal/HyperdriveMultiToken.sol +++ b/contracts/src/internal/HyperdriveMultiToken.sol @@ -230,7 +230,9 @@ abstract contract HyperdriveMultiToken is IHyperdriveEvents, HyperdriveBase { } // Increment the signature nonce. - ++_nonces[owner]; + unchecked { + ++_nonces[owner]; + } // Set the state. _isApprovedForAll[owner][spender] = _approved; diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 3d1bc0db5..984b586e4 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -288,7 +288,9 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP { IHyperdrive.InsufficientLiquidityReason.SolvencyViolated ); } - shareReserves -= _shareReservesDelta; + unchecked { + shareReserves -= _shareReservesDelta; + } // The share reserves are decreased in this operation, so we need to // verify that our invariants that z >= z_min and z - zeta >= z_min diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 9ef193fff..91585bd22 100644 --- a/contracts/src/libraries/HyperdriveMath.sol +++ b/contracts/src/libraries/HyperdriveMath.sol @@ -267,7 +267,9 @@ library HyperdriveMath { // interest proceeds, and the margin released. if (totalValue > _shareAmount) { // proceeds = (c1 / (c0 * c)) * dy - dz - shareProceeds = totalValue - _shareAmount; + unchecked { + shareProceeds = totalValue - _shareAmount; + } } return shareProceeds; @@ -329,7 +331,9 @@ library HyperdriveMath { // interest proceeds, and the margin released. if (totalValue > _shareAmount) { // proceeds = (c1 / (c0 * c)) * dy - dz - shareProceeds = totalValue - _shareAmount; + unchecked { + shareProceeds = totalValue - _shareAmount; + } } return shareProceeds; From 2a853e69b6e8425bfdf8bce5f7e9e74e28666510 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 1 Mar 2024 19:02:41 -0600 Subject: [PATCH 11/15] Consolidated the derivative functions in the `LPMath` library --- contracts/src/libraries/LPMath.sol | 239 +++++++---------------------- 1 file changed, 53 insertions(+), 186 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 7573ac853..e084d675c 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -683,10 +683,10 @@ library LPMath { ( derivative, success - ) = calculateSharesOutGivenBondsInDerivativeSafe( + ) = calculateSharesDeltaGivenBondsDeltaDerivativeSafe( _params, _originalEffectiveShareReserves, - uint256(_params.netCurveTrade) + _params.netCurveTrade ); if (!success || derivative >= ONE) { // NOTE: Return 0 to indicate that the share proceeds @@ -792,14 +792,14 @@ library LPMath { // If the net curve trade is less than or equal to the maximum // amount of bonds that can be sold with this share proceeds, we // can calculate the derivative using the derivative of - // `calculateSharesOutGivenBondsIn`. + // `calculateSharesInGivenBondsOut`. ( uint256 derivative, bool success - ) = calculateSharesInGivenBondsOutDerivativeSafe( + ) = calculateSharesDeltaGivenBondsDeltaDerivativeSafe( _params, _originalEffectiveShareReserves, - uint256(-_params.netCurveTrade) + _params.netCurveTrade ); if (!success || derivative >= ONE) { // NOTE: Return 0 to indicate that the share proceeds @@ -1038,10 +1038,13 @@ library LPMath { return (maxShareReservesDelta, true); } - /// @dev Calculates the derivative of `calculateSharesOutGivenBondsIn`. This - /// derivative is given by: + /// @dev Given a signed bond amount, this function calculates the negation + /// of the derivative of `calculateSharesOutGivenBondsIn` when the + /// bond amount is positive or the derivative of + /// `calculateSharesInGivenBondsOut` when the bond amount is negative. + /// In both cases, the calculation is given by: /// - /// derivative = - (1 - zeta / z) * ( + /// derivative = (1 - zeta / z) * ( /// 1 - (1 / c) * ( /// c * (mu * z_e(x)) ** -t_s + /// (y / z_e) * y(x) ** -t_s - @@ -1051,20 +1054,51 @@ library LPMath { /// ) ** (t_s / (1 - t_s)) /// ) /// - /// We round down to avoid overshooting the optimal solution in Newton's - /// method (the derivative we use is 1 minus this derivative so this - /// rounds the derivative up). + /// This quantity is used in Newton's method to search for the optimal + /// share proceeds. The derivative of the objective function F(x) is + /// given by: + /// + /// F'(x) = 1 - derivative + /// + /// With this in mind, this function rounds its result down so that + /// F'(x) is overestimated. Since F'(x) is in the denominator of + /// Newton's method, overestimating F'(x) helps to avoid overshooting + /// the optimal solution. /// @param _params The parameters for the calculation. /// @param _originalEffectiveShareReserves The original effective share /// reserves. - /// @param _bondAmount The amount of bonds to sell. - /// @return The derivative. + /// @param _bondAmount The amount of bonds that are being bought or sold. + /// @return The negation of the derivative of + /// `calculateSharesOutGivenBondsIn` when the bond amount is + /// positive or the derivative of `calculateSharesInGivenBondsOut` + /// when the bond amount is negative. /// @return A flag indicating whether the derivative could be computed. - function calculateSharesOutGivenBondsInDerivativeSafe( + function calculateSharesDeltaGivenBondsDeltaDerivativeSafe( DistributeExcessIdleParams memory _params, uint256 _originalEffectiveShareReserves, - uint256 _bondAmount + int256 _bondAmount ) internal pure returns (uint256, bool) { + // Calculate the bond reserves after the bond amount is applied. + uint256 bondReserves; + if (_bondAmount >= 0) { + bondReserves = + _params.presentValueParams.bondReserves + + uint256(_bondAmount); + } else { + uint256 bondAmount = uint256(-_bondAmount); + if (bondAmount < _params.presentValueParams.bondReserves) { + unchecked { + bondReserves = + _params.presentValueParams.bondReserves - + bondAmount; + } + } else { + // NOTE: Return a failure flag if calculating the bond reserves + // would underflow. + return (0, false); + } + } + // NOTE: Round up since this is on the rhs of the final subtraction. // // derivative = c * (mu * z_e(x)) ** -t_s + @@ -1092,9 +1126,7 @@ library LPMath { // NOTE: Round down to round the subtraction up. _params.originalBondReserves.divDown( _originalEffectiveShareReserves.mulUp( - (_params.presentValueParams.bondReserves + _bondAmount).pow( - _params.presentValueParams.timeStretch - ) + bondReserves.pow(_params.presentValueParams.timeStretch) ) ); @@ -1110,8 +1142,9 @@ library LPMath { _params.presentValueParams.vaultSharePrice, _params.presentValueParams.initialVaultSharePrice ); - uint256 inner = (_params.presentValueParams.bondReserves + _bondAmount) - .pow(ONE - _params.presentValueParams.timeStretch); + uint256 inner = bondReserves.pow( + ONE - _params.presentValueParams.timeStretch + ); if (k < inner) { // NOTE: In this case, we shouldn't proceed with distributing excess // idle since the derivative couldn't be computed. @@ -1185,170 +1218,4 @@ library LPMath { return (derivative, true); } - - /// @dev Calculates the derivative of `calculateSharesInGivenBondsOut`. This - /// derivative is given by: - /// - /// derivative = - (1 - zeta / z) * ( - /// (1 / c) * ( - /// c * (mu * z_e(x)) ** -t_s + - /// (y / z_e) * y(x) ** -t_s - - /// (y / z_e) * (y(x) - dy) ** -t_s - /// ) * ( - /// (mu / c) * (k(x) - (y(x) - dy) ** (1 - t_s)) - /// ) ** (t_s / (1 - t_s)) - 1 - /// ) - /// - /// We round down to avoid overshooting the optimal solution in - /// Newton's method (the derivative we use is 1 minus this derivative - /// so this rounds the derivative up). - /// @param _params The parameters for the calculation. - /// @param _originalEffectiveShareReserves The original effective share - /// reserves. - /// @param _bondAmount The amount of bonds to sell. - /// @return The derivative. - /// @return A flag indicating whether the derivative could be computed. - function calculateSharesInGivenBondsOutDerivativeSafe( - DistributeExcessIdleParams memory _params, - uint256 _originalEffectiveShareReserves, - uint256 _bondAmount - ) internal pure returns (uint256, bool) { - // If the bond amount exceeds the bond reserves, we must return a - // failure flag. - if (_params.presentValueParams.bondReserves < _bondAmount) { - return (0, false); - } - - // NOTE: Round up since this is on the rhs of the final subtraction. - // - // derivative = c * (mu * z_e(x)) ** -t_s + - // (y / z_e) * (y(x)) ** -t_s - - // (y / z_e) * (y(x) - dy) ** -t_s - uint256 effectiveShareReserves = HyperdriveMath - .calculateEffectiveShareReserves( - _params.presentValueParams.shareReserves, - _params.presentValueParams.shareAdjustment - ); - uint256 derivative = _params.presentValueParams.vaultSharePrice.divUp( - _params - .presentValueParams - .initialVaultSharePrice - .mulDown(effectiveShareReserves) - .pow(_params.presentValueParams.timeStretch) - ) + - _params.originalBondReserves.divUp( - _originalEffectiveShareReserves.mulDown( - _params.presentValueParams.bondReserves.pow( - _params.presentValueParams.timeStretch - ) - ) - ); - // NOTE: Round down this rounds the subtraction up. - uint256 rhs = _params.originalBondReserves.divDown( - _originalEffectiveShareReserves.mulUp( - (_params.presentValueParams.bondReserves - _bondAmount).pow( - _params.presentValueParams.timeStretch - ) - ) - ); - if (derivative < rhs) { - return (0, false); - } - unchecked { - derivative -= rhs; - } - - // NOTE: Round up since this is on the rhs of the final subtraction. - // - // inner = ( - // (mu / c) * (k(x) - (y(x) - dy) ** (1 - t_s)) - // ) ** (t_s / (1 - t_s)) - uint256 k = YieldSpaceMath.kUp( - effectiveShareReserves, - _params.presentValueParams.bondReserves, - ONE - _params.presentValueParams.timeStretch, - _params.presentValueParams.vaultSharePrice, - _params.presentValueParams.initialVaultSharePrice - ); - uint256 inner = (_params.presentValueParams.bondReserves - _bondAmount) - .pow(ONE - _params.presentValueParams.timeStretch); - if (k < inner) { - // NOTE: In this case, we shouldn't proceed with distributing excess - // idle since the derivative couldn't be computed. - return (0, false); - } - unchecked { - inner = k - inner; - } - inner = inner.mulDivUp( - _params.presentValueParams.initialVaultSharePrice, - _params.presentValueParams.vaultSharePrice - ); - if (inner >= ONE) { - // NOTE: Round the exponent up since this rounds the result up. - inner = inner.pow( - _params.presentValueParams.timeStretch.divUp( - ONE - _params.presentValueParams.timeStretch - ) - ); - } else { - // NOTE: Round the exponent down since this rounds the result up. - inner = inner.pow( - _params.presentValueParams.timeStretch.divDown( - ONE - _params.presentValueParams.timeStretch - ) - ); - } - - // NOTE: Round up since this is on the rhs of the final subtraction. - // - // derivative = (1 / c) * ( - // c * (mu * z_e(x)) ** -t_s + - // (y / z_e) * y(x) ** -t_s - - // (y / z_e) * (y(x) - dy) ** -t_s - // ) * ( - // (mu / c) * (k(x) - (y(x) - dy) ** (1 - t_s)) - // ) ** (t_s / (1 - t_s)) - derivative = derivative.mulDivUp( - inner, - _params.presentValueParams.vaultSharePrice - ); - - // derivative = 1 - derivative - if (ONE >= derivative) { - unchecked { - derivative = ONE - derivative; - } - } else { - // NOTE: Small rounding errors can result in the derivative being - // slightly (on the order of a few wei) greater than 1. In this case, - // we return 0 since we should proceed with Newton's method. - return (0, true); - } - - // NOTE: Round down to round the final result down. - // - // derivative = derivative * (1 - (zeta / z)) - if (_params.originalShareAdjustment >= 0) { - rhs = uint256(_params.originalShareAdjustment).divUp( - _params.originalShareReserves - ); - if (rhs >= ONE) { - return (0, false); - } - unchecked { - rhs = ONE - rhs; - } - derivative = derivative.mulDown(rhs); - } else { - derivative = derivative.mulDown( - ONE + - uint256(-_params.originalShareAdjustment).divDown( - _params.originalShareReserves - ) - ); - } - - return (derivative, true); - } } From 88cbfb05b620903839e5c4c9f357c1022fe959a3 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 5 Mar 2024 15:40:25 -0600 Subject: [PATCH 12/15] Addressed review feedback from @jrhea --- contracts/src/libraries/LPMath.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index e084d675c..a1b2ca3d8 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -661,10 +661,8 @@ library LPMath { break; } - // If the net curve trade is less than or equal to the maximum - // amount of bonds that can be sold with this share proceeds, we - // can calculate the derivative using the derivative of - // `calculateSharesOutGivenBondsIn`. + // Calculate the max bond amount. If the calculation fails, we + // return a failure flag. (uint256 maxBondAmount, bool success) = YieldSpaceMath .calculateMaxSellBondsInSafe( _params.presentValueParams.shareReserves, @@ -678,6 +676,11 @@ library LPMath { if (!success) { break; } + + // If the net curve trade is less than or equal to the maximum + // amount of bonds that can be sold with this share proceeds, we + // can calculate the derivative using the derivative of + // `calculateSharesOutGivenBondsIn`. uint256 derivative; if (uint256(_params.netCurveTrade) <= maxBondAmount) { ( From 49c01a78ba01b10899c8a5fca4eb6fc8f311d562 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 6 Mar 2024 18:17:06 -0600 Subject: [PATCH 13/15] Simplified the main loop of `calculateDistributeExcessIdle` --- contracts/src/libraries/LPMath.sol | 296 ++++++++++------------------- 1 file changed, 102 insertions(+), 194 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index a1b2ca3d8..0235a7d1c 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -630,40 +630,50 @@ library LPMath { lpTotalSupply ); - // If the net curve trade is positive, the pool is net long. - if (_params.netCurveTrade > 0) { - for (uint256 i = 0; i < SHARE_PROCEEDS_MAX_ITERATIONS; ) { - // Simulate applying the share proceeds to the reserves and - // recalculate the present value. - ( - _params.presentValueParams.shareReserves, - _params.presentValueParams.shareAdjustment, - _params.presentValueParams.bondReserves - ) = calculateUpdateLiquidity( - _params.originalShareReserves, - _params.originalShareAdjustment, - _params.originalBondReserves, - _params.presentValueParams.minimumShareReserves, - -int256(shareProceeds) - ); - uint256 presentValue = calculatePresentValue( - _params.presentValueParams - ); + // Proceed with Newton's method. The objective function, `F(x)`, is + // given by: + // + // F(x) = PV(x) * l - PV(0) * (l - w) + // + // Newton's method will terminate as soon as the current iteration is + // within the minimum tolerance or the maximum number of iterations has + // been reached. + for (uint256 i = 0; i < SHARE_PROCEEDS_MAX_ITERATIONS; ) { + // Simulate applying the share proceeds to the reserves and + // recalculate the present value. + ( + _params.presentValueParams.shareReserves, + _params.presentValueParams.shareAdjustment, + _params.presentValueParams.bondReserves + ) = calculateUpdateLiquidity( + _params.originalShareReserves, + _params.originalShareAdjustment, + _params.originalBondReserves, + _params.presentValueParams.minimumShareReserves, + -int256(shareProceeds) + ); + uint256 presentValue = calculatePresentValue( + _params.presentValueParams + ); - // Short-circuit if we are within the minimum tolerance. - if ( - shouldShortCircuitDistributeExcessIdleShareProceeds( - _params, - presentValue, - lpTotalSupply - ) - ) { - break; - } + // Short-circuit if we are within the minimum tolerance. + if ( + shouldShortCircuitDistributeExcessIdleShareProceeds( + _params, + presentValue, + lpTotalSupply + ) + ) { + break; + } + // If the pool is net long, we can solve for the next iteration of + // Newton's method directly when the net curve trade is greater than + // or equal to the max bond amount. + if (_params.netCurveTrade > 0) { // Calculate the max bond amount. If the calculation fails, we // return a failure flag. - (uint256 maxBondAmount, bool success) = YieldSpaceMath + (uint256 maxBondAmount, bool success_) = YieldSpaceMath .calculateMaxSellBondsInSafe( _params.presentValueParams.shareReserves, _params.presentValueParams.shareAdjustment, @@ -673,191 +683,89 @@ library LPMath { _params.presentValueParams.vaultSharePrice, _params.presentValueParams.initialVaultSharePrice ); - if (!success) { - break; + if (!success_) { + // NOTE: Return 0 to indicate that the share proceeds + // couldn't be calculated. + return 0; } - // If the net curve trade is less than or equal to the maximum - // amount of bonds that can be sold with this share proceeds, we - // can calculate the derivative using the derivative of - // `calculateSharesOutGivenBondsIn`. - uint256 derivative; - if (uint256(_params.netCurveTrade) <= maxBondAmount) { - ( - derivative, - success - ) = calculateSharesDeltaGivenBondsDeltaDerivativeSafe( - _params, - _originalEffectiveShareReserves, - _params.netCurveTrade - ); - if (!success || derivative >= ONE) { - // NOTE: Return 0 to indicate that the share proceeds - // couldn't be calculated. - return 0; - } - unchecked { - derivative = ONE - derivative; - } - } - // Otherwise, we can solve directly for the share proceeds. - else { + // If the net curve trade is greater than or equal to the max + // bond amount, we can solve directly for the share proceeds. + if (uint256(_params.netCurveTrade) >= maxBondAmount) { ( shareProceeds, - success + success_ ) = calculateDistributeExcessIdleShareProceedsNetLongEdgeCaseSafe( _params ); - if (!success) { + if (!success_) { // NOTE: Return 0 to indicate that the share proceeds // couldn't be calculated. return 0; } return shareProceeds; } - - // NOTE: Round the delta down to avoid overshooting. - // - // We calculate the updated share proceeds `x_n+1` by proceeding - // with Newton's method. This is given by: - // - // x_n+1 = x_n - F(x_n) / F'(x_n) - // - // where our objective function `F(x)` is: - // - // F(x) = PV(x) * l - PV(0) * (l - w) - int256 delta = int256(presentValue.mulDown(lpTotalSupply)) - - int256( - _params.startingPresentValue.mulUp( - _params.activeLpTotalSupply - ) - ); - if (delta > 0) { - // NOTE: Round the quotient down to avoid overshooting. - shareProceeds = - shareProceeds + - uint256(delta).divDown(derivative.mulUp(lpTotalSupply)); - } else if (delta < 0) { - // NOTE: Round the quotient down to avoid overshooting. - uint256 delta_ = uint256(-delta).divDown( - derivative.mulUp(lpTotalSupply) - ); - if (delta_ < shareProceeds) { - unchecked { - shareProceeds = shareProceeds - delta_; - } - } else { - // NOTE: Returning 0 to indicate that the share proceeds - // couldn't be calculated. - return 0; - } - } else { - break; - } - - // Increment the loop counter. - unchecked { - ++i; - } } - } - // Otherwise, the pool is net short. - else { - for (uint256 i = 0; i < SHARE_PROCEEDS_MAX_ITERATIONS; ) { - // Simulate applying the share proceeds to the reserves and - // recalculate the present value. - ( - _params.presentValueParams.shareReserves, - _params.presentValueParams.shareAdjustment, - _params.presentValueParams.bondReserves - ) = calculateUpdateLiquidity( - _params.originalShareReserves, - _params.originalShareAdjustment, - _params.originalBondReserves, - _params.presentValueParams.minimumShareReserves, - -int256(shareProceeds) - ); - uint256 presentValue = calculatePresentValue( - _params.presentValueParams + + // We calculate the derivative of F(x) using the derivative of + // `calculateSharesOutGivenBondsIn` when the pool is net long or + // the derivative of `calculateSharesInGivenBondsOut`. when the pool + // is net short. + ( + uint256 derivative, + bool success + ) = calculateSharesDeltaGivenBondsDeltaDerivativeSafe( + _params, + _originalEffectiveShareReserves, + _params.netCurveTrade ); + if (!success || derivative >= ONE) { + // NOTE: Return 0 to indicate that the share proceeds + // couldn't be calculated. + return 0; + } + unchecked { + derivative = ONE - derivative; + } - // Short-circuit if we are within the minimum tolerance. - if ( - shouldShortCircuitDistributeExcessIdleShareProceeds( - _params, - presentValue, - lpTotalSupply + // NOTE: Round the delta down to avoid overshooting. + // + // We calculate the updated share proceeds `x_n+1` by proceeding + // with Newton's method. This is given by: + // + // x_n+1 = x_n - F(x_n) / F'(x_n) + int256 delta = int256(presentValue.mulDown(lpTotalSupply)) - + int256( + _params.startingPresentValue.mulUp( + _params.activeLpTotalSupply ) - ) { - break; - } - - // If the net curve trade is less than or equal to the maximum - // amount of bonds that can be sold with this share proceeds, we - // can calculate the derivative using the derivative of - // `calculateSharesInGivenBondsOut`. - ( - uint256 derivative, - bool success - ) = calculateSharesDeltaGivenBondsDeltaDerivativeSafe( - _params, - _originalEffectiveShareReserves, - _params.netCurveTrade - ); - if (!success || derivative >= ONE) { - // NOTE: Return 0 to indicate that the share proceeds - // couldn't be calculated. - return 0; - } - unchecked { - derivative = ONE - derivative; - } - - // NOTE: Round the delta down to avoid overshooting. - // - // We calculate the updated share proceeds `x_n+1` by proceeding - // with Newton's method. This is given by: - // - // x_n+1 = x_n - F(x_n) / F'(x_n) - // - // where our objective function `F(x)` is: - // - // F(x) = PV(x) * l - PV(0) * (l - w) - int256 delta = int256(presentValue.mulDown(lpTotalSupply)) - - int256( - _params.startingPresentValue.mulUp( - _params.activeLpTotalSupply - ) - ); - if (delta > 0) { - // NOTE: Round the quotient down to avoid overshooting. - shareProceeds = - shareProceeds + - uint256(delta).divDown(derivative).divDown( - lpTotalSupply - ); - } else if (delta < 0) { - // NOTE: Round the quotient down to avoid overshooting. - uint256 delta_ = uint256(-delta) - .divDown(derivative) - .divDown(lpTotalSupply); - if (delta_ < shareProceeds) { - unchecked { - shareProceeds = shareProceeds - delta_; - } - } else { - // NOTE: Returning 0 to indicate that the share proceeds - // couldn't be calculated. - return 0; + ); + if (delta > 0) { + // NOTE: Round the quotient down to avoid overshooting. + shareProceeds = + shareProceeds + + uint256(delta).divDown(derivative).divDown(lpTotalSupply); + } else if (delta < 0) { + // NOTE: Round the quotient down to avoid overshooting. + uint256 delta_ = uint256(-delta).divDown(derivative).divDown( + lpTotalSupply + ); + if (delta_ < shareProceeds) { + unchecked { + shareProceeds = shareProceeds - delta_; } } else { - break; + // NOTE: Returning 0 to indicate that the share proceeds + // couldn't be calculated. + return 0; } + } else { + break; + } - // Increment the loop counter. - unchecked { - ++i; - } + // Increment the loop counter. + unchecked { + ++i; } } From 21076275ad4583fd86417819126a9b5601e0651e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 4 Mar 2024 11:56:40 -0600 Subject: [PATCH 14/15] Addressed review feedback from @saw-mon-and-natalie --- contracts/src/libraries/LPMath.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 04aa3a76b..0cf8bf352 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -749,11 +749,11 @@ library LPMath { return 0; } - // If the max bond amount is greater than or equal to the - // net curve trade, then Newton's method has terminated since + // If the max bond amount is less or equal to the net curve + // trade, then Newton's method has terminated since // proceeding to the next step would result in reaching the // same point. - if (maxBondAmount >= uint256(_params.netCurveTrade)) { + if (maxBondAmount <= uint256(_params.netCurveTrade)) { return shareProceeds; } // Otherwise, we continue to the next iteration of Newton's @@ -840,7 +840,7 @@ library LPMath { /// /// (1) zeta > 0: /// - /// y_max_out(dz) = (z - dz) - zeta * ((z - dz) / z) - z_min + /// z_max_out(dz) = ((z - dz) / z) * (z - zeta) - z_min /// /// => /// @@ -848,7 +848,7 @@ library LPMath { /// /// (2) zeta <= 0: /// - /// y_max_out(dz) = (z - dz) - z_min + /// z_max_out(dz) = (z - dz) - z_min /// /// => /// From c4a1d6180abcfece85199c18c8426bfd5d0c1a61 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 5 Mar 2024 13:05:14 -0600 Subject: [PATCH 15/15] Addressed review feedback from @saw-mon-and-natalie --- contracts/src/libraries/LPMath.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 0cf8bf352..147e9024e 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -749,8 +749,8 @@ library LPMath { return 0; } - // If the max bond amount is less or equal to the net curve - // trade, then Newton's method has terminated since + // If the max bond amount is less than or equal to the net + // curve trade, then Newton's method has terminated since // proceeding to the next step would result in reaching the // same point. if (maxBondAmount <= uint256(_params.netCurveTrade)) {