From 7d3c155f26321c684c2022bda719c53ea2e09554 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 15:22:59 -0600 Subject: [PATCH 1/9] Made the rounding more consistent in `LPMath` --- contracts/src/libraries/HyperdriveMath.sol | 6 ++-- contracts/src/libraries/LPMath.sol | 33 ++++++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 6e6ee6e7d..9dcdf2f27 100644 --- a/contracts/src/libraries/HyperdriveMath.sol +++ b/contracts/src/libraries/HyperdriveMath.sol @@ -54,9 +54,11 @@ library HyperdriveMath { uint256 _positionDuration, uint256 _timeStretch ) internal pure returns (uint256 apr) { - // We are interested calculating the fixed APR for the pool. The annualized rate - // is given by the following formula: + // We are interested calculating the fixed APR for the pool. The + // annualized rate is given by the following formula: + // // r = (1 - p) / (p * t) + // // where t = _positionDuration / 365 uint256 spotPrice = calculateSpotPrice( _effectiveShareReserves, diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 22bd49bfc..717bce4bc 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -136,10 +136,9 @@ library LPMath { uint256 shortAverageTimeRemaining; } - // TODO: Evaluate the rounding. - // /// @dev Calculates the present value LPs capital in the pool and reverts - /// if the value is negative. + /// if the value is negative. This calculation underestimates the + /// present value to avoid paying out more than the pool can afford. /// @param _params The parameters for the present value calculation. /// @return The present value of the pool. function calculatePresentValue( @@ -154,10 +153,10 @@ library LPMath { return presentValue; } - // TODO: Evaluate the rounding. - // /// @dev Calculates the present value LPs capital in the pool and returns /// a flag indicating whether the calculation succeeded or failed. + /// This calculation underestimates the present value to avoid paying + /// out more than the pool can afford. /// @param _params The parameters for the present value calculation. /// @return The present value of the pool. /// @return A flag indicating whether the calculation succeeded or failed. @@ -194,8 +193,6 @@ library LPMath { return (uint256(presentValue), true); } - // TODO: Evaluate the rounding. - // /// @dev Calculates the result of closing the net curve position. /// @param _params The parameters for the present value calculation. /// @return The impact of closing the net curve position on the share @@ -204,6 +201,12 @@ library LPMath { function calculateNetCurveTradeSafe( PresentValueParams memory _params ) internal pure returns (int256, bool) { + // NOTE: To underestimate the impact of closing the net curve position, + // we round up the long side of the net curve position (since this + // results in a larger value removed from the share reserves) and round + // down the short side of the net curve position (since this results in + // a smaller value added to the share reserves). + // // The net curve position is the net of the longs and shorts that are // currently tradeable on the curve. Given the amount of outstanding // longs `y_l` and shorts `y_s` as well as the average time remaining @@ -212,7 +215,7 @@ library LPMath { // // netCurveTrade = y_l * t_l - y_s * t_s. int256 netCurvePosition = int256( - _params.longsOutstanding.mulDown(_params.longAverageTimeRemaining) + _params.longsOutstanding.mulUp(_params.longAverageTimeRemaining) ) - int256( _params.shortsOutstanding.mulDown( @@ -335,6 +338,8 @@ library LPMath { _params.initialVaultSharePrice ); return ( + // NOTE: We round the difference down to underestimate the + // impact of closing the net curve position. int256( maxSharePayment + (netCurvePosition_ - maxCurveTrade).divDown( @@ -349,8 +354,6 @@ library LPMath { return (0, true); } - // TODO: Evaluate the rounding. - // /// @dev Calculates the result of closing the net flat position. /// @param _params The parameters for the present value calculation. /// @return The impact of closing the net flat position on the share @@ -358,6 +361,10 @@ library LPMath { function calculateNetFlatTrade( PresentValueParams memory _params ) internal pure returns (int256) { + // NOTE: In order to underestimate the impact of closing all of the + // flat trades, we round the impact of closing the shorts down and round + // the impact of closing the longs up. + // // The net curve position is the net of the component of longs and // shorts that have matured. Given the amount of outstanding longs `y_l` // and shorts `y_s` as well as the average time remaining of outstanding @@ -372,7 +379,7 @@ library LPMath { ) ) - int256( - _params.longsOutstanding.mulDivDown( + _params.longsOutstanding.mulDivUp( ONE - _params.longAverageTimeRemaining, _params.vaultSharePrice ) @@ -818,10 +825,6 @@ library LPMath { return (0, false); } - // TODO: Double-check how the flat trade is used in the context of - // rounding once we do a rounding path through the present value - // calculation. - // // Calculate the net flat trade using the original reserves. _params.presentValueParams.shareReserves = _params .originalShareReserves; From 60b0aec856de481002d32a87168e0f6c22697e8b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 20:24:19 -0600 Subject: [PATCH 2/9] Updated the rounding behavior in `HyperdriveMath` --- contracts/src/internal/HyperdriveBase.sol | 28 ++- .../src/internal/HyperdriveCheckpoint.sol | 7 +- contracts/src/internal/HyperdriveLP.sol | 12 ++ contracts/src/internal/HyperdriveLong.sol | 12 ++ contracts/src/internal/HyperdriveShort.sol | 20 +- contracts/src/libraries/HyperdriveMath.sol | 159 +++++++++++++-- contracts/src/libraries/LPMath.sol | 8 +- contracts/test/MockHyperdriveMath.sol | 23 ++- .../VariableInterestShortTest.t.sol | 5 + test/units/hyperdrive/CloseShortTest.t.sol | 6 +- test/units/libraries/HyperdriveMath.t.sol | 192 +++++++++++++++++- test/utils/HyperdriveTest.sol | 5 +- test/utils/HyperdriveUtils.sol | 2 +- 13 files changed, 433 insertions(+), 46 deletions(-) diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 13b40d45d..d13e0e23e 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -183,6 +183,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { /// Helpers /// + // TODO: Do a rounding pass here. + // /// @dev Calculates the normalized time remaining of a position. /// @param _maturityTime The maturity time of the position. /// @return timeRemaining The normalized time remaining (in [0, 1]). @@ -193,9 +195,11 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; - timeRemaining = (timeRemaining).divDown(_positionDuration); + timeRemaining = timeRemaining.divDown(_positionDuration); } + // TODO: Do a rounding pass here. + // /// @dev Calculates the normalized time remaining of a position when the /// maturity time is scaled up 18 decimals. /// @param _maturityTime The maturity time of the position. @@ -206,7 +210,7 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; - timeRemaining = (timeRemaining).divDown(_positionDuration * ONE); + timeRemaining = timeRemaining.divDown(_positionDuration * ONE); } /// @dev Gets the most recent checkpoint time. @@ -262,6 +266,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { ); } + // TODO: Do a rounding pass here. + // /// @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. @@ -349,6 +355,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { return endingSpotPrice > _maxSpotPrice; } + // TODO: Do a rounding pass here. + // /// @dev Check solvency by verifying that the share reserves are greater /// than the exposure plus the minimum share reserves. /// @param _vaultSharePrice The current vault share price. @@ -379,6 +387,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { } } + // TODO: Do a rounding pass here. + // /// @dev Apply the updates to the market state as a result of closing a /// position after maturity. This function also adjusts the proceeds /// to account for any negative interest that has accrued in the @@ -426,6 +436,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { return _shareProceeds; } + // TODO: Do a rounding pass here. + // /// @dev Collect the interest earned on unredeemed matured positions. This /// interest is split between the LPs and governance. /// @param _vaultSharePrice The current vault share price. @@ -484,6 +496,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { } } + // TODO: Do a rounding pass here. + // /// @dev Calculates the number of share reserves that are not reserved by /// open positions. /// @param _vaultSharePrice The current vault share price. @@ -504,6 +518,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { return idleShares; } + // TODO: Do a rounding pass here. + // /// @dev Calculates the LP share price. /// @param _vaultSharePrice The current vault share price. /// @return lpSharePrice The LP share price in units of (base / lp shares). @@ -524,6 +540,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { return lpSharePrice; } + // TODO: Do a rounding pass here. + // /// @dev Calculates the fees that go to the LPs and governance. /// @param _shareAmount The amount of shares exchanged for bonds. /// @param _spotPrice The price without slippage of bonds in terms of base @@ -567,6 +585,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { governanceCurveFee = curveFee.mulDown(_governanceLPFee); } + // TODO: Do a rounding pass here. + // /// @dev Calculates the fees that go to the LPs and governance. /// @param _bondAmount The amount of bonds being exchanged for shares. /// @param _normalizedTimeRemaining The normalized amount of time until @@ -641,6 +661,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { flatFee.mulDown(_governanceLPFee); } + // TODO: Do a rounding pass here. + // /// @dev Converts input to base if necessary according to what is specified /// in options. /// @param _amount The amount to convert. @@ -659,6 +681,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { } } + // TODO: Do a rounding pass here. + // /// @dev Converts input to what is specified in the options from base. /// @param _amount The amount to convert. /// @param _vaultSharePrice The current vault share price. diff --git a/contracts/src/internal/HyperdriveCheckpoint.sol b/contracts/src/internal/HyperdriveCheckpoint.sol index 5e9458327..bda5d8f18 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -68,6 +68,8 @@ abstract contract HyperdriveCheckpoint is } } + // TODO: Do a rounding pass here. + // /// @dev Creates a new checkpoint if necessary. /// @param _checkpointTime The time of the checkpoint to create. /// @param _vaultSharePrice The current vault share price. @@ -129,7 +131,8 @@ abstract contract HyperdriveCheckpoint is uint256 shareReservesDelta = maturedShortsAmount.divDown( _vaultSharePrice ); - shareProceeds = HyperdriveMath.calculateShortProceeds( + // NOTE: Round down to underestimate the short proceeds. + shareProceeds = HyperdriveMath.calculateShortProceedsDown( maturedShortsAmount, shareReservesDelta, openVaultSharePrice, @@ -205,6 +208,8 @@ abstract contract HyperdriveCheckpoint is return _vaultSharePrice; } + // TODO: Do a rounding pass here. + // /// @dev Calculates the proceeds of the holders of a given position at /// maturity. /// @param _bondAmount The bond amount of the position. diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index 25d4ce710..e109708bb 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -109,6 +109,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return lpShares; } + // TODO: Do a rounding pass on this function. + // /// @dev Allows LPs to supply liquidity for LP shares. /// @param _contribution The amount to supply. /// @param _minLpSharePrice The minimum LP share price the LP is willing @@ -230,6 +232,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { ); } + // TODO: Do a rounding pass on this function. + // /// @dev Allows an LP to burn shares and withdraw from the pool. /// @param _lpShares The LP shares to burn. /// @param _minOutputPerShare The minimum amount of base per LP share that @@ -300,6 +304,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return (proceeds, withdrawalShares); } + // TODO: Do a rounding pass on this function. + // /// @dev Redeems withdrawal shares by giving the LP a pro-rata amount of the /// withdrawal pool's proceeds. This function redeems the maximum /// amount of the specified withdrawal shares given the amount of @@ -352,6 +358,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return (proceeds, withdrawalSharesRedeemed); } + // TODO: Do a rounding pass on this function. + // /// @dev Redeems withdrawal shares by giving the LP a pro-rata amount of the /// withdrawal pool's proceeds. This function redeems the maximum /// amount of the specified withdrawal shares given the amount of @@ -411,6 +419,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return (proceeds, withdrawalSharesRedeemed); } + // TODO: Do a rounding pass on this function. + // /// @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. @@ -449,6 +459,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { _updateLiquidity(-int256(shareProceeds)); } + // TODO: Do a rounding pass on this function. + // /// @dev Updates the pool's liquidity and holds the pool's spot price constant. /// @param _shareReservesDelta The delta that should be applied to share reserves. function _updateLiquidity(int256 _shareReservesDelta) internal { diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 759dc34f7..3e2502b0d 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -20,6 +20,8 @@ abstract contract HyperdriveLong is HyperdriveLP { using SafeCast for uint256; using SafeCast for int256; + // TODO: Do a rounding pass here. + // /// @dev Opens a long position. /// @param _amount The amount to open a long with. /// @param _minOutput The minimum number of bonds to receive. @@ -123,6 +125,8 @@ abstract contract HyperdriveLong is HyperdriveLP { return (maturityTime, _bondProceeds); } + // TODO: Do a rounding pass here. + // /// @dev Closes a long position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. @@ -225,6 +229,8 @@ abstract contract HyperdriveLong is HyperdriveLP { return proceeds; } + // TODO: Do a rounding pass here. + // /// @dev Applies an open long to the state. This includes updating the /// reserves and maintaining the reserve invariants. /// @param _shareReservesDelta The amount of shares paid to the curve. @@ -276,6 +282,8 @@ abstract contract HyperdriveLong is HyperdriveLP { _distributeExcessIdle(_vaultSharePrice); } + // TODO: Do a rounding pass here. + // /// @dev Applies the trading deltas from a closed long to the reserves and /// the withdrawal pool. /// @param _bondAmount The amount of longs that were closed. @@ -343,6 +351,8 @@ abstract contract HyperdriveLong is HyperdriveLP { } } + // TODO: Do a rounding pass here. + // /// @dev Calculate the pool reserve and trader deltas that result from /// opening a long. This calculation includes trading fees. /// @param _shareAmount The amount of shares being paid to open the long. @@ -453,6 +463,8 @@ abstract contract HyperdriveLong is HyperdriveLP { ); } + // TODO: Do a rounding pass here. + // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a long. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the short. diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 0fa19854a..bc1d1f216 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -20,6 +20,8 @@ abstract contract HyperdriveShort is HyperdriveLP { using SafeCast for uint256; using SafeCast for int256; + // TODO: Do a rounding pass here. + // /// @dev Opens a short position. /// @param _bondAmount The amount of bonds to short. /// @param _maxDeposit The most the user expects to deposit for this trade. @@ -127,6 +129,8 @@ abstract contract HyperdriveShort is HyperdriveLP { return (maturityTime, traderDeposit); } + // TODO: Do a rounding pass here. + // /// @notice Closes a short position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. @@ -233,6 +237,8 @@ abstract contract HyperdriveShort is HyperdriveLP { return proceeds; } + // TODO: Do a rounding pass here. + // /// @dev Applies an open short to the state. This includes updating the /// reserves and maintaining the reserve invariants. /// @param _bondAmount The amount of bonds shorted. @@ -297,6 +303,8 @@ abstract contract HyperdriveShort is HyperdriveLP { _distributeExcessIdle(_vaultSharePrice); } + // TODO: Do a rounding pass here. + // /// @dev Applies the trading deltas from a closed short to the reserves and /// the withdrawal pool. /// @param _bondAmount The amount of shorts that were closed. @@ -335,6 +343,8 @@ abstract contract HyperdriveShort is HyperdriveLP { _marketState.bondReserves -= _bondReservesDelta.toUint128(); } + // TODO: Do a rounding pass here. + // /// @dev Calculate the pool reserve and trader deltas that result from /// opening a short. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being sold to open the short. @@ -410,6 +420,8 @@ abstract contract HyperdriveShort is HyperdriveLP { // shares -= shares - shares shareReservesDelta -= curveFee - governanceCurveFee; + // NOTE: Round up to overestimate the base deposit. + // // The trader will need to deposit capital to pay for the fixed rate, // the curve fee, the flat fee, and any back-paid interest that will be // received back upon closing the trade. If negative interest has @@ -418,7 +430,7 @@ abstract contract HyperdriveShort is HyperdriveLP { // don't benefit from negative interest that accrued during the current // checkpoint. baseDeposit = HyperdriveMath - .calculateShortProceeds( + .calculateShortProceedsUp( _bondAmount, // NOTE: We add the governance fee back to the share reserves // delta here because the trader will need to provide this in @@ -434,6 +446,8 @@ abstract contract HyperdriveShort is HyperdriveLP { return (baseDeposit, shareReservesDelta, governanceCurveFee); } + // TODO: Do a rounding pass here. + // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a short. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the @@ -556,6 +570,8 @@ abstract contract HyperdriveShort is HyperdriveLP { ? _vaultSharePrice : _checkpoints[_maturityTime].vaultSharePrice; + // NOTE: Round down to underestimate the short proceeds. + // // Calculate the share proceeds owed to the short. We calculate this // before scaling the share payment for negative interest. Shorts // are responsible for paying for 100% of the negative interest, so @@ -563,7 +579,7 @@ abstract contract HyperdriveShort is HyperdriveLP { // negative interest. Similarly, the governance fee is included in // the share payment. The LPs don't receive the governance fee, but // the short is responsible for paying it. - shareProceeds = HyperdriveMath.calculateShortProceeds( + shareProceeds = HyperdriveMath.calculateShortProceedsDown( _bondAmount, shareReservesDelta, openVaultSharePrice, diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 9dcdf2f27..6ab023c4b 100644 --- a/contracts/src/libraries/HyperdriveMath.sol +++ b/contracts/src/libraries/HyperdriveMath.sol @@ -17,7 +17,8 @@ library HyperdriveMath { using FixedPointMath for int256; using SafeCast for uint256; - /// @dev Calculates the spot price of bonds in terms of base. + /// @dev Calculates the spot price of bonds in terms of base. This + /// calculation underestimates the pool's spot price. /// @param _effectiveShareReserves The pool's effective share reserves. The /// effective share reserves are a modified version of the share /// reserves used when pricing trades. @@ -31,6 +32,8 @@ library HyperdriveMath { uint256 _initialVaultSharePrice, uint256 _timeStretch ) internal pure returns (uint256 spotPrice) { + // NOTE: Round down to underestimate the spot price. + // // p = (y / (mu * (z - zeta))) ** -t_s // = ((mu * (z - zeta)) / y) ** t_s spotPrice = _initialVaultSharePrice @@ -38,7 +41,8 @@ library HyperdriveMath { .pow(_timeStretch); } - /// @dev Calculates the spot APR of the pool. + /// @dev Calculates the spot APR of the pool. This calculation + /// underestimates the pool's spot APR. /// @param _effectiveShareReserves The pool's effective share reserves. The /// effective share reserves are a modified version of the share /// reserves used when pricing trades. @@ -54,12 +58,14 @@ library HyperdriveMath { uint256 _positionDuration, uint256 _timeStretch ) internal pure returns (uint256 apr) { + // NOTE: Round down to underestimate the spot APR. + // // We are interested calculating the fixed APR for the pool. The // annualized rate is given by the following formula: // // r = (1 - p) / (p * t) // - // where t = _positionDuration / 365 + // where t = _positionDuration / 365. uint256 spotPrice = calculateSpotPrice( _effectiveShareReserves, _bondReserves, @@ -68,6 +74,7 @@ library HyperdriveMath { ); return (ONE - spotPrice).divDown( + // NOTE: Round up since this is in the denominator. spotPrice.mulDivUp(_positionDuration, 365 days) ); } @@ -96,7 +103,8 @@ library HyperdriveMath { /// the codebase, the bond reserves used include the LP share /// adjustment specified in YieldSpace. The bond reserves returned by /// this function are unadjusted which makes it easier to calculate the - /// initial LP shares. + /// initial LP shares. This calculation underestimates the pool's + /// initial bond reserves. /// @param _effectiveShareReserves The pool's effective share reserves. The /// effective share reserves are a modified version of the share /// reserves used when pricing trades. @@ -113,13 +121,30 @@ library HyperdriveMath { uint256 _positionDuration, uint256 _timeStretch ) internal pure returns (uint256 bondReserves) { - // NOTE: Using divDown to convert to fixed point format. + // NOTE: Round down to underestimate the initial bond reserves. + // + // Normalize the time to maturity to fractions of a year since the + // provided rate is an APR. uint256 t = _positionDuration.divDown(365 days); - // mu * (z - zeta) * (1 + apr * t) ** (1 / tau) + // NOTE: Round down to underestimate the initial bond reserves. + // + // inner = (1 + apr * t) ** (1 / t_s) + uint256 inner = ONE + _apr.mulDown(t); + if (inner >= ONE) { + // Rounding down the exponent results in a smaller result. + inner = inner.pow(ONE.divDown(_timeStretch)); + } else { + // Rounding up the exponent results in a smaller result. + inner = inner.pow(ONE.divUp(_timeStretch)); + } + + // NOTE: Round down to underestimate the initial bond reserves. + // + // mu * (z - zeta) * (1 + apr * t) ** (1 / t_s) return _initialVaultSharePrice.mulDown(_effectiveShareReserves).mulDown( - (ONE + _apr.mulDown(t)).pow(ONE.divUp(_timeStretch)) + inner ); } @@ -137,6 +162,8 @@ library HyperdriveMath { /// share price. In the event that the interest is negative and /// outweighs the trading profits and margin released, the short's /// proceeds are marked to zero. + /// + /// This variant of the calculation overestimates the short proceeds. /// @param _bondAmount The amount of bonds underlying the closed short. /// @param _shareAmount The amount of shares that it costs to close the /// short. @@ -145,7 +172,7 @@ library HyperdriveMath { /// @param _vaultSharePrice The current vault share price. /// @param _flatFee The flat fee currently within the pool /// @return shareProceeds The short proceeds in shares. - function calculateShortProceeds( + function calculateShortProceedsUp( uint256 _bondAmount, uint256 _shareAmount, uint256 _openVaultSharePrice, @@ -153,23 +180,95 @@ library HyperdriveMath { uint256 _vaultSharePrice, uint256 _flatFee ) internal pure returns (uint256 shareProceeds) { + // NOTE: Round up to overestimate the short proceeds. + // + // The total value is the amount of shares that underlies the bonds that + // were shorted. The bonds start by being backed 1:1 with base, and the + // total value takes into account all of the interest that has accrued + // since the short was opened. + // + // total_value = (c1 / (c0 * c)) * dy + uint256 totalValue = _bondAmount + .mulDivUp(_closeVaultSharePrice, _openVaultSharePrice) + .divUp(_vaultSharePrice); + + // NOTE: Round up to overestimate the short proceeds. + // + // We increase the total value by the flat fee amount, because it is + // included in the total amount of capital underlying the short. + totalValue += _bondAmount.mulDivUp(_flatFee, _vaultSharePrice); + // If the interest is more negative than the trading profits and margin // released, then the short proceeds are marked to zero. Otherwise, we // calculate the proceeds as the sum of the trading proceeds, the // interest proceeds, and the margin released. - uint256 bondFactor = _bondAmount + if (totalValue > _shareAmount) { + // proceeds = (c1 / (c0 * c)) * dy - dz + shareProceeds = totalValue - _shareAmount; + } + + return shareProceeds; + } + + /// @dev Calculates the proceeds in shares of closing a short position. This + /// takes into account the trading profits, the interest that was + /// earned by the short, the flat fee the short pays, and the amount of + /// margin that was released by closing the short. The math for the + /// short's proceeds in base is given by: + /// + /// proceeds = (1 + flat_fee) * dy - c * dz + (c1 - c0) * (dy / c0) + /// = (1 + flat_fee) * dy - c * dz + (c1 / c0) * dy - dy + /// = (c1 / c0 + flat_fee) * dy - c * dz + /// + /// We convert the proceeds to shares by dividing by the current vault + /// share price. In the event that the interest is negative and + /// outweighs the trading profits and margin released, the short's + /// proceeds are marked to zero. + /// + /// This variant of the calculation underestimates the short proceeds. + /// @param _bondAmount The amount of bonds underlying the closed short. + /// @param _shareAmount The amount of shares that it costs to close the + /// short. + /// @param _openVaultSharePrice The vault share price at the short's open. + /// @param _closeVaultSharePrice The vault share price at the short's close. + /// @param _vaultSharePrice The current vault share price. + /// @param _flatFee The flat fee currently within the pool + /// @return shareProceeds The short proceeds in shares. + function calculateShortProceedsDown( + uint256 _bondAmount, + uint256 _shareAmount, + uint256 _openVaultSharePrice, + uint256 _closeVaultSharePrice, + uint256 _vaultSharePrice, + uint256 _flatFee + ) internal pure returns (uint256 shareProceeds) { + // NOTE: Round down to underestimate the short proceeds. + // + // The total value is the amount of shares that underlies the bonds that + // were shorted. The bonds start by being backed 1:1 with base, and the + // total value takes into account all of the interest that has accrued + // since the short was opened. + // + // total_value = (c1 / (c0 * c)) * dy + uint256 totalValue = _bondAmount .mulDivDown(_closeVaultSharePrice, _openVaultSharePrice) .divDown(_vaultSharePrice); - // We increase the bondFactor by the flat fee amount, because the trader - // has provided the flat fee as margin, and so it must be returned to - // them if it's not charged. - bondFactor += _bondAmount.mulDivDown(_flatFee, _vaultSharePrice); + // NOTE: Round down to underestimate the short proceeds. + // + // We increase the total value by the flat fee amount, because it is + // included in the total amount of capital underlying the short. + totalValue += _bondAmount.mulDivDown(_flatFee, _vaultSharePrice); - if (bondFactor > _shareAmount) { - // proceeds = (c1 / c0 * c) * dy - dz - shareProceeds = bondFactor - _shareAmount; + // If the interest is more negative than the trading profits and margin + // released, then the short proceeds are marked to zero. Otherwise, we + // calculate the proceeds as the sum of the trading proceeds, the + // interest proceeds, and the margin released. + if (totalValue > _shareAmount) { + // proceeds = (c1 / (c0 * c)) * dy - dz + shareProceeds = totalValue - _shareAmount; } + return shareProceeds; } @@ -182,6 +281,7 @@ library HyperdriveMath { /// /// p_max = (1 - phi_f) / (1 + phi_c * (1 / p_0 - 1) * (1 - phi_f)) /// + /// We underestimate the maximum spot price to be conservative. /// @param _startingSpotPrice The spot price at the start of the trade. /// @param _curveFee The curve fee. /// @param _flatFee The flat fee. @@ -191,8 +291,10 @@ library HyperdriveMath { uint256 _curveFee, uint256 _flatFee ) internal pure returns (uint256) { + // NOTE: Round down to underestimate the maximum spot price. return (ONE - _flatFee).divDown( + // NOTE: Round up since this is in the denominator. ONE + _curveFee.mulUp(ONE.divUp(_startingSpotPrice) - ONE).mulUp( ONE - _flatFee @@ -209,6 +311,7 @@ library HyperdriveMath { /// /// p_max = 1 - phi_c * (1 - p_0) /// + /// We underestimate the maximum spot price to be conservative. /// @param _startingSpotPrice The spot price at the start of the trade. /// @param _curveFee The curve fee. /// @return The maximum spot price. @@ -216,6 +319,7 @@ library HyperdriveMath { uint256 _startingSpotPrice, uint256 _curveFee ) internal pure returns (uint256) { + // Round the rhs down to underestimate the maximum spot price. return ONE - _curveFee.mulUp(ONE - _startingSpotPrice); } @@ -287,6 +391,9 @@ library HyperdriveMath { uint256 shareProceeds ) { + // NOTE: We underestimate the trader's share proceeds to avoid sandwich + // attacks. + // // We consider `(1 - timeRemaining) * amountIn` of the bonds to be fully // matured and timeRemaining * amountIn of the bonds to be newly // minted. The fully matured bonds are redeemed one-to-one to base @@ -301,8 +408,8 @@ library HyperdriveMath { // Calculate the curved part of the trade. bondCurveDelta = _amountIn.mulDown(_normalizedTimeRemaining); - // NOTE: We underestimate the trader's share proceeds to avoid - // sandwich attacks. + // NOTE: Round the `shareCurveDelta` down to underestimate the + // share proceeds. shareCurveDelta = YieldSpaceMath.calculateSharesOutGivenBondsInDown( _effectiveShareReserves, _bondReserves, @@ -384,6 +491,8 @@ library HyperdriveMath { uint256 sharePayment ) { + // NOTE: We overestimate the trader's share payment to avoid sandwiches. + // // Since we are buying bonds, it's possible that `timeRemaining < 1`. // We consider `(1 - timeRemaining) * amountOut` of the bonds being // purchased to be fully matured and `timeRemaining * amountOut of the @@ -392,15 +501,15 @@ library HyperdriveMath { // the one-to-one redemption by the vault share price) and the newly // minted bonds are traded on a YieldSpace curve configured to // timeRemaining = 1. - sharePayment = _amountOut.mulDivDown( + sharePayment = _amountOut.mulDivUp( ONE - _normalizedTimeRemaining, _vaultSharePrice ); if (_normalizedTimeRemaining > 0) { bondCurveDelta = _amountOut.mulDown(_normalizedTimeRemaining); - // NOTE: We overestimate the trader's share payment to avoid - // sandwiches. + // NOTE: Round the `shareCurveDelta` up to overestimate the share + // payment. shareCurveDelta = YieldSpaceMath.calculateSharesInGivenBondsOutUp( _effectiveShareReserves, _bondReserves, @@ -433,6 +542,10 @@ library HyperdriveMath { /// /// shareAdjustmentDelta = min(c_1 / c_0, 1) * sharePayment - /// shareReservesDelta + /// + /// We underestimate the share proceeds to avoid sandwiches, and we + /// round the share reserves delta and share adjustment in the same + /// direction for consistency. /// @param _shareProceeds The proceeds in shares from the trade. /// @param _shareReservesDelta The change in share reserves from the trade. /// @param _shareCurveDelta The curve portion of the change in share reserves. @@ -471,6 +584,8 @@ library HyperdriveMath { // shareCurveDelta int256 shareAdjustmentDelta; if (_closeVaultSharePrice < _openVaultSharePrice) { + // NOTE: Round down to underestimate the share proceeds. + // // We only need to scale the proceeds in the case that we're closing // a long since `calculateShortProceeds` accounts for negative // interest. @@ -481,6 +596,8 @@ library HyperdriveMath { ); } + // NOTE: Round down to underestimate the quantities. + // // Scale the other values. _shareReservesDelta = _shareReservesDelta.mulDivDown( _closeVaultSharePrice, diff --git a/contracts/src/libraries/LPMath.sol b/contracts/src/libraries/LPMath.sol index 717bce4bc..33bb72add 100644 --- a/contracts/src/libraries/LPMath.sol +++ b/contracts/src/libraries/LPMath.sol @@ -31,8 +31,6 @@ library LPMath { /// to short-circuit. uint256 internal constant MAX_SHARE_RESERVES_DELTA_MIN_TOLERANCE = 1e6; - // TODO: Evaluate the rounding. - // /// @dev Calculates the new share reserves, share adjustment, and bond /// reserves after liquidity is added or removed from the pool. This /// update is made in such a way that the pool's spot price remains @@ -84,6 +82,8 @@ library LPMath { // => // zeta_new = zeta_old * (z_new / z_old) if (_shareAdjustment >= 0) { + // NOTE: Rounding down to avoid introducing dust into the + // computation. shareAdjustment = int256( uint256(shareReserves).mulDivDown( uint256(_shareAdjustment), @@ -91,6 +91,8 @@ library LPMath { ) ); } else { + // NOTE: Rounding down to avoid introducing dust into the + // computation. shareAdjustment = -int256( uint256(shareReserves).mulDivDown( uint256(-_shareAdjustment), @@ -99,6 +101,8 @@ library LPMath { ); } + // NOTE: Rounding down to avoid introducing dust into the computation. + // // The liquidity update should hold the spot price invariant. The spot // price of base in terms of bonds is given by: // diff --git a/contracts/test/MockHyperdriveMath.sol b/contracts/test/MockHyperdriveMath.sol index d5c7aeeb8..661b9de77 100644 --- a/contracts/test/MockHyperdriveMath.sol +++ b/contracts/test/MockHyperdriveMath.sol @@ -229,7 +229,7 @@ contract MockHyperdriveMath { return result; } - function calculateShortProceeds( + function calculateShortProceedsUp( uint256 _bondAmount, uint256 _shareAmount, uint256 _openVaultSharePrice, @@ -237,7 +237,26 @@ contract MockHyperdriveMath { uint256 _vaultSharePrice, uint256 _flatFee ) external pure returns (uint256) { - uint256 result = HyperdriveMath.calculateShortProceeds( + uint256 result = HyperdriveMath.calculateShortProceedsUp( + _bondAmount, + _shareAmount, + _openVaultSharePrice, + _closeVaultSharePrice, + _vaultSharePrice, + _flatFee + ); + return result; + } + + function calculateShortProceedsDown( + uint256 _bondAmount, + uint256 _shareAmount, + uint256 _openVaultSharePrice, + uint256 _closeVaultSharePrice, + uint256 _vaultSharePrice, + uint256 _flatFee + ) external pure returns (uint256) { + uint256 result = HyperdriveMath.calculateShortProceedsDown( _bondAmount, _shareAmount, _openVaultSharePrice, diff --git a/test/integrations/hyperdrive/VariableInterestShortTest.t.sol b/test/integrations/hyperdrive/VariableInterestShortTest.t.sol index 4f6368bc3..99f3e068d 100644 --- a/test/integrations/hyperdrive/VariableInterestShortTest.t.sol +++ b/test/integrations/hyperdrive/VariableInterestShortTest.t.sol @@ -197,6 +197,11 @@ contract VariableInterestShortTest is HyperdriveTest { int256 preTradeVariableInterest, int256 variableInterest ) external { + // FIXME + initialVaultSharePrice = 34778671436198528228969157798148911829006461373551783056986145839822293636051; + preTradeVariableInterest = 102440065373578136490305101693001783737566310; + variableInterest = -218271149486368278932534866513971484546; + // Fuzz inputs // initialVaultSharePrice [0.1,5] // preTradeVariableInterest [-50,50] diff --git a/test/units/hyperdrive/CloseShortTest.t.sol b/test/units/hyperdrive/CloseShortTest.t.sol index 465c81e91..e419ebd88 100644 --- a/test/units/hyperdrive/CloseShortTest.t.sol +++ b/test/units/hyperdrive/CloseShortTest.t.sol @@ -809,11 +809,11 @@ contract CloseShortTest is HyperdriveTest { } // Verify that the proceeds are about the same. - assertApproxEqAbs(shortProceeds1, shortProceeds2, 60 wei); - assertApproxEqAbs(shortProceeds1, shortProceeds3, 6.2e9); + assertApproxEqAbs(shortProceeds1, shortProceeds2, 106 wei); + assertApproxEqAbs(shortProceeds1, shortProceeds3, 9.8e9); // NOTE: This is a large tolerance, but it is explained in issue #691. - assertApproxEqAbs(shortProceeds1, shortProceeds4, 3.6e18); + assertApproxEqAbs(shortProceeds1, shortProceeds4, 5.3e18); assertGe(shortProceeds1, shortProceeds4); } diff --git a/test/units/libraries/HyperdriveMath.t.sol b/test/units/libraries/HyperdriveMath.t.sol index bcb20049f..56224791f 100644 --- a/test/units/libraries/HyperdriveMath.t.sol +++ b/test/units/libraries/HyperdriveMath.t.sol @@ -1180,7 +1180,7 @@ contract HyperdriveMathTest is HyperdriveTest { closeShort(bob, maturityTime, maxShort); } - function test__calculateShortProceeds() external { + function test__calculateShortProceedsUp() external { // NOTE: Coverage only works if I initialize the fixture in the test function MockHyperdriveMath hyperdriveMath = new MockHyperdriveMath(); @@ -1191,7 +1191,7 @@ contract HyperdriveMathTest is HyperdriveTest { uint256 closeVaultSharePrice = 1e18; uint256 vaultSharePrice = 1e18; uint256 flatFee = 0; - uint256 shortProceeds = hyperdriveMath.calculateShortProceeds( + uint256 shortProceeds = hyperdriveMath.calculateShortProceedsUp( bondAmount, shareAmount, openVaultSharePrice, @@ -1209,7 +1209,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.05e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsUp( bondAmount, shareAmount, openVaultSharePrice, @@ -1231,7 +1231,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.05e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsUp( bondAmount, shareAmount, openVaultSharePrice, @@ -1253,7 +1253,181 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.155e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsUp( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price = (0.05 + 1.05 * 0.05) / 1.155 + assertApproxEqAbs( + shortProceeds, + (0.05e18 + bondAmount.mulDown(0.05e18)).divDown(vaultSharePrice), + 2 + ); + + // -10% interest - 5% margin released - 0% interest after close + bondAmount = 1.05e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 0.9e18; + vaultSharePrice = 0.9e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0; + shortProceeds = hyperdriveMath.calculateShortProceedsUp( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + assertEq(shortProceeds, 0); + + // -10% interest - 5% margin released - 20% interest after close + bondAmount = 1.05e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 0.9e18; + vaultSharePrice = 1.08e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0; + shortProceeds = hyperdriveMath.calculateShortProceedsUp( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + assertEq(shortProceeds, 0); + + // 5% interest - 0% margin released - 0% interest after close + // 50% flatFee applied + bondAmount = 1e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 1.05e18; + vaultSharePrice = 1.05e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0.5e18; + shortProceeds = hyperdriveMath.calculateShortProceedsUp( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price + // + (bondAmount * flatFee) / vault_share_price + // = (0 + 1 * 0.05) / 1.05 + (1 * 0.5) / 1.05 + assertApproxEqAbs( + shortProceeds, + (bondAmount.mulDown(0.05e18)).divDown(vaultSharePrice) + + (bondAmount.mulDivDown(flatFee, vaultSharePrice)), + 2 + ); + + // 5% interest - 5% margin released - 0% interest after close + bondAmount = 1.05e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 1.05e18; + vaultSharePrice = 1.05e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0.25e18; + shortProceeds = hyperdriveMath.calculateShortProceedsUp( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price + // + (bondAmount * flatFee) / vault_share_price + // = ((0.05 + 1.05 * 0.05) / 1.05) + ((1 * 0.25) / 1.05) + assertApproxEqAbs( + shortProceeds, + (0.05e18 + bondAmount.mulDown(0.05e18)).divDown(vaultSharePrice) + + bondAmount.mulDivDown(flatFee, vaultSharePrice), + 1 + ); + } + + function test__calculateShortProceedsDown() external { + // NOTE: Coverage only works if I initialize the fixture in the test function + MockHyperdriveMath hyperdriveMath = new MockHyperdriveMath(); + + // 0% interest - 5% margin released - 0% interest after close + uint256 bondAmount = 1.05e18; + uint256 shareAmount = 1e18; + uint256 openVaultSharePrice = 1e18; + uint256 closeVaultSharePrice = 1e18; + uint256 vaultSharePrice = 1e18; + uint256 flatFee = 0; + uint256 shortProceeds = hyperdriveMath.calculateShortProceedsDown( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price = (0.05 + 0) / 1 + assertEq(shortProceeds, 0.05e18); + + // 5% interest - 5% margin released - 0% interest after close + bondAmount = 1.05e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 1.05e18; + vaultSharePrice = 1.05e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0; + shortProceeds = hyperdriveMath.calculateShortProceedsDown( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price = (0.05 + 1.05 * 0.05) / 1.05 + assertApproxEqAbs( + shortProceeds, + (0.05e18 + bondAmount.mulDown(0.05e18)).divDown(vaultSharePrice), + 1 + ); + + // 5% interest - 0% margin released - 0% interest after close + bondAmount = 1e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 1.05e18; + vaultSharePrice = 1.05e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0; + shortProceeds = hyperdriveMath.calculateShortProceedsDown( + bondAmount, + shareAmount, + openVaultSharePrice, + closeVaultSharePrice, + vaultSharePrice, + flatFee + ); + // proceeds = (margin + interest) / vault_share_price = (0 + 1 * 0.05) / 1.05 + assertApproxEqAbs( + shortProceeds, + (bondAmount.mulDown(0.05e18)).divDown(vaultSharePrice), + 1 + ); + + // 5% interest - 5% margin released - 10% interest after close + bondAmount = 1.05e18; + openVaultSharePrice = 1e18; + closeVaultSharePrice = 1.05e18; + vaultSharePrice = 1.155e18; + shareAmount = uint256(1e18).divDown(vaultSharePrice); + flatFee = 0; + shortProceeds = hyperdriveMath.calculateShortProceedsDown( bondAmount, shareAmount, openVaultSharePrice, @@ -1275,7 +1449,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 0.9e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsDown( bondAmount, shareAmount, openVaultSharePrice, @@ -1292,7 +1466,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.08e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsDown( bondAmount, shareAmount, openVaultSharePrice, @@ -1310,7 +1484,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.05e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0.5e18; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsDown( bondAmount, shareAmount, openVaultSharePrice, @@ -1335,7 +1509,7 @@ contract HyperdriveMathTest is HyperdriveTest { vaultSharePrice = 1.05e18; shareAmount = uint256(1e18).divDown(vaultSharePrice); flatFee = 0.25e18; - shortProceeds = hyperdriveMath.calculateShortProceeds( + shortProceeds = hyperdriveMath.calculateShortProceedsDown( bondAmount, shareAmount, openVaultSharePrice, diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index 8a686c799..829fb62fc 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -856,9 +856,8 @@ contract HyperdriveTest is BaseTest { variableRate, timeElapsed ); - int256 delta = int256( - shortAmount - poolInfo.vaultSharePrice.mulDown(expectedSharePayment) - ); + int256 delta = int256(shortAmount) - + int256(poolInfo.vaultSharePrice.mulDown(expectedSharePayment)); if (delta + expectedInterest > 0) { return uint256(delta + expectedInterest); } else { diff --git a/test/utils/HyperdriveUtils.sol b/test/utils/HyperdriveUtils.sol index 89cf76f3a..577c1e952 100644 --- a/test/utils/HyperdriveUtils.sol +++ b/test/utils/HyperdriveUtils.sol @@ -30,7 +30,7 @@ library HyperdriveUtils { timeRemaining = _maturityTime > latestCheckpoint(_hyperdrive) ? _maturityTime - latestCheckpoint(_hyperdrive) : 0; - timeRemaining = (timeRemaining).divDown( + timeRemaining = timeRemaining.divDown( _hyperdrive.getPoolConfig().positionDuration ); return timeRemaining; From 0e3bf78db51191f73730e08a6dca50d8aae5e108 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 20:38:00 -0600 Subject: [PATCH 3/9] Did a rounding pass on `HyperdriveLong` --- contracts/src/internal/HyperdriveLong.sol | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 3e2502b0d..1696b9886 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -20,8 +20,6 @@ abstract contract HyperdriveLong is HyperdriveLP { using SafeCast for uint256; using SafeCast for int256; - // TODO: Do a rounding pass here. - // /// @dev Opens a long position. /// @param _amount The amount to open a long with. /// @param _minOutput The minimum number of bonds to receive. @@ -58,6 +56,9 @@ abstract contract HyperdriveLong is HyperdriveLP { // against the minimum transaction amount because in the event of // slippage on the deposit, we want the inputs to the state updates to // respect the minimum transaction amount requirements. + // + // NOTE: Round down to underestimate the base deposit. This makes the + // minimum transaction amount check more conservative. uint256 baseDeposited = sharesDeposited.mulDown(vaultSharePrice); if (baseDeposited < _minimumTransactionAmount) { revert IHyperdrive.MinimumTransactionAmount(); @@ -125,8 +126,6 @@ abstract contract HyperdriveLong is HyperdriveLP { return (maturityTime, _bondProceeds); } - // TODO: Do a rounding pass here. - // /// @dev Closes a long position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. @@ -229,8 +228,6 @@ abstract contract HyperdriveLong is HyperdriveLP { return proceeds; } - // TODO: Do a rounding pass here. - // /// @dev Applies an open long to the state. This includes updating the /// reserves and maintaining the reserve invariants. /// @param _shareReservesDelta The amount of shares paid to the curve. @@ -282,8 +279,6 @@ abstract contract HyperdriveLong is HyperdriveLP { _distributeExcessIdle(_vaultSharePrice); } - // TODO: Do a rounding pass here. - // /// @dev Applies the trading deltas from a closed long to the reserves and /// the withdrawal pool. /// @param _bondAmount The amount of longs that were closed. @@ -351,8 +346,6 @@ abstract contract HyperdriveLong is HyperdriveLP { } } - // TODO: Do a rounding pass here. - // /// @dev Calculate the pool reserve and trader deltas that result from /// opening a long. This calculation includes trading fees. /// @param _shareAmount The amount of shares being paid to open the long. @@ -435,6 +428,8 @@ abstract contract HyperdriveLong is HyperdriveLP { // bonds = bonds + bonds bondReservesDelta = bondProceeds + governanceCurveFee; + // NOTE: Round down to underestimate the governance fee. + // // Calculate the fees owed to governance in shares. Open longs are // calculated entirely on the curve so the curve fee is the total // governance fee. In order to convert it to shares we need to multiply @@ -463,8 +458,6 @@ abstract contract HyperdriveLong is HyperdriveLP { ); } - // TODO: Do a rounding pass here. - // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a long. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the short. From 5d4b08717b25638ac7f75810f9570a1826abe830 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 20:46:44 -0600 Subject: [PATCH 4/9] Did a rounding pass on `HyperdriveShort` --- contracts/src/internal/HyperdriveShort.sol | 18 ++++-------------- .../hyperdrive/NonstandardDecimals.sol | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index bc1d1f216..44f957630 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -20,8 +20,6 @@ abstract contract HyperdriveShort is HyperdriveLP { using SafeCast for uint256; using SafeCast for int256; - // TODO: Do a rounding pass here. - // /// @dev Opens a short position. /// @param _bondAmount The amount of bonds to short. /// @param _maxDeposit The most the user expects to deposit for this trade. @@ -129,8 +127,6 @@ abstract contract HyperdriveShort is HyperdriveLP { return (maturityTime, traderDeposit); } - // TODO: Do a rounding pass here. - // /// @notice Closes a short position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. @@ -237,8 +233,6 @@ abstract contract HyperdriveShort is HyperdriveLP { return proceeds; } - // TODO: Do a rounding pass here. - // /// @dev Applies an open short to the state. This includes updating the /// reserves and maintaining the reserve invariants. /// @param _bondAmount The amount of bonds shorted. @@ -303,8 +297,6 @@ abstract contract HyperdriveShort is HyperdriveLP { _distributeExcessIdle(_vaultSharePrice); } - // TODO: Do a rounding pass here. - // /// @dev Applies the trading deltas from a closed short to the reserves and /// the withdrawal pool. /// @param _bondAmount The amount of shorts that were closed. @@ -343,8 +335,6 @@ abstract contract HyperdriveShort is HyperdriveLP { _marketState.bondReserves -= _bondReservesDelta.toUint128(); } - // TODO: Do a rounding pass here. - // /// @dev Calculate the pool reserve and trader deltas that result from /// opening a short. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being sold to open the short. @@ -379,10 +369,12 @@ abstract contract HyperdriveShort is HyperdriveLP { _initialVaultSharePrice ); + // NOTE: Round up to make the check stricter. + // // If the base proceeds of selling the bonds is greater than the bond // amount, then the trade occurred in the negative interest domain. We // revert in these pathological cases. - if (shareReservesDelta.mulDown(_vaultSharePrice) > _bondAmount) { + if (shareReservesDelta.mulUp(_vaultSharePrice) > _bondAmount) { revert IHyperdrive.NegativeInterest(); } @@ -441,13 +433,11 @@ abstract contract HyperdriveShort is HyperdriveLP { _vaultSharePrice, _flatFee ) - .mulDown(_vaultSharePrice); + .mulUp(_vaultSharePrice); return (baseDeposit, shareReservesDelta, governanceCurveFee); } - // TODO: Do a rounding pass here. - // /// @dev Calculate the pool reserve and trader deltas that result from /// closing a short. This calculation includes trading fees. /// @param _bondAmount The amount of bonds being purchased to close the diff --git a/test/integrations/hyperdrive/NonstandardDecimals.sol b/test/integrations/hyperdrive/NonstandardDecimals.sol index b10b325d4..bc92c06a3 100644 --- a/test/integrations/hyperdrive/NonstandardDecimals.sol +++ b/test/integrations/hyperdrive/NonstandardDecimals.sol @@ -54,7 +54,7 @@ contract NonstandardDecimalsTest is HyperdriveTest { (, uint256 shortBasePaid) = openShort(celine, bondAmount); // Ensure that the long and short fixed interest are equal. - assertApproxEqAbs(bondAmount - longBasePaid, shortBasePaid, 2); + assertApproxEqAbs(bondAmount - longBasePaid, shortBasePaid, 3); } function test_nonstandard_decimals_longs_outstanding() external { From 6413f4855a9901a61a5bc74537be3d692bd14452 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 21:01:40 -0600 Subject: [PATCH 5/9] Did a rounding pass on `HyperdriveLP` --- contracts/src/internal/HyperdriveLP.sol | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index e109708bb..35efdb63e 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -109,8 +109,6 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return lpShares; } - // TODO: Do a rounding pass on this function. - // /// @dev Allows LPs to supply liquidity for LP shares. /// @param _contribution The amount to supply. /// @param _minLpSharePrice The minimum LP share price the LP is willing @@ -186,6 +184,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { params.bondReserves = _marketState.bondReserves; endingPresentValue = LPMath.calculatePresentValue(params); + // NOTE: Round down to underestimate the amount of LP shares minted. + // // The LP shares minted to the LP is derived by solving for the // change in LP shares that preserves the ratio of present value to // total LP shares. This ensures that LPs are fairly rewarded for @@ -203,6 +203,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { } } + // NOTE: Round down to make the check more conservative. + // // Enforce the minimum LP share price slippage guard. if (_contribution.divDown(lpShares) < _minLpSharePrice) { revert IHyperdrive.InvalidLpSharePrice(); @@ -216,7 +218,7 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { // Emit an AddLiquidity event. uint256 lpSharePrice = lpTotalSupply == 0 - ? 0 + ? 0 // NOTE: We always round the LP share price down for consistency. : startingPresentValue.divDown(lpTotalSupply); uint256 baseContribution = _convertToBaseFromOption( _contribution, @@ -232,8 +234,6 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { ); } - // TODO: Do a rounding pass on this function. - // /// @dev Allows an LP to burn shares and withdraw from the pool. /// @param _lpShares The LP shares to burn. /// @param _minOutputPerShare The minimum amount of base per LP share that @@ -304,8 +304,6 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return (proceeds, withdrawalShares); } - // TODO: Do a rounding pass on this function. - // /// @dev Redeems withdrawal shares by giving the LP a pro-rata amount of the /// withdrawal pool's proceeds. This function redeems the maximum /// amount of the specified withdrawal shares given the amount of @@ -358,8 +356,6 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { return (proceeds, withdrawalSharesRedeemed); } - // TODO: Do a rounding pass on this function. - // /// @dev Redeems withdrawal shares by giving the LP a pro-rata amount of the /// withdrawal pool's proceeds. This function redeems the maximum /// amount of the specified withdrawal shares given the amount of @@ -395,6 +391,8 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { withdrawalSharesRedeemed ); + // NOTE: Round down to underestimate the share proceeds. + // // The LP gets the pro-rata amount of the collected proceeds. uint128 proceeds_ = _withdrawPool.proceeds; uint256 shareProceeds = withdrawalSharesRedeemed.mulDivDown( @@ -411,16 +409,16 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { // Withdraw the share proceeds to the user. proceeds = _withdraw(shareProceeds, _sharePrice, _options); + // NOTE: Round up to make the check more conservative. + // // Enforce the minimum user output per share. - if (_minOutputPerShare.mulDown(withdrawalSharesRedeemed) > proceeds) { + if (_minOutputPerShare.mulUp(withdrawalSharesRedeemed) > proceeds) { revert IHyperdrive.OutputLimit(); } return (proceeds, withdrawalSharesRedeemed); } - // TODO: Do a rounding pass on this function. - // /// @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. @@ -459,8 +457,6 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken { _updateLiquidity(-int256(shareProceeds)); } - // TODO: Do a rounding pass on this function. - // /// @dev Updates the pool's liquidity and holds the pool's spot price constant. /// @param _shareReservesDelta The delta that should be applied to share reserves. function _updateLiquidity(int256 _shareReservesDelta) internal { From e275edea2ae4e094ee415a34ca5c833d06314bf1 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 21:17:55 -0600 Subject: [PATCH 6/9] Did a rounding pass on `HyperdriveBase` --- contracts/src/internal/HyperdriveBase.sol | 68 +++++++++++-------- .../src/internal/HyperdriveCheckpoint.sol | 15 ++-- contracts/src/libraries/YieldSpaceMath.sol | 4 ++ .../VariableInterestShortTest.t.sol | 5 -- .../hyperdrive/RemoveLiquidityTest.t.sol | 2 +- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index d13e0e23e..712013a15 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -183,8 +183,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { /// Helpers /// - // TODO: Do a rounding pass here. - // /// @dev Calculates the normalized time remaining of a position. /// @param _maturityTime The maturity time of the position. /// @return timeRemaining The normalized time remaining (in [0, 1]). @@ -195,11 +193,11 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; + + // NOTE: Round down to underestimate the time remaining. timeRemaining = timeRemaining.divDown(_positionDuration); } - // TODO: Do a rounding pass here. - // /// @dev Calculates the normalized time remaining of a position when the /// maturity time is scaled up 18 decimals. /// @param _maturityTime The maturity time of the position. @@ -210,6 +208,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; + + // NOTE: Round down to underestimate the time remaining. timeRemaining = timeRemaining.divDown(_positionDuration * ONE); } @@ -266,8 +266,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { ); } - // TODO: Do a rounding pass here. - // /// @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. @@ -283,8 +281,10 @@ abstract contract HyperdriveBase is HyperdriveStorage { uint256 startingPresentValue = LPMath.calculatePresentValue( presentValueParams ); + // NOTE: For consistency with the present value calculation, we round + // up the long side and round down the short side. int256 netCurveTrade = int256( - presentValueParams.longsOutstanding.mulDown( + presentValueParams.longsOutstanding.mulUp( presentValueParams.longAverageTimeRemaining ) ) - @@ -355,17 +355,17 @@ abstract contract HyperdriveBase is HyperdriveStorage { return endingSpotPrice > _maxSpotPrice; } - // TODO: Do a rounding pass here. - // /// @dev Check solvency by verifying that the share reserves are greater /// than the exposure plus the minimum share reserves. /// @param _vaultSharePrice The current vault share price. /// @return True if the share reserves are greater than the exposure plus /// the minimum share reserves. function _isSolvent(uint256 _vaultSharePrice) internal view returns (bool) { + // NOTE: Round the lhs up and the rhs down to make the strict more + // conservative. return int256( - (uint256(_marketState.shareReserves).mulDown(_vaultSharePrice)) + (uint256(_marketState.shareReserves).mulUp(_vaultSharePrice)) ) - int128(_marketState.longExposure) >= int256(_minimumShareReserves.mulDown(_vaultSharePrice)); @@ -387,8 +387,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { } } - // TODO: Do a rounding pass here. - // /// @dev Apply the updates to the market state as a result of closing a /// position after maturity. This function also adjusts the proceeds /// to account for any negative interest that has accrued in the @@ -407,6 +405,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { uint256 zombieBaseReserves ) = _collectZombieInterest(_vaultSharePrice); + // NOTE: Round down to underestimate the proceeds. + // // If negative interest has accrued in the zombie reserves, we // discount the share proceeds in proportion to the amount of // negative interest that has accrued. @@ -436,8 +436,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { return _shareProceeds; } - // TODO: Do a rounding pass here. - // /// @dev Collect the interest earned on unredeemed matured positions. This /// interest is split between the LPs and governance. /// @param _vaultSharePrice The current vault share price. @@ -451,6 +449,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { internal returns (uint256 zombieBaseProceeds, uint256 zombieBaseReserves) { + // NOTE: Round down to underestimate the proceeds. + // // Get the zombie base proceeds and reserves. zombieBaseReserves = _vaultSharePrice.mulDown( _marketState.zombieShareReserves @@ -464,11 +464,17 @@ abstract contract HyperdriveBase is HyperdriveStorage { // difference between the base reserves and the base proceeds. uint256 zombieInterest = zombieBaseReserves - zombieBaseProceeds; + // NOTE: Round up to overestimate the impact that removing the + // interest had on the zombie share reserves. + // // Remove the zombie interest from the zombie share reserves. _marketState.zombieShareReserves -= zombieInterest .divUp(_vaultSharePrice) .toUint128(); + // NOTE: Round down to underestimate the zombhie interest given to + // the LPs and governance. + // // Calculate and collect the governance fee. // The fee is calculated in terms of shares and paid to // governance. @@ -496,8 +502,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { } } - // TODO: Do a rounding pass here. - // /// @dev Calculates the number of share reserves that are not reserved by /// open positions. /// @param _vaultSharePrice The current vault share price. @@ -506,7 +510,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { function _calculateIdleShareReserves( uint256 _vaultSharePrice ) internal view returns (uint256 idleShares) { - uint256 longExposure = uint256(_marketState.longExposure).divDown( + // NOTE: Round up to underestimate the pool's idle. + uint256 longExposure = uint256(_marketState.longExposure).divUp( _vaultSharePrice ); if (_marketState.shareReserves > longExposure + _minimumShareReserves) { @@ -518,14 +523,13 @@ abstract contract HyperdriveBase is HyperdriveStorage { return idleShares; } - // TODO: Do a rounding pass here. - // /// @dev Calculates the LP share price. /// @param _vaultSharePrice The current vault share price. /// @return lpSharePrice The LP share price in units of (base / lp shares). function _calculateLPSharePrice( uint256 _vaultSharePrice ) internal view returns (uint256 lpSharePrice) { + // NOTE: Round down to underestimate the LP share price. uint256 presentValue = _vaultSharePrice > 0 ? LPMath .calculatePresentValue(_getPresentValueParams(_vaultSharePrice)) @@ -535,13 +539,11 @@ abstract contract HyperdriveBase is HyperdriveStorage { _totalSupply[AssetId._WITHDRAWAL_SHARE_ASSET_ID] - _withdrawPool.readyToWithdraw; lpSharePrice = lpTotalSupply == 0 - ? 0 + ? 0 // NOTE: Round down to underestimate the LP share price. : presentValue.divDown(lpTotalSupply); return lpSharePrice; } - // TODO: Do a rounding pass here. - // /// @dev Calculates the fees that go to the LPs and governance. /// @param _shareAmount The amount of shares exchanged for bonds. /// @param _spotPrice The price without slippage of bonds in terms of base @@ -555,6 +557,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { uint256 _spotPrice, uint256 _vaultSharePrice ) internal view returns (uint256 curveFee, uint256 governanceCurveFee) { + // NOTE: Round down to underestimate the curve fee. + // // Fixed Rate (r) = (value at maturity - purchase price)/(purchase price) // = (1-p)/p // = ((1 / p) - 1) @@ -579,14 +583,14 @@ abstract contract HyperdriveBase is HyperdriveStorage { .mulDown(_vaultSharePrice) .mulDown(_shareAmount); + // NOTE: Round down to underestimate the governance curve fee. + // // We leave the governance fee in terms of bonds: // governanceCurveFee = curve_fee * p * phi_gov // = bonds * phi_gov governanceCurveFee = curveFee.mulDown(_governanceLPFee); } - // TODO: Do a rounding pass here. - // /// @dev Calculates the fees that go to the LPs and governance. /// @param _bondAmount The amount of bonds being exchanged for shares. /// @param _normalizedTimeRemaining The normalized amount of time until @@ -613,10 +617,12 @@ abstract contract HyperdriveBase is HyperdriveStorage { uint256 totalGovernanceFee ) { + // NOTE: Round down to underestimate the curve fee. + // // p (spot price) tells us how many base a bond is worth -> p = base/bonds // 1 - p tells us how many additional base a bond is worth at // maturity -> (1 - p) = additional base/bonds - + // // The curve fee is taken from the additional base the user gets for // each bond at maturity: // @@ -630,12 +636,16 @@ abstract contract HyperdriveBase is HyperdriveStorage { .mulDown(_bondAmount) .mulDivDown(_normalizedTimeRemaining, _vaultSharePrice); + // NOTE: Round down to underestimate the governance curve fee. + // // Calculate the curve portion of the governance fee: // // governanceCurveFee = curve_fee * phi_gov // = shares * phi_gov governanceCurveFee = curveFee.mulDown(_governanceLPFee); + // NOTE: Round down to underestimate the flat fee. + // // The flat portion of the fee is taken from the matured bonds. // Since a matured bond is worth 1 base, it is appropriate to consider // d_y in units of base: @@ -650,6 +660,8 @@ abstract contract HyperdriveBase is HyperdriveStorage { ); flatFee = flat.mulDown(_flatFee); + // NOTE: Round down to underestimate the total governance fee. + // // We calculate the flat portion of the governance fee as: // // governance_flat_fee = flat_fee * phi_gov @@ -661,8 +673,6 @@ abstract contract HyperdriveBase is HyperdriveStorage { flatFee.mulDown(_governanceLPFee); } - // TODO: Do a rounding pass here. - // /// @dev Converts input to base if necessary according to what is specified /// in options. /// @param _amount The amount to convert. @@ -677,12 +687,11 @@ abstract contract HyperdriveBase is HyperdriveStorage { if (_options.asBase) { return _amount; } else { + // NOTE: Round down to underestimate the base amount. return _amount.mulDown(_vaultSharePrice); } } - // TODO: Do a rounding pass here. - // /// @dev Converts input to what is specified in the options from base. /// @param _amount The amount to convert. /// @param _vaultSharePrice The current vault share price. @@ -696,6 +705,7 @@ abstract contract HyperdriveBase is HyperdriveStorage { if (_options.asBase) { return _amount; } else { + // NOTE: Round down to underestimate the shares amount. return _amount.divDown(_vaultSharePrice); } } diff --git a/contracts/src/internal/HyperdriveCheckpoint.sol b/contracts/src/internal/HyperdriveCheckpoint.sol index bda5d8f18..577763c35 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -68,8 +68,6 @@ abstract contract HyperdriveCheckpoint is } } - // TODO: Do a rounding pass here. - // /// @dev Creates a new checkpoint if necessary. /// @param _checkpointTime The time of the checkpoint to create. /// @param _vaultSharePrice The current vault share price. @@ -128,7 +126,8 @@ abstract contract HyperdriveCheckpoint is int256(shareProceeds), // keep the effective share reserves constant _checkpointTime ); - uint256 shareReservesDelta = maturedShortsAmount.divDown( + // NOTE: Round up to underestimate the short proceeds. + uint256 shareReservesDelta = maturedShortsAmount.divUp( _vaultSharePrice ); // NOTE: Round down to underestimate the short proceeds. @@ -140,6 +139,7 @@ abstract contract HyperdriveCheckpoint is _vaultSharePrice, _flatFee ); + // NOTE: Round down to underestimate the short proceeds. _marketState.zombieBaseProceeds += shareProceeds .mulDown(_vaultSharePrice) .toUint112(); @@ -173,6 +173,7 @@ abstract contract HyperdriveCheckpoint is int256(shareProceeds), // keep the effective share reserves constant checkpointTime ); + // NOTE: Round down to underestimate the long proceeds. _marketState.zombieBaseProceeds += shareProceeds .mulDown(_vaultSharePrice) .toUint112(); @@ -208,8 +209,6 @@ abstract contract HyperdriveCheckpoint is return _vaultSharePrice; } - // TODO: Do a rounding pass here. - // /// @dev Calculates the proceeds of the holders of a given position at /// maturity. /// @param _bondAmount The bond amount of the position. @@ -228,6 +227,9 @@ abstract contract HyperdriveCheckpoint is // Calculate the share proceeds, flat fee, and governance fee. Since the // position is closed at maturity, the share proceeds are equal to the // bond amount divided by the vault share price. + // + // NOTE: Round down to underestimate the share proceeds, flat fee, and + // governance fee. shareProceeds = _bondAmount.divDown(_vaultSharePrice); uint256 flatFee = shareProceeds.mulDown(_flatFee); governanceFee = flatFee.mulDown(_governanceLPFee); @@ -254,10 +256,13 @@ abstract contract HyperdriveCheckpoint is // governance fee are given a "haircut" proportional to the negative // interest that accrued. if (_vaultSharePrice < _openVaultSharePrice) { + // NOTE: Round down to underestimate the proceeds. shareProceeds = shareProceeds.mulDivDown( _vaultSharePrice, _openVaultSharePrice ); + + // NOTE: Round down to underestimate the governance fee. governanceFee = governanceFee.mulDivDown( _vaultSharePrice, _openVaultSharePrice diff --git a/contracts/src/libraries/YieldSpaceMath.sol b/contracts/src/libraries/YieldSpaceMath.sol index 2c8757390..15b524e0c 100644 --- a/contracts/src/libraries/YieldSpaceMath.sol +++ b/contracts/src/libraries/YieldSpaceMath.sol @@ -401,6 +401,8 @@ library YieldSpaceMath { uint256 c, uint256 mu ) internal pure returns (uint256) { + // NOTE: Rounding up to overestimate the result. + // /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) return c.mulDivUp(mu.mulUp(ze).pow(t), mu) + y.pow(t); } @@ -423,6 +425,8 @@ library YieldSpaceMath { uint256 c, uint256 mu ) internal pure returns (uint256) { + // NOTE: Rounding down to underestimate the result. + // /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t) return c.mulDivDown(mu.mulDown(ze).pow(t), mu) + y.pow(t); } diff --git a/test/integrations/hyperdrive/VariableInterestShortTest.t.sol b/test/integrations/hyperdrive/VariableInterestShortTest.t.sol index 99f3e068d..4f6368bc3 100644 --- a/test/integrations/hyperdrive/VariableInterestShortTest.t.sol +++ b/test/integrations/hyperdrive/VariableInterestShortTest.t.sol @@ -197,11 +197,6 @@ contract VariableInterestShortTest is HyperdriveTest { int256 preTradeVariableInterest, int256 variableInterest ) external { - // FIXME - initialVaultSharePrice = 34778671436198528228969157798148911829006461373551783056986145839822293636051; - preTradeVariableInterest = 102440065373578136490305101693001783737566310; - variableInterest = -218271149486368278932534866513971484546; - // Fuzz inputs // initialVaultSharePrice [0.1,5] // preTradeVariableInterest [-50,50] diff --git a/test/units/hyperdrive/RemoveLiquidityTest.t.sol b/test/units/hyperdrive/RemoveLiquidityTest.t.sol index 53d7985b7..9265d08b0 100644 --- a/test/units/hyperdrive/RemoveLiquidityTest.t.sol +++ b/test/units/hyperdrive/RemoveLiquidityTest.t.sol @@ -223,7 +223,7 @@ contract RemoveLiquidityTest is HyperdriveTest { assertApproxEqAbs( testCase.initialLpBaseProceeds, expectedBaseProceeds, - 1 + 2 ); { assertEq( From 6f76c90ccb1a3fd58f60f6158c5850d21d4d70a3 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 22:03:37 -0600 Subject: [PATCH 7/9] Did a rounding pass of `FixedPointMath` --- contracts/src/libraries/FixedPointMath.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/src/libraries/FixedPointMath.sol b/contracts/src/libraries/FixedPointMath.sol index 5188233b0..8f6fc7558 100644 --- a/contracts/src/libraries/FixedPointMath.sol +++ b/contracts/src/libraries/FixedPointMath.sol @@ -309,8 +309,9 @@ library FixedPointMath { // average = (totalWeight * average + deltaWeight * delta) / // (totalWeight + deltaWeight) if (_isAdding) { + // NOTE: Round down to underestimate the average. average = (_totalWeight.mulDown(_average) + - _deltaWeight.mulDown(_delta)).divUp( + _deltaWeight.mulDown(_delta)).divDown( _totalWeight + _deltaWeight ); @@ -335,9 +336,13 @@ library FixedPointMath { // average = (totalWeight * average - deltaWeight * delta) / // (totalWeight - deltaWeight) else { - if (_totalWeight == _deltaWeight) return 0; + if (_totalWeight == _deltaWeight) { + return 0; + } + + // NOTE: Round down to underestimate the average. average = (_totalWeight.mulDown(_average) - - _deltaWeight.mulDown(_delta)).divUp( + _deltaWeight.mulUp(_delta)).divDown( _totalWeight - _deltaWeight ); } From db0e07f7c0e616cc6ce95bdab47c22c293c57035 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 22 Jan 2024 22:05:36 -0600 Subject: [PATCH 8/9] Did a rounding pass of `ERC4626Base` and `StETHBase` --- contracts/src/instances/erc4626/ERC4626Base.sol | 2 ++ contracts/src/instances/steth/StETHBase.sol | 2 ++ 2 files changed, 4 insertions(+) diff --git a/contracts/src/instances/erc4626/ERC4626Base.sol b/contracts/src/instances/erc4626/ERC4626Base.sol index 51c0952cc..105955642 100644 --- a/contracts/src/instances/erc4626/ERC4626Base.sol +++ b/contracts/src/instances/erc4626/ERC4626Base.sol @@ -93,6 +93,8 @@ abstract contract ERC4626Base is HyperdriveBase { uint256 _sharePrice, IHyperdrive.Options calldata _options ) internal override returns (uint256 amountWithdrawn) { + // NOTE: Round down to underestimate the base proceeds. + // // Correct for any error that crept into the calculation of the share // amount by converting the shares to base and then back to shares // using the vault's share conversion logic. diff --git a/contracts/src/instances/steth/StETHBase.sol b/contracts/src/instances/steth/StETHBase.sol index 296a83f61..712003312 100644 --- a/contracts/src/instances/steth/StETHBase.sol +++ b/contracts/src/instances/steth/StETHBase.sol @@ -115,6 +115,8 @@ abstract contract StETHBase is HyperdriveBase { revert IHyperdrive.UnsupportedToken(); } + // NOTE: Round down to underestimate the base proceeds. + // // Correct for any error that crept into the calculation of the share // amount by converting the shares to base and then back to shares // using the vault's share conversion logic. From 0fba78baaa16beacdcb77bea789880c91ae63c6b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 23 Jan 2024 11:13:40 -0600 Subject: [PATCH 9/9] Fixed the Rust test and addressed review from @jrhea --- contracts/src/internal/HyperdriveBase.sol | 2 +- crates/hyperdrive-math/src/lp.rs | 28 ++++++++++++++++------- crates/hyperdrive-math/src/short/close.rs | 9 ++++++-- crates/hyperdrive-math/src/utils.rs | 25 ++++++++++++++++---- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 712013a15..369450655 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -361,7 +361,7 @@ abstract contract HyperdriveBase is HyperdriveStorage { /// @return True if the share reserves are greater than the exposure plus /// the minimum share reserves. function _isSolvent(uint256 _vaultSharePrice) internal view returns (bool) { - // NOTE: Round the lhs up and the rhs down to make the strict more + // NOTE: Round the lhs up and the rhs down to make the check more // conservative. return int256( diff --git a/crates/hyperdrive-math/src/lp.rs b/crates/hyperdrive-math/src/lp.rs index 465016614..ba045e193 100644 --- a/crates/hyperdrive-math/src/lp.rs +++ b/crates/hyperdrive-math/src/lp.rs @@ -41,6 +41,12 @@ impl State { long_average_time_remaining: FixedPoint, short_average_time_remaining: FixedPoint, ) -> I256 { + // NOTE: To underestimate the impact of closing the net curve position, + // we round up the long side of the net curve position (since this + // results in a larger value removed from the share reserves) and round + // down the short side of the net curve position (since this results in + // a smaller value added to the share reserves). + // // The net curve position is the net of the longs and shorts that are // currently tradeable on the curve. Given the amount of outstanding // longs `y_l` and shorts `y_s` as well as the average time remaining @@ -48,13 +54,12 @@ impl State { // compute the net curve position as: // // netCurveTrade = y_l * t_l - y_s * t_s. - let net_curve_position: I256 = I256::from( - self.longs_outstanding() - .mul_down(long_average_time_remaining), - ) - I256::from( - self.shorts_outstanding() - .mul_down(short_average_time_remaining), - ); + let net_curve_position: I256 = + I256::from(self.longs_outstanding().mul_up(long_average_time_remaining)) + - I256::from( + self.shorts_outstanding() + .mul_down(short_average_time_remaining), + ); // If the net curve position is positive, then the pool is net long. // Closing the net curve position results in the longs being paid out @@ -78,6 +83,9 @@ impl State { I256::from(self.calculate_shares_in_given_bonds_out_up(_net_curve_position)) } else { let max_share_payment = self.calculate_max_buy_shares_in(); + + // 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) @@ -94,12 +102,16 @@ impl State { long_average_time_remaining: FixedPoint, short_average_time_remaining: FixedPoint, ) -> I256 { + // NOTE: In order to underestimate the impact of closing all of the + // flat trades, we round the impact of closing the shorts down and round + // the impact of closing the longs up. + // // Compute the net of the longs and shorts that will be traded flat and // apply this net to the reserves. I256::from(self.shorts_outstanding().mul_div_down( fixed!(1e18) - short_average_time_remaining, self.vault_share_price(), - )) - I256::from(self.longs_outstanding().mul_div_down( + )) - I256::from(self.longs_outstanding().mul_div_up( fixed!(1e18) - long_average_time_remaining, self.vault_share_price(), )) diff --git a/crates/hyperdrive-math/src/short/close.rs b/crates/hyperdrive-math/src/short/close.rs index ff9545476..918274cfd 100644 --- a/crates/hyperdrive-math/src/short/close.rs +++ b/crates/hyperdrive-math/src/short/close.rs @@ -12,14 +12,19 @@ impl State { let bond_amount = bond_amount.into(); let normalized_time_remaining = normalized_time_remaining.into(); + // NOTE: We overestimate the trader's share payment to avoid sandwiches. + // // Calculate the flat part of the trade - let flat = bond_amount.mul_div_down( + let flat = bond_amount.mul_div_up( fixed!(1e18) - normalized_time_remaining, self.vault_share_price(), ); // Calculate the curve part of the trade let curve = if normalized_time_remaining > fixed!(0) { + // NOTE: Round the `shareCurveDelta` up to overestimate the share + // payment. + // let curve_bonds_in = bond_amount * normalized_time_remaining; self.calculate_shares_in_given_bonds_out_up(curve_bonds_in) } else { @@ -121,7 +126,7 @@ mod tests { ) }); match mock - .calculate_short_proceeds( + .calculate_short_proceeds_down( bond_amount.into(), share_amount.into(), open_vault_share_price.into(), diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index 309cf428c..af2304d4d 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -89,13 +89,30 @@ pub fn calculate_initial_bond_reserves( position_duration: FixedPoint, time_stretch: FixedPoint, ) -> FixedPoint { - let annualized_time = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); + // NOTE: Round down to underestimate the initial bond reserves. + // + // Normalize the time to maturity to fractions of a year since the provided + // rate is an APR. + let t = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); + + // NOTE: Round down to underestimate the initial bond reserves. + // + // inner = (1 + apr * t) ** (1 / t_s) + let mut inner = fixed!(1e18) + apr.mul_down(t); + if inner >= fixed!(1e18) { + // Rounding down the exponent results in a smaller result. + inner = inner.pow(fixed!(1e18) / time_stretch); + } else { + // Rounding up the exponent results in a smaller result. + inner = inner.pow(fixed!(1e18).div_up(time_stretch)); + } + + // NOTE: Round down to underestimate the initial bond reserves. + // // mu * (z - zeta) * (1 + apr * t) ** (1 / tau) initial_vault_share_price .mul_down(effective_share_reserves) - .mul_down( - (fixed!(1e18) + apr.mul_down(annualized_time)).pow(fixed!(1e18).div_up(time_stretch)), - ) + .mul_down(inner) } #[cfg(test)]