From fa9461134cfb03a94446eecf6ef37f5fcd4dee20 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Feb 2023 11:33:34 -0600 Subject: [PATCH 1/2] Calculate the average maturity time of long and short positions --- contracts/Hyperdrive.sol | 120 ++++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 19 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 7a6b798a9..461fbaf2a 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -21,37 +21,37 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// Tokens /// - // @dev The base asset. + // @notice The base asset. IERC20 public immutable baseToken; /// Time /// - // @dev The amount of seconds between share price checkpoints. + // @notice The amount of seconds between share price checkpoints. uint256 public immutable checkpointDuration; - // @dev The amount of seconds that elapse before a bond can be redeemed. + // @notice The amount of seconds that elapse before a bond can be redeemed. uint256 public immutable positionDuration; - // @dev A parameter that decreases slippage around a target rate. + // @notice A parameter that decreases slippage around a target rate. uint256 public immutable timeStretch; /// Market state /// - // @dev The share price at the time the pool was created. + // @notice The share price at the time the pool was created. uint256 public immutable initialSharePrice; - /// @dev Checkpoints of historical share prices. + /// @notice Checkpoints of historical share prices. mapping(uint256 => uint256) public checkpoints; - /// @dev The share reserves. The share reserves multiplied by the share price - /// give the base reserves, so shares are a mechanism of ensuring that - /// interest is properly awarded over time. + /// @notice The share reserves. The share reserves multiplied by the share + /// price give the base reserves, so shares are a mechanism of + /// ensuring that interest is properly awarded over time. uint256 public shareReserves; - /// @dev The bond reserves. In Hyperdrive, the bond reserves aren't backed by - /// pre-minted bonds and are instead used as a virtual value that - /// ensures that the spot rate changes according to the laws of supply - /// and demand. + /// @notice The bond reserves. In Hyperdrive, the bond reserves aren't + /// backed by pre-minted bonds and are instead used as a virtual + /// value that ensures that the spot rate changes according to the + /// laws of supply and demand. uint256 public bondReserves; /// @notice The amount of longs that are still open. @@ -60,6 +60,12 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @notice The amount of shorts that are still open. uint256 public shortsOutstanding; + /// @notice The average maturity time of long positions. + uint256 public longAverageMaturityTime; + + /// @notice The average maturity time of short positions. + uint256 public shortAverageMaturityTime; + /// @notice The amount of long withdrawal shares that haven't been paid out. uint256 public longWithdrawalSharesOutstanding; @@ -72,10 +78,16 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @notice The proceeds that have accrued to the short withdrawal shares. uint256 public shortWithdrawalShareProceeds; - // @notice the fee paramater to apply to the curve portion of the hyperdrive trade equation. + // TODO: Should this be immutable? + // + /// @notice The fee paramater to apply to the curve portion of the + /// hyperdrive trade equation. uint256 public curveFee; - // @notice the fee paramater to apply to the flat portion of the hyperdrive trade equation. + // TODO: Should this be immutable? + // + /// @notice The fee paramater to apply to the flat portion of the hyperdrive + /// trade equation. uint256 public flatFee; /// @notice Initializes a Hyperdrive pool. @@ -451,6 +463,15 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Enforce min user outputs if (_minOutput > bondProceeds) revert Errors.OutputLimit(); + // Update the average maturity time of long positions. + longAverageMaturityTime = _calculateAverageMaturityTime( + longsOutstanding, + bondProceeds, + longAverageMaturityTime, + maturityTime, + true + ); + // Apply the trading deltas to the reserves and update the amount of // longs outstanding. shareReserves += shares; @@ -632,6 +653,15 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { deposit(userDeposit); // max_loss + interest } + // Update the average maturity time of long positions. + shortAverageMaturityTime = _calculateAverageMaturityTime( + shortsOutstanding, + _bondAmount, + shortAverageMaturityTime, + maturityTime, + true + ); + // Apply the trading deltas to the reserves and increase the bond buffer // by the amount of bonds that were shorted. We don't need to add the // margin or pre-paid interest to the reserves because of the way that @@ -707,7 +737,8 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { _bondAmount, poolBondDelta, sharePayment, - sharePrice + sharePrice, + _maturityTime ); } else { // Perform a checkpoint for the short's maturity time. This ensures @@ -828,7 +859,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @param _shareProceeds The proceeds in shares received from closing the /// long. /// @param _sharePrice The current share price. - /// @param _maturityTime The maturity time of the longs. + /// @param _maturityTime The maturity time of the long. function _applyCloseLong( uint256 _bondAmount, uint256 _poolBondDelta, @@ -836,6 +867,15 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { uint256 _sharePrice, uint256 _maturityTime ) internal { + // Update the long average maturity time. + longAverageMaturityTime = _calculateAverageMaturityTime( + longsOutstanding, + _bondAmount, + longAverageMaturityTime, + _maturityTime, + false + ); + // Reduce the amount of outstanding longs. longsOutstanding -= _bondAmount; @@ -917,12 +957,23 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// pool. /// @param _sharePayment The payment in shares required to close the short. /// @param _sharePrice The current share price. + /// @param _maturityTime The maturity time of the short. function _applyCloseShort( uint256 _bondAmount, uint256 _poolBondDelta, uint256 _sharePayment, - uint256 _sharePrice + uint256 _sharePrice, + uint256 _maturityTime ) internal { + // Update the short average maturity time. + shortAverageMaturityTime = _calculateAverageMaturityTime( + shortsOutstanding, + _bondAmount, + shortAverageMaturityTime, + _maturityTime, + false + ); + // Decrease the amount of shorts outstanding. shortsOutstanding -= _bondAmount; @@ -1032,7 +1083,8 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { maturedShortsAmount, 0, maturedShortsAmount, - _sharePrice + _sharePrice, + _checkpointTime ); } @@ -1068,6 +1120,36 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { return proceeds; } + /// @dev Calculate a new average maturity time when positions are opened or + /// closed. + /// @param _positionsOutstanding The amount of positions outstanding. + /// @param _positionAmount The position balance being opened or closed. + /// @param _averageMaturityTime The average maturity time of positions. + /// @param _positionMaturityTime The maturity time of the position being + /// opened or closed. + /// @param _isOpen A flag indicating that the position is being opened if + /// true and that the position is being closed if false. + /// @return averageMaturityTime The updated average maturity time. + function _calculateAverageMaturityTime( + uint256 _positionsOutstanding, + uint256 _positionAmount, + uint256 _averageMaturityTime, + uint256 _positionMaturityTime, + bool _isOpen + ) internal pure returns (uint256 averageMaturityTime) { + if (_isOpen) { + return + (_positionsOutstanding.mulDown(_averageMaturityTime)) + .add(_positionAmount.mulDown(_positionMaturityTime)) + .divDown(_positionsOutstanding.add(_positionAmount)); + } else { + return + (_positionsOutstanding.mulDown(_averageMaturityTime)) + .sub(_positionAmount.mulDown(_positionMaturityTime)) + .divDown(_positionsOutstanding.sub(_positionAmount)); + } + } + /// @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]). From 4f73c1f68ad6548aedab724ac450608d83b22958 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Feb 2023 11:55:16 -0600 Subject: [PATCH 2/2] Added tests for the average maturity time calculations --- contracts/Hyperdrive.sol | 17 +++++++++++++---- test/Hyperdrive.t.sol | 32 +++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 461fbaf2a..5290b180e 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -824,8 +824,12 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @return bondReserves_ The bond reserves. /// @return lpTotalSupply The total supply of LP shares. /// @return sharePrice The share price. - /// @return longsOutstanding_ The longs that haven't been closed. - /// @return shortsOutstanding_ The shorts that haven't been closed. + /// @return longsOutstanding_ The outstanding longs that haven't matured. + /// @return longAverageMaturityTime_ The average maturity time of the + /// outstanding longs. + /// @return shortsOutstanding_ The outstanding shorts that haven't matured. + /// @return shortAverageMaturityTime_ The average maturity time of the + /// outstanding shorts. function getPoolInfo() external view @@ -835,7 +839,9 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { uint256 lpTotalSupply, uint256 sharePrice, uint256 longsOutstanding_, - uint256 shortsOutstanding_ + uint256 longAverageMaturityTime_, + uint256 shortsOutstanding_, + uint256 shortAverageMaturityTime_ ) { return ( @@ -844,7 +850,9 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { totalSupply[AssetId._LP_ASSET_ID], pricePerShare(), longsOutstanding, - shortsOutstanding + longAverageMaturityTime, + shortsOutstanding, + shortAverageMaturityTime ); } @@ -1143,6 +1151,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { .add(_positionAmount.mulDown(_positionMaturityTime)) .divDown(_positionsOutstanding.add(_positionAmount)); } else { + if (_positionsOutstanding == _positionAmount) return 0; return (_positionsOutstanding.mulDown(_averageMaturityTime)) .sub(_positionAmount.mulDown(_positionMaturityTime)) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 442acbd3f..16e37a156 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -99,7 +99,7 @@ contract HyperdriveTest is Test { hyperdrive.positionDuration(), hyperdrive.timeStretch() ); - assertApproxEqAbs(poolApr, apr, 1e1); // 17 decimals of precision + assertApproxEqAbs(poolApr, apr, 1); // 17 decimals of precision // Ensure that Alice's base balance has been depleted and that Alice // received some LP tokens. @@ -225,10 +225,16 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding + bondAmount ); + assertApproxEqAbs( + poolInfoAfter.longAverageMaturityTime, + maturityTime, + 1 + ); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); } /// Close Long /// @@ -370,10 +376,12 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding - bondAmount ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); } // TODO: Clean up these tests. @@ -440,10 +448,12 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding - bondAmount ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); } /// Open Short /// @@ -547,10 +557,16 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding + bondAmount ); + assertApproxEqAbs( + poolInfoAfter.shortAverageMaturityTime, + maturityTime, + 1 + ); } /// Close Short /// @@ -688,10 +704,12 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding - bondAmount ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); } // TODO: Clean up these tests. @@ -759,10 +777,12 @@ contract HyperdriveTest is Test { poolInfoAfter.longsOutstanding, poolInfoBefore.longsOutstanding ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); assertEq( poolInfoAfter.shortsOutstanding, poolInfoBefore.shortsOutstanding - bondAmount ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); } /// Utils /// @@ -784,7 +804,9 @@ contract HyperdriveTest is Test { uint256 lpTotalSupply; uint256 sharePrice; uint256 longsOutstanding; + uint256 longAverageMaturityTime; uint256 shortsOutstanding; + uint256 shortAverageMaturityTime; } function getPoolInfo() internal view returns (PoolInfo memory) { @@ -794,7 +816,9 @@ contract HyperdriveTest is Test { uint256 lpTotalSupply, uint256 sharePrice, uint256 longsOutstanding, - uint256 shortsOutstanding + uint256 longAverageMaturityTime, + uint256 shortsOutstanding, + uint256 shortAverageMaturityTime ) = hyperdrive.getPoolInfo(); return PoolInfo({ @@ -803,7 +827,9 @@ contract HyperdriveTest is Test { lpTotalSupply: lpTotalSupply, sharePrice: sharePrice, longsOutstanding: longsOutstanding, - shortsOutstanding: shortsOutstanding + longAverageMaturityTime: longAverageMaturityTime, + shortsOutstanding: shortsOutstanding, + shortAverageMaturityTime: shortAverageMaturityTime }); } }