diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 0abc32cf3..b42e20b16 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -135,7 +135,11 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { // Initialize the base token address. baseToken = _baseToken; - // Initialize the time configurations. + // 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; checkpointDuration = _checkpointDuration; timeStretch = _timeStretch; @@ -246,18 +250,29 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { timeStretch ); - // Calculate the amount of LP shares that the supplier should receive. - lpShares = HyperdriveMath.calculateLpSharesOutForSharesIn( - shares, - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], + // 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, - shortsOutstanding, + longBaseVolume, + _calculateTimeRemaining(longAverageMaturityTime), sharePrice ); - - // Enforce min user outputs - if (_minOutput > lpShares) revert Errors.OutputLimit(); + uint256 shortAdjustment = HyperdriveMath + .calculateLpAllocationAdjustment( + shortsOutstanding, + shortBaseVolume, + _calculateTimeRemaining(shortAverageMaturityTime), + sharePrice + ); + lpShares = shares.mulDown(totalSupply[AssetId._LP_ASSET_ID]).divDown( + shareReserves.add(shortAdjustment).sub(longAdjustment) + ); // Update the reserves. shareReserves += shares; @@ -270,12 +285,13 @@ 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); } - // 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 @@ -360,8 +376,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); } @@ -489,7 +507,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. @@ -684,15 +703,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 @@ -941,6 +959,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, @@ -948,7 +967,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( @@ -958,12 +978,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 2e46fd626..7bb603835 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -487,32 +487,60 @@ 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. + /// @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. + /// @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) + ) + ).divDown(_timeRemaining); + return baseVolume; + } + + /// @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. + /// @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 The amount of LP shares awarded. - function calculateLpSharesOutForSharesIn( - uint256 _shares, - uint256 _shareReserves, - uint256 _lpTotalSupply, - uint256 _longsOutstanding, - uint256 _shortsOutstanding, + /// @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 diff --git a/test/mocks/MockHyperdriveMath.sol b/test/mocks/MockHyperdriveMath.sol index 800eb8a17..2c057c9f1 100644 --- a/test/mocks/MockHyperdriveMath.sol +++ b/test/mocks/MockHyperdriveMath.sol @@ -220,25 +220,6 @@ contract MockHyperdriveMath { return (result1, result2); } - function calculateLpSharesOutForSharesIn( - uint256 _shares, - uint256 _shareReserves, - uint256 _lpTotalSupply, - uint256 _longsOutstanding, - uint256 _shortsOutstanding, - uint256 _sharePrice - ) external pure returns (uint256) { - uint256 result = HyperdriveMath.calculateLpSharesOutForSharesIn( - _shares, - _shareReserves, - _lpTotalSupply, - _longsOutstanding, - _shortsOutstanding, - _sharePrice - ); - return result; - } - function calculateOutForLpSharesIn( uint256 _shares, uint256 _shareReserves, diff --git a/test/units/hyperdrive/AddLiquidityTest.t.sol b/test/units/hyperdrive/AddLiquidityTest.t.sol index 772b60a0a..46224dbf5 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,190 @@ 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 lpSupplyBefore = hyperdrive.totalSupply(AssetId._LP_ASSET_ID); + uint256 baseBalance = baseToken.balanceOf(address(hyperdrive)); + + // Add liquidity with the same amount as the original contribution. + uint256 lpShares = 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(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( + hyperdrive.shareReserves(), + hyperdrive.bondReserves(), + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + hyperdrive.initialSharePrice(), + hyperdrive.positionDuration(), + hyperdrive.timeStretch() + ); + 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(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(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(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(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(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(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(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(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(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(bob, lpShares); + assertApproxEqAbs(withdrawalProceeds, contribution, 1e9); + } } 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); + } +} diff --git a/test/units/hyperdrive/HyperdriveTest.sol b/test/units/hyperdrive/HyperdriveTest.sol index eef220753..259f4a1e4 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,7 +66,10 @@ 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); @@ -73,14 +77,22 @@ contract HyperdriveTest is Test { baseToken.mint(contribution); 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. + uint256 baseBalanceBefore = baseToken.balanceOf(lp); hyperdrive.removeLiquidity(shares, 0, lp); + + return baseToken.balanceOf(lp) - baseBalanceBefore; } function openLong( @@ -200,6 +212,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,