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. diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 13b40d45d..369450655 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -193,7 +193,9 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; - timeRemaining = (timeRemaining).divDown(_positionDuration); + + // NOTE: Round down to underestimate the time remaining. + timeRemaining = timeRemaining.divDown(_positionDuration); } /// @dev Calculates the normalized time remaining of a position when the @@ -206,7 +208,9 @@ abstract contract HyperdriveBase is HyperdriveStorage { timeRemaining = _maturityTime > latestCheckpoint ? _maturityTime - latestCheckpoint : 0; - timeRemaining = (timeRemaining).divDown(_positionDuration * ONE); + + // NOTE: Round down to underestimate the time remaining. + timeRemaining = timeRemaining.divDown(_positionDuration * ONE); } /// @dev Gets the most recent checkpoint time. @@ -277,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,9 +361,11 @@ 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 check more + // conservative. return int256( - (uint256(_marketState.shareReserves).mulDown(_vaultSharePrice)) + (uint256(_marketState.shareReserves).mulUp(_vaultSharePrice)) ) - int128(_marketState.longExposure) >= int256(_minimumShareReserves.mulDown(_vaultSharePrice)); @@ -397,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. @@ -439,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 @@ -452,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. @@ -492,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) { @@ -510,6 +529,7 @@ abstract contract HyperdriveBase is HyperdriveStorage { 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)) @@ -519,7 +539,7 @@ 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; } @@ -537,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) @@ -561,6 +583,8 @@ 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 @@ -593,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: // @@ -610,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: @@ -630,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 @@ -655,6 +687,7 @@ abstract contract HyperdriveBase is HyperdriveStorage { if (_options.asBase) { return _amount; } else { + // NOTE: Round down to underestimate the base amount. return _amount.mulDown(_vaultSharePrice); } } @@ -672,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 5e9458327..577763c35 100644 --- a/contracts/src/internal/HyperdriveCheckpoint.sol +++ b/contracts/src/internal/HyperdriveCheckpoint.sol @@ -126,10 +126,12 @@ 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 ); - shareProceeds = HyperdriveMath.calculateShortProceeds( + // NOTE: Round down to underestimate the short proceeds. + shareProceeds = HyperdriveMath.calculateShortProceedsDown( maturedShortsAmount, shareReservesDelta, openVaultSharePrice, @@ -137,6 +139,7 @@ abstract contract HyperdriveCheckpoint is _vaultSharePrice, _flatFee ); + // NOTE: Round down to underestimate the short proceeds. _marketState.zombieBaseProceeds += shareProceeds .mulDown(_vaultSharePrice) .toUint112(); @@ -170,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(); @@ -223,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); @@ -249,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/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index 25d4ce710..35efdb63e 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -184,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 @@ -201,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(); @@ -214,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, @@ -387,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( @@ -403,8 +409,10 @@ 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(); } diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 759dc34f7..1696b9886 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -56,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(); @@ -425,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 diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 0fa19854a..44f957630 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -369,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(); } @@ -410,6 +412,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 +422,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 @@ -429,7 +433,7 @@ abstract contract HyperdriveShort is HyperdriveLP { _vaultSharePrice, _flatFee ) - .mulDown(_vaultSharePrice); + .mulUp(_vaultSharePrice); return (baseDeposit, shareReservesDelta, governanceCurveFee); } @@ -556,6 +560,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 +569,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/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 ); } diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 6e6ee6e7d..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,10 +58,14 @@ 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: + // 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, @@ -66,6 +74,7 @@ library HyperdriveMath { ); return (ONE - spotPrice).divDown( + // NOTE: Round up since this is in the denominator. spotPrice.mulDivUp(_positionDuration, 365 days) ); } @@ -94,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. @@ -111,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 ); } @@ -135,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. @@ -143,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, @@ -151,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; } @@ -180,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. @@ -189,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 @@ -207,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. @@ -214,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); } @@ -285,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 @@ -299,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, @@ -382,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 @@ -390,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, @@ -431,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. @@ -469,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. @@ -479,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 22bd49bfc..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: // @@ -136,10 +140,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 +157,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 +197,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 +205,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 +219,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 +342,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 +358,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 +365,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 +383,7 @@ library LPMath { ) ) - int256( - _params.longsOutstanding.mulDivDown( + _params.longsOutstanding.mulDivUp( ONE - _params.longAverageTimeRemaining, _params.vaultSharePrice ) @@ -818,10 +829,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; 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/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/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)] 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 { 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/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( 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;