From 3259c85d86c3bd1e2895c46b9ac75a95077be47b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Feb 2023 13:19:23 -0600 Subject: [PATCH 1/9] Added a fair LP allocation scheme --- contracts/Hyperdrive.sol | 18 +++++++-- contracts/libraries/HyperdriveMath.sol | 54 ++++++++++++++++++-------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 0abc32cf3..fc68ddca6 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -247,13 +247,25 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { ); // Calculate the amount of LP shares that the supplier should receive. + uint256 longAdjustment = HyperdriveMath.calculateLpAllocationAdjustment( + longsOutstanding, + longBaseVolume, + longAverageMaturityTime, + sharePrice + ); + uint256 shortAdjustment = HyperdriveMath + .calculateLpAllocationAdjustment( + shortsOutstanding, + shortBaseVolume, + shortAverageMaturityTime, + sharePrice + ); lpShares = HyperdriveMath.calculateLpSharesOutForSharesIn( shares, shareReserves, totalSupply[AssetId._LP_ASSET_ID], - longsOutstanding, - shortsOutstanding, - sharePrice + longAdjustment, + shortAdjustment ); // Enforce min user outputs diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 2e46fd626..6f27c3dd0 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -487,32 +487,54 @@ library HyperdriveMath { } } - // TODO: Use an allocation scheme that doesn't punish early LPs. - // /// @dev Calculates the amount of LP shares that should be awarded for /// supplying a specified amount of base shares to the pool. /// @param _shares The amount of base shares supplied to the pool. /// @param _shareReserves The pool's share reserves. /// @param _lpTotalSupply The pool's total supply of LP shares. - /// @param _longsOutstanding The amount of long positions outstanding. - /// @param _shortsOutstanding The amount of short positions outstanding. - /// @param _sharePrice The pool's share price. - /// @return The amount of LP shares awarded. + /// @param _longAdjustment A parameter denominated in base shares that + /// accounts for the duration risk that the LP takes on from longs. + /// @param _shortAdjustment A parameter denominated in base shares that + /// accounts for the duration risk that the LP takes on from shorts. + /// @param _shortAdjustment The amount of short positions outstanding. + /// @return lpShares The amount of LP shares awarded. function calculateLpSharesOutForSharesIn( uint256 _shares, uint256 _shareReserves, uint256 _lpTotalSupply, - uint256 _longsOutstanding, - uint256 _shortsOutstanding, + uint256 _longAdjustment, + uint256 _shortAdjustment + ) internal pure returns (uint256 lpShares) { + // lpShares = (dz * l) / (z + a_s - a_l) + lpShares = _shares.mulDown(_lpTotalSupply).divDown( + _shareReserves.add(_shortAdjustment).sub(_longAdjustment) + ); + return lpShares; + } + + /// @dev Computes the LP allocation adjustment for a position. This is used + /// to accurately account for the duration risk that LPs take on when + /// adding liquidtion to fairly reward LP shares. + /// @param _positionsOutstanding The position balance outstanding. + /// @param _baseVolume The base volume created by opening the positions. + /// @param _averageTimeRemaining The average time remaining of the positions. + /// @param _sharePrice The pool's share price. + /// @return adjustment The allocation adjustment. + function calculateLpAllocationAdjustment( + uint256 _positionsOutstanding, + uint256 _baseVolume, + uint256 _averageTimeRemaining, uint256 _sharePrice - ) internal pure returns (uint256) { - // (dz * l) / (z + b_y / c - b_x / c) - return - _shares.mulDown(_lpTotalSupply).divDown( - _shareReserves.add(_shortsOutstanding.divDown(_sharePrice)).sub( - _longsOutstanding.divDown(_sharePrice) - ) - ); + ) internal pure returns (uint256 adjustment) { + // baseAdjustment = t * _baseVolume + (1 - t) * _positionsOutstanding + adjustment = (_averageTimeRemaining.mulDown(_baseVolume)).add( + (FixedPointMath.ONE_18.sub(_averageTimeRemaining)).mulDown( + _positionsOutstanding + ) + ); + // adjustment = baseAdjustment / c + adjustment = adjustment.divDown(_sharePrice); + return adjustment; } /// @dev Calculates the amount of base shares released from burning a From 84005d1da5d33fadfcf00dccda5e821b58c43bed Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Feb 2023 13:43:58 -0600 Subject: [PATCH 2/9] Updated the base volume tracking to take into accounting backdating --- contracts/Hyperdrive.sol | 41 +++++++++++++++----------- contracts/libraries/Errors.sol | 1 + contracts/libraries/HyperdriveMath.sol | 28 +++++++++++++++++- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index fc68ddca6..ee6f68e52 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -136,6 +136,9 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { baseToken = _baseToken; // Initialize the time configurations. + if (_checkpointsPerTerm < 2) { + revert Errors.InvalidCheckpointsPerTerm(); + } positionDuration = _checkpointsPerTerm * _checkpointDuration; checkpointDuration = _checkpointDuration; timeStretch = _timeStretch; @@ -286,8 +289,6 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { _mint(AssetId._LP_ASSET_ID, _destination, lpShares); } - // TODO: Consider if some MEV protection is necessary for the LP. - // /// @notice Allows an LP to burn shares and withdraw from the pool. /// @param _shares The LP shares to burn. /// @param _minOutput The minium amount of the base token to receive. Note - this @@ -372,8 +373,10 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Withdraw the shares from the yield source. (uint256 baseOutput, ) = withdraw(shareProceeds, _destination); + // Enforce min user outputs if (_minOutput > baseOutput) revert Errors.OutputLimit(); + return (baseOutput, longWithdrawalShares, shortWithdrawalShares); } @@ -501,7 +504,8 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { poolBondDelta, sharePrice, latestCheckpoint, - maturityTime + maturityTime, + timeRemaining ); // Mint the bonds to the trader with an ID of the maturity time. @@ -696,15 +700,14 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { true ); - // TODO: Think about backdating more. This is only correct when the time - // remaining is one. - // // Update the base volume of short positions. - { - uint256 baseAmount = shareProceeds.mulDown(openSharePrice); - shortBaseVolume += baseAmount; - shortBaseVolumeCheckpoints[latestCheckpoint] += baseAmount; - } + uint256 baseVolume = HyperdriveMath.calculateBaseVolume( + shareProceeds.mulDown(openSharePrice), + _bondAmount, + timeRemaining + ); + shortBaseVolume += baseVolume; + shortBaseVolumeCheckpoints[latestCheckpoint] += baseVolume; // 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 @@ -953,6 +956,7 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { /// @param _sharePrice The share price. /// @param _checkpointTime The time of the latest checkpoint. /// @param _maturityTime The maturity time of the long. + /// @param _timeRemaining The time remaining until maturity. function _applyOpenLong( uint256 _baseAmount, uint256 _shareAmount, @@ -960,7 +964,8 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { uint256 _poolBondDelta, uint256 _sharePrice, uint256 _checkpointTime, - uint256 _maturityTime + uint256 _maturityTime, + uint256 _timeRemaining ) internal { // Update the average maturity time of long positions. longAverageMaturityTime = longAverageMaturityTime.updateWeightedAverage( @@ -970,12 +975,14 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { true ); - // TODO: Think about backdating more. This is only correct when the time - // remaining is one. - // // Update the base volume of long positions. - longBaseVolume += _baseAmount; - longBaseVolumeCheckpoints[_checkpointTime] += _baseAmount; + uint256 baseVolume = HyperdriveMath.calculateBaseVolume( + _baseAmount, + _bondProceeds, + _timeRemaining + ); + longBaseVolume += baseVolume; + longBaseVolumeCheckpoints[_checkpointTime] += baseVolume; // Apply the trading deltas to the reserves and update the amount of // longs outstanding. diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index 6defba758..29c3bf98e 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -14,6 +14,7 @@ library Errors { error BaseBufferExceedsShareReserves(); error InvalidCheckpointTime(); error InvalidCheckpointDuration(); + error InvalidCheckpointsPerTerm(); error InvalidMaturityTime(); error PoolAlreadyInitialized(); error TransferFailed(); diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 6f27c3dd0..d78d6a523 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -487,6 +487,32 @@ library HyperdriveMath { } } + /// @dev Calculates the base volume of an open trade given the base amount, + /// the bond amount, and the time remaining. Since the base amount + /// takes into account backdating, we can't use this as our base + /// volume. Since we linearly interpolate between the base volume + /// and the bond amount as the time remaining goes from 1 to 0, the + /// base volume is can be determined as follows: + /// + /// baseAmount = t * baseVolume + (1 - t) * bondAmount + /// => + /// baseVolume = (baseAmount - (1 - t) * bondAmount) / t + /// @param _baseAmount The base exchanged in the open trade. + /// @param _bondAmount The bonds exchanged in the open trade. + /// @param _timeRemaining The time remaining in the position. + function calculateBaseVolume( + uint256 _baseAmount, + uint256 _bondAmount, + uint256 _timeRemaining + ) internal pure returns (uint256 baseVolume) { + baseVolume = ( + _baseAmount.sub( + (FixedPointMath.ONE_18.sub(_timeRemaining)).mulDown(_bondAmount) + ) + ).divDown(_timeRemaining); + return baseVolume; + } + /// @dev Calculates the amount of LP shares that should be awarded for /// supplying a specified amount of base shares to the pool. /// @param _shares The amount of base shares supplied to the pool. @@ -514,7 +540,7 @@ library HyperdriveMath { /// @dev Computes the LP allocation adjustment for a position. This is used /// to accurately account for the duration risk that LPs take on when - /// adding liquidtion to fairly reward LP shares. + /// adding liquidity so that LP shares can be rewarded fairly. /// @param _positionsOutstanding The position balance outstanding. /// @param _baseVolume The base volume created by opening the positions. /// @param _averageTimeRemaining The average time remaining of the positions. From 87415abfae763e8a9bb597fb6552bf951070b3f1 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Feb 2023 17:01:45 -0600 Subject: [PATCH 3/9] Addressed review feedback from @jhrea --- contracts/Hyperdrive.sol | 5 +++-- contracts/libraries/HyperdriveMath.sol | 6 +++++- test/mocks/MockHyperdriveMath.sol | 10 ++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index ee6f68e52..a22daf524 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -135,8 +135,9 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Initialize the base token address. baseToken = _baseToken; - // Initialize the time configurations. - if (_checkpointsPerTerm < 2) { + // Initialize the time configurations. There must be at least one + // checkpoint per term to avoid having a position duration of zero. + if (_checkpointsPerTerm == 0) { revert Errors.InvalidCheckpointsPerTerm(); } positionDuration = _checkpointsPerTerm * _checkpointDuration; diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index d78d6a523..255e622bd 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -500,11 +500,16 @@ library HyperdriveMath { /// @param _baseAmount The base exchanged in the open trade. /// @param _bondAmount The bonds exchanged in the open trade. /// @param _timeRemaining The time remaining in the position. + /// @return baseVolume The calculated base volume. function calculateBaseVolume( uint256 _baseAmount, uint256 _bondAmount, uint256 _timeRemaining ) internal pure returns (uint256 baseVolume) { + // If the time remaining is 0, the position has already matured and + // doesn't have an impact on LP's ability to withdraw. This is a + // pathological case that should never arise. + if (_timeRemaining == 0) return 0; baseVolume = ( _baseAmount.sub( (FixedPointMath.ONE_18.sub(_timeRemaining)).mulDown(_bondAmount) @@ -522,7 +527,6 @@ library HyperdriveMath { /// accounts for the duration risk that the LP takes on from longs. /// @param _shortAdjustment A parameter denominated in base shares that /// accounts for the duration risk that the LP takes on from shorts. - /// @param _shortAdjustment The amount of short positions outstanding. /// @return lpShares The amount of LP shares awarded. function calculateLpSharesOutForSharesIn( uint256 _shares, diff --git a/test/mocks/MockHyperdriveMath.sol b/test/mocks/MockHyperdriveMath.sol index 800eb8a17..e97a9b207 100644 --- a/test/mocks/MockHyperdriveMath.sol +++ b/test/mocks/MockHyperdriveMath.sol @@ -224,17 +224,15 @@ contract MockHyperdriveMath { uint256 _shares, uint256 _shareReserves, uint256 _lpTotalSupply, - uint256 _longsOutstanding, - uint256 _shortsOutstanding, - uint256 _sharePrice + uint256 _longAdjustment, + uint256 _shortAdjustment ) external pure returns (uint256) { uint256 result = HyperdriveMath.calculateLpSharesOutForSharesIn( _shares, _shareReserves, _lpTotalSupply, - _longsOutstanding, - _shortsOutstanding, - _sharePrice + _longAdjustment, + _shortAdjustment ); return result; } From 4fa97af784bd99213934f996e0622f87d42cc197 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Feb 2023 22:50:58 -0600 Subject: [PATCH 4/9] Added a simple test for adding liquidity --- test/units/hyperdrive/AddLiquidityTest.t.sol | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/units/hyperdrive/AddLiquidityTest.t.sol b/test/units/hyperdrive/AddLiquidityTest.t.sol index 772b60a0a..e5da615e0 100644 --- a/test/units/hyperdrive/AddLiquidityTest.t.sol +++ b/test/units/hyperdrive/AddLiquidityTest.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.18; import { AssetId } from "contracts/libraries/AssetId.sol"; import { Errors } from "contracts/libraries/Errors.sol"; import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { HyperdriveTest } from "./HyperdriveTest.sol"; contract AddLiquidityTest is HyperdriveTest { @@ -22,4 +23,40 @@ contract AddLiquidityTest is HyperdriveTest { vm.expectRevert(Errors.ZeroAmount.selector); hyperdrive.addLiquidity(0, 0, bob); } + + function test_add_liquidity_identical_lp_shares() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + uint256 lpShares = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + + // Add liquidity with the same amount as the original contribution. + addLiquidity(bob, contribution); + + // Ensure that the contribution was transferred to Hyperdrive. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + baseBalance.add(contribution) + ); + + // Ensure that the new LP receives the same amount of LP shares as + // the initializer. + assertEq(hyperdrive.balanceOf(AssetId._LP_ASSET_ID, bob), lpShares); + assertEq(hyperdrive.totalSupply(AssetId._LP_ASSET_ID), lpShares * 2); + + // Ensure the pool APR is still approximately equal to the target APR. + uint256 poolApr = HyperdriveMath.calculateAPRFromReserves( + hyperdrive.shareReserves(), + hyperdrive.bondReserves(), + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + hyperdrive.initialSharePrice(), + hyperdrive.positionDuration(), + hyperdrive.timeStretch() + ); + assertApproxEqAbs(poolApr, apr, 1); + } } From d0068a4c1bf615ccaef8c617667d8283b312b63f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 20 Feb 2023 15:52:00 -0600 Subject: [PATCH 5/9] Fixed rebasing issues --- test/units/hyperdrive/AddLiquidityTest.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/units/hyperdrive/AddLiquidityTest.t.sol b/test/units/hyperdrive/AddLiquidityTest.t.sol index e5da615e0..8d054dd75 100644 --- a/test/units/hyperdrive/AddLiquidityTest.t.sol +++ b/test/units/hyperdrive/AddLiquidityTest.t.sol @@ -34,7 +34,7 @@ contract AddLiquidityTest is HyperdriveTest { uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); // Add liquidity with the same amount as the original contribution. - addLiquidity(bob, contribution); + addLiquidity(hyperdrive, bob, contribution); // Ensure that the contribution was transferred to Hyperdrive. assertEq(baseToken.balanceOf(bob), 0); From 774a7f47669d320a251d39f1dbc7860fcdb31c22 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 20 Feb 2023 19:01:34 -0600 Subject: [PATCH 6/9] Added tests for the fair LP allocation --- contracts/Hyperdrive.sol | 10 +- test/units/hyperdrive/AddLiquidityTest.t.sol | 158 ++++++++++++++++++- test/units/hyperdrive/HyperdriveTest.sol | 30 +++- 3 files changed, 184 insertions(+), 14 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index a22daf524..879398454 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -254,14 +254,14 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { uint256 longAdjustment = HyperdriveMath.calculateLpAllocationAdjustment( longsOutstanding, longBaseVolume, - longAverageMaturityTime, + _calculateTimeRemaining(longAverageMaturityTime), sharePrice ); uint256 shortAdjustment = HyperdriveMath .calculateLpAllocationAdjustment( shortsOutstanding, shortBaseVolume, - shortAverageMaturityTime, + _calculateTimeRemaining(shortAverageMaturityTime), sharePrice ); lpShares = HyperdriveMath.calculateLpSharesOutForSharesIn( @@ -272,9 +272,6 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { shortAdjustment ); - // Enforce min user outputs - if (_minOutput > lpShares) revert Errors.OutputLimit(); - // Update the reserves. shareReserves += shares; bondReserves = HyperdriveMath.calculateBondReserves( @@ -286,6 +283,9 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { timeStretch ); + // Enforce min user outputs + if (_minOutput > lpShares) revert Errors.OutputLimit(); + // Mint LP shares to the supplier. _mint(AssetId._LP_ASSET_ID, _destination, lpShares); } diff --git a/test/units/hyperdrive/AddLiquidityTest.t.sol b/test/units/hyperdrive/AddLiquidityTest.t.sol index 8d054dd75..623bfdebb 100644 --- a/test/units/hyperdrive/AddLiquidityTest.t.sol +++ b/test/units/hyperdrive/AddLiquidityTest.t.sol @@ -30,11 +30,11 @@ contract AddLiquidityTest is HyperdriveTest { // Initialize the pool with a large amount of capital. uint256 contribution = 500_000_000e18; initialize(alice, apr, contribution); - uint256 lpShares = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + uint256 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); // Add liquidity with the same amount as the original contribution. - addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); // Ensure that the contribution was transferred to Hyperdrive. assertEq(baseToken.balanceOf(bob), 0); @@ -45,8 +45,11 @@ contract AddLiquidityTest is HyperdriveTest { // Ensure that the new LP receives the same amount of LP shares as // the initializer. - assertEq(hyperdrive.balanceOf(AssetId._LP_ASSET_ID, bob), lpShares); - assertEq(hyperdrive.totalSupply(AssetId._LP_ASSET_ID), lpShares * 2); + assertEq(lpShares, lpSupplyBefore); + assertEq( + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + lpSupplyBefore * 2 + ); // Ensure the pool APR is still approximately equal to the target APR. uint256 poolApr = HyperdriveMath.calculateAPRFromReserves( @@ -59,4 +62,151 @@ contract AddLiquidityTest is HyperdriveTest { ); assertApproxEqAbs(poolApr, apr, 1); } + + function test_add_liquidity_with_long_immediately() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + uint256 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + + // Celine opens a long. + openLong(hyperdrive, celine, 50_000_000e18); + + // Add liquidity with the same amount as the original contribution. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + + // Ensure that the contribution was transferred to Hyperdrive. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + baseBalance.add(contribution) + ); + + // Ensure that the new LP receives the same amount of LP shares as + // the initializer. + assertEq(lpShares, lpSupplyBefore); + assertEq( + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + lpSupplyBefore * 2 + ); + + // Ensure the pool APR is still approximately equal to the target APR. + uint256 aprAfter = calculateAPRFromReserves(hyperdrive); + assertApproxEqAbs(aprAfter, aprBefore, 1); + } + + function test_add_liquidity_with_short_immediately() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + uint256 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + + // Celine opens a short. + openShort(hyperdrive, celine, 50_000_000e18); + + // Add liquidity with the same amount as the original contribution. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + + // Ensure that the contribution was transferred to Hyperdrive. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + baseBalance.add(contribution) + ); + + // Ensure that the new LP receives the same amount of LP shares as + // the initializer. + assertEq(lpShares, lpSupplyBefore); + assertEq( + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + lpSupplyBefore * 2 + ); + + // Ensure the pool APR is still approximately equal to the target APR. + uint256 aprAfter = calculateAPRFromReserves(hyperdrive); + assertApproxEqAbs(aprAfter, aprBefore, 1); + } + + function test_add_liquidity_with_long_at_maturity() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + + // Celine opens a long. + openLong(hyperdrive, celine, 50_000_000e18); + + // The term passes. + vm.warp(block.timestamp + POSITION_DURATION); + + // Add liquidity with the same amount as the original contribution. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + + // TODO: This suggests an issue with the flat+curve usage in the + // checkpointing mechanism. These APR figures should be the same. + // + // Ensure the pool APR hasn't decreased after adding liquidity. + uint256 aprAfter = calculateAPRFromReserves(hyperdrive); + assertGe(aprAfter, aprBefore); + + // Ensure that the contribution was transferred to Hyperdrive. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + baseBalance.add(contribution) + ); + + // Ensure that if the new LP withdraws, they get their money back. + uint256 withdrawalProceeds = removeLiquidity(hyperdrive, bob, lpShares); + assertApproxEqAbs(withdrawalProceeds, contribution, 1e9); + } + + function test_add_liquidity_with_short_at_maturity() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Celine opens a short. + openShort(hyperdrive, celine, 50_000_000e18); + + // The term passes. + vm.warp(block.timestamp + POSITION_DURATION); + + // Add liquidity with the same amount as the original contribution. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + + // TODO: This suggests an issue with the flat+curve usage in the + // checkpointing mechanism. These APR figures should be the same. + // + // Ensure the pool APR hasn't increased after adding liquidity. + uint256 aprAfter = calculateAPRFromReserves(hyperdrive); + assertLe(aprAfter, aprBefore); + + // Ensure that the contribution was transferred to Hyperdrive. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + baseBalance.add(contribution) + ); + + // Ensure that if the new LP withdraws, they get their money back. + uint256 withdrawalProceeds = removeLiquidity(hyperdrive, bob, lpShares); + assertApproxEqAbs(withdrawalProceeds, contribution, 1e9); + } } diff --git a/test/units/hyperdrive/HyperdriveTest.sol b/test/units/hyperdrive/HyperdriveTest.sol index eef220753..22b27918a 100644 --- a/test/units/hyperdrive/HyperdriveTest.sol +++ b/test/units/hyperdrive/HyperdriveTest.sol @@ -5,6 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; import { AssetId } from "contracts/libraries/AssetId.sol"; import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { ERC20Mintable } from "test/mocks/ERC20Mintable.sol"; import { MockHyperdrive } from "test/mocks/MockHyperdrive.sol"; @@ -65,22 +66,27 @@ contract HyperdriveTest is Test { hyperdrive.initialize(contribution, apr, lp); } - function addLiquidity(address lp, uint256 contribution) internal { + function addLiquidity(address lp, uint256 contribution) internal returns (uint256 lpShares) { vm.stopPrank(); vm.startPrank(lp); // Add liquidity to the pool. baseToken.mint(contribution); - baseToken.approve(address(hyperdrive), contribution); - hyperdrive.addLiquidity(contribution, 0, lp); + baseToken.approve(address(_hyperdrive), contribution); + _hyperdrive.addLiquidity(contribution, 0, lp); + + return _hyperdrive.balanceOf(AssetId._LP_ASSET_ID, lp); } - function removeLiquidity(address lp, uint256 shares) internal { + function removeLiquidity(address lp, uint256 shares) internal returns (uint256 baseProceeds) { vm.stopPrank(); vm.startPrank(lp); // Remove liquidity from the pool. - hyperdrive.removeLiquidity(shares, 0, lp); + uint256 baseBalanceBefore = baseToken.balanceOf(lp); + _hyperdrive.removeLiquidity(shares, 0, lp); + + return baseToken.balanceOf(lp) - baseBalanceBefore; } function openLong( @@ -200,6 +206,20 @@ contract HyperdriveTest is Test { }); } + function calculateAPRFromReserves( + MockHyperdrive _hyperdrive + ) internal view returns (uint256) { + return + HyperdriveMath.calculateAPRFromReserves( + _hyperdrive.shareReserves(), + _hyperdrive.bondReserves(), + _hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + _hyperdrive.initialSharePrice(), + _hyperdrive.positionDuration(), + _hyperdrive.timeStretch() + ); + } + function calculateAPRFromRealizedPrice( uint256 baseAmount, uint256 bondAmount, From 45bcee53836b207b0b7fe4398b468a580e3dd320 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 20 Feb 2023 20:08:14 -0600 Subject: [PATCH 7/9] Fix issues after rebase --- test/units/hyperdrive/AddLiquidityTest.t.sol | 22 ++++++++++---------- test/units/hyperdrive/HyperdriveTest.sol | 18 ++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/test/units/hyperdrive/AddLiquidityTest.t.sol b/test/units/hyperdrive/AddLiquidityTest.t.sol index 623bfdebb..46224dbf5 100644 --- a/test/units/hyperdrive/AddLiquidityTest.t.sol +++ b/test/units/hyperdrive/AddLiquidityTest.t.sol @@ -34,7 +34,7 @@ contract AddLiquidityTest is HyperdriveTest { uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); // Add liquidity with the same amount as the original contribution. - uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(bob, contribution); // Ensure that the contribution was transferred to Hyperdrive. assertEq(baseToken.balanceOf(bob), 0); @@ -72,12 +72,12 @@ contract AddLiquidityTest is HyperdriveTest { uint256 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); // Celine opens a long. - openLong(hyperdrive, celine, 50_000_000e18); + openLong(celine, 50_000_000e18); // Add liquidity with the same amount as the original contribution. uint256 aprBefore = calculateAPRFromReserves(hyperdrive); uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); - uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(bob, contribution); // Ensure that the contribution was transferred to Hyperdrive. assertEq(baseToken.balanceOf(bob), 0); @@ -108,12 +108,12 @@ contract AddLiquidityTest is HyperdriveTest { uint256 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); // Celine opens a short. - openShort(hyperdrive, celine, 50_000_000e18); + openShort(celine, 50_000_000e18); // Add liquidity with the same amount as the original contribution. uint256 aprBefore = calculateAPRFromReserves(hyperdrive); uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); - uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(bob, contribution); // Ensure that the contribution was transferred to Hyperdrive. assertEq(baseToken.balanceOf(bob), 0); @@ -144,7 +144,7 @@ contract AddLiquidityTest is HyperdriveTest { hyperdrive.totalSupply(AssetId._LP_ASSET_ID); // Celine opens a long. - openLong(hyperdrive, celine, 50_000_000e18); + openLong(celine, 50_000_000e18); // The term passes. vm.warp(block.timestamp + POSITION_DURATION); @@ -152,7 +152,7 @@ contract AddLiquidityTest is HyperdriveTest { // Add liquidity with the same amount as the original contribution. uint256 aprBefore = calculateAPRFromReserves(hyperdrive); uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); - uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(bob, contribution); // TODO: This suggests an issue with the flat+curve usage in the // checkpointing mechanism. These APR figures should be the same. @@ -169,7 +169,7 @@ contract AddLiquidityTest is HyperdriveTest { ); // Ensure that if the new LP withdraws, they get their money back. - uint256 withdrawalProceeds = removeLiquidity(hyperdrive, bob, lpShares); + uint256 withdrawalProceeds = removeLiquidity(bob, lpShares); assertApproxEqAbs(withdrawalProceeds, contribution, 1e9); } @@ -181,7 +181,7 @@ contract AddLiquidityTest is HyperdriveTest { initialize(alice, apr, contribution); // Celine opens a short. - openShort(hyperdrive, celine, 50_000_000e18); + openShort(celine, 50_000_000e18); // The term passes. vm.warp(block.timestamp + POSITION_DURATION); @@ -189,7 +189,7 @@ contract AddLiquidityTest is HyperdriveTest { // Add liquidity with the same amount as the original contribution. uint256 aprBefore = calculateAPRFromReserves(hyperdrive); uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); - uint256 lpShares = addLiquidity(hyperdrive, bob, contribution); + uint256 lpShares = addLiquidity(bob, contribution); // TODO: This suggests an issue with the flat+curve usage in the // checkpointing mechanism. These APR figures should be the same. @@ -206,7 +206,7 @@ contract AddLiquidityTest is HyperdriveTest { ); // Ensure that if the new LP withdraws, they get their money back. - uint256 withdrawalProceeds = removeLiquidity(hyperdrive, bob, lpShares); + uint256 withdrawalProceeds = removeLiquidity(bob, lpShares); assertApproxEqAbs(withdrawalProceeds, contribution, 1e9); } } diff --git a/test/units/hyperdrive/HyperdriveTest.sol b/test/units/hyperdrive/HyperdriveTest.sol index 22b27918a..259f4a1e4 100644 --- a/test/units/hyperdrive/HyperdriveTest.sol +++ b/test/units/hyperdrive/HyperdriveTest.sol @@ -66,25 +66,31 @@ contract HyperdriveTest is Test { hyperdrive.initialize(contribution, apr, lp); } - function addLiquidity(address lp, uint256 contribution) internal returns (uint256 lpShares) { + function addLiquidity( + address lp, + uint256 contribution + ) internal returns (uint256 lpShares) { vm.stopPrank(); vm.startPrank(lp); // Add liquidity to the pool. baseToken.mint(contribution); - baseToken.approve(address(_hyperdrive), contribution); - _hyperdrive.addLiquidity(contribution, 0, lp); + baseToken.approve(address(hyperdrive), contribution); + hyperdrive.addLiquidity(contribution, 0, lp); - return _hyperdrive.balanceOf(AssetId._LP_ASSET_ID, lp); + return hyperdrive.balanceOf(AssetId._LP_ASSET_ID, lp); } - function removeLiquidity(address lp, uint256 shares) internal returns (uint256 baseProceeds) { + function removeLiquidity( + address lp, + uint256 shares + ) internal returns (uint256 baseProceeds) { vm.stopPrank(); vm.startPrank(lp); // Remove liquidity from the pool. uint256 baseBalanceBefore = baseToken.balanceOf(lp); - _hyperdrive.removeLiquidity(shares, 0, lp); + hyperdrive.removeLiquidity(shares, 0, lp); return baseToken.balanceOf(lp) - baseBalanceBefore; } From 93ce29fbedb6ba88c3f533a20c9ca00835e1d9f9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 20 Feb 2023 22:40:00 -0600 Subject: [PATCH 8/9] Added unit tests for the checkpointing flow --- test/units/hyperdrive/CheckpointTest.t.sol | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/units/hyperdrive/CheckpointTest.t.sol diff --git a/test/units/hyperdrive/CheckpointTest.t.sol b/test/units/hyperdrive/CheckpointTest.t.sol new file mode 100644 index 000000000..49950c706 --- /dev/null +++ b/test/units/hyperdrive/CheckpointTest.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.18; + +import { Errors } from "contracts/libraries/Errors.sol"; +import { HyperdriveTest } from "./HyperdriveTest.sol"; + +contract CheckpointTest is HyperdriveTest { + function test_checkpoint_failure_future_checkpoint() external { + vm.expectRevert(Errors.InvalidCheckpointTime.selector); + hyperdrive.checkpoint(block.timestamp + CHECKPOINT_DURATION); + } + + function test_checkpoint_failure_invalid_checkpoint_time() external { + vm.expectRevert(Errors.InvalidCheckpointTime.selector); + hyperdrive.checkpoint(latestCheckpoint() + 1); + } + + function test_checkpoint_preset_checkpoint() external { + // Initialize the Hyperdrive pool. + initialize(alice, 0.05e18, 500_000_000e18); + + // Open a long and a short. + (, uint256 longAmount) = openLong(bob, 10_000_000e18); + uint256 shortAmount = 50_000e18; + openShort(celine, shortAmount); + + // Update the share price. Since the long and short were opened in this + // checkpoint, the checkpoint should be of the old checkpoint price. + uint256 sharePrice = getPoolInfo().sharePrice; + hyperdrive.setSharePrice(1.5e18); + + // Create a checkpoint. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + hyperdrive.checkpoint(latestCheckpoint()); + + // Ensure that the pool's APR wasn't changed by the checkpoint. + assertEq(calculateAPRFromReserves(hyperdrive), aprBefore); + + // Ensure that the checkpoint contains the share price prior to the + // share price update. + assertEq(hyperdrive.checkpoints(latestCheckpoint()), sharePrice); + + // Ensure that the long and short balance wasn't effected by the + // checkpoint (the long and short haven't matured yet). + assertEq(hyperdrive.longsOutstanding(), longAmount); + assertEq(hyperdrive.shortsOutstanding(), shortAmount); + } + + function test_checkpoint_latest_checkpoint() external { + // Initialize the Hyperdrive pool. + initialize(alice, 0.05e18, 500_000_000e18); + + // Advance a checkpoint. + vm.warp(block.timestamp + CHECKPOINT_DURATION); + + // Update the share price. Since the long and short were opened in this + // checkpoint, the checkpoint should be of the old checkpoint price. + uint256 sharePrice = 1.5e18; + hyperdrive.setSharePrice(sharePrice); + + // Create a checkpoint. + uint256 aprBefore = calculateAPRFromReserves(hyperdrive); + hyperdrive.checkpoint(latestCheckpoint()); + + // Ensure that the pool's APR wasn't changed by the checkpoint. + assertEq(calculateAPRFromReserves(hyperdrive), aprBefore); + + // Ensure that the checkpoint contains the latest share price. + assertEq(hyperdrive.checkpoints(latestCheckpoint()), sharePrice); + } + + function test_checkpoint_redemption() external { + // Initialize the Hyperdrive pool. + initialize(alice, 0.05e18, 500_000_000e18); + + // Open a long and a short. + openLong(bob, 10_000_000e18); + uint256 shortAmount = 50_000e18; + openShort(celine, shortAmount); + + // Advance a term. + vm.warp(block.timestamp + POSITION_DURATION); + + // Create a checkpoint. + hyperdrive.checkpoint(latestCheckpoint()); + + // TODO: This should be either removed or uncommented when we decide + // whether or not the flat+curve invariant should have an impact on + // the market rate. + // + // Ensure that the pool's APR wasn't changed by the checkpoint. + // assertEq(calculateAPRFromReserves(hyperdrive), aprBefore); + + // Ensure that the checkpoint contains the share price prior to the + // share price update. + assertEq( + hyperdrive.checkpoints(latestCheckpoint()), + getPoolInfo().sharePrice + ); + + // Ensure that the long and short balance has gone to zero (all of the + // matured positions have been closed). + assertEq(hyperdrive.longsOutstanding(), 0); + assertEq(hyperdrive.shortsOutstanding(), 0); + } +} From 4922ad6783003dc02cadf0f686f87a6fc80b0559 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 21 Feb 2023 11:59:31 -0600 Subject: [PATCH 9/9] Addressed review feedback from @aleph_v --- contracts/Hyperdrive.sol | 16 +++++++++------- contracts/libraries/HyperdriveMath.sol | 24 ------------------------ test/mocks/MockHyperdriveMath.sol | 17 ----------------- 3 files changed, 9 insertions(+), 48 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 879398454..b42e20b16 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -250,7 +250,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { timeStretch ); - // Calculate the amount of LP shares that the supplier should receive. + // To ensure that our LP allocation scheme fairly rewards LPs for adding + // liquidity, we linearly interpolate between the present and future + // value of longs and shorts. These interpolated values are the long and + // short adjustments. The following calculation is used to determine the + // amount of LP shares rewarded to new LP: + // + // lpShares = (dz * l) / (z + a_s - a_l) uint256 longAdjustment = HyperdriveMath.calculateLpAllocationAdjustment( longsOutstanding, longBaseVolume, @@ -264,12 +270,8 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { _calculateTimeRemaining(shortAverageMaturityTime), sharePrice ); - lpShares = HyperdriveMath.calculateLpSharesOutForSharesIn( - shares, - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - longAdjustment, - shortAdjustment + lpShares = shares.mulDown(totalSupply[AssetId._LP_ASSET_ID]).divDown( + shareReserves.add(shortAdjustment).sub(longAdjustment) ); // Update the reserves. diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 255e622bd..7bb603835 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -518,30 +518,6 @@ library HyperdriveMath { return baseVolume; } - /// @dev Calculates the amount of LP shares that should be awarded for - /// supplying a specified amount of base shares to the pool. - /// @param _shares The amount of base shares supplied to the pool. - /// @param _shareReserves The pool's share reserves. - /// @param _lpTotalSupply The pool's total supply of LP shares. - /// @param _longAdjustment A parameter denominated in base shares that - /// accounts for the duration risk that the LP takes on from longs. - /// @param _shortAdjustment A parameter denominated in base shares that - /// accounts for the duration risk that the LP takes on from shorts. - /// @return lpShares The amount of LP shares awarded. - function calculateLpSharesOutForSharesIn( - uint256 _shares, - uint256 _shareReserves, - uint256 _lpTotalSupply, - uint256 _longAdjustment, - uint256 _shortAdjustment - ) internal pure returns (uint256 lpShares) { - // lpShares = (dz * l) / (z + a_s - a_l) - lpShares = _shares.mulDown(_lpTotalSupply).divDown( - _shareReserves.add(_shortAdjustment).sub(_longAdjustment) - ); - return lpShares; - } - /// @dev Computes the LP allocation adjustment for a position. This is used /// to accurately account for the duration risk that LPs take on when /// adding liquidity so that LP shares can be rewarded fairly. diff --git a/test/mocks/MockHyperdriveMath.sol b/test/mocks/MockHyperdriveMath.sol index e97a9b207..2c057c9f1 100644 --- a/test/mocks/MockHyperdriveMath.sol +++ b/test/mocks/MockHyperdriveMath.sol @@ -220,23 +220,6 @@ contract MockHyperdriveMath { return (result1, result2); } - function calculateLpSharesOutForSharesIn( - uint256 _shares, - uint256 _shareReserves, - uint256 _lpTotalSupply, - uint256 _longAdjustment, - uint256 _shortAdjustment - ) external pure returns (uint256) { - uint256 result = HyperdriveMath.calculateLpSharesOutForSharesIn( - _shares, - _shareReserves, - _lpTotalSupply, - _longAdjustment, - _shortAdjustment - ); - return result; - } - function calculateOutForLpSharesIn( uint256 _shares, uint256 _shareReserves,